@lazyneoaz/metachat 1.0.0 → 1.0.2

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.0",
3
+ "version": "1.0.2",
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.",
@@ -8,8 +8,7 @@
8
8
  "files": [
9
9
  "index.js",
10
10
  "src/",
11
- "LICENSE",
12
- "README.md"
11
+ "LICENSE"
13
12
  ],
14
13
  "repository": {
15
14
  "type": "git",
@@ -41,18 +40,18 @@
41
40
  "axios": "^1.13.5",
42
41
  "axios-cookiejar-support": "^4.0.7",
43
42
  "bluebird": "^3.7.2",
44
- "cheerio": "^1.0.0",
43
+ "cheerio": "^1.0.0-rc.12",
45
44
  "cli-progress": "^3.12.0",
46
45
  "deepdash": "^5.3.9",
47
46
  "duplexify": "^4.1.3",
48
47
  "form-data": "^4.0.4",
49
- "gradient-string": "^3.0.0",
48
+ "gradient-string": "^2.0.2",
50
49
  "https-proxy-agent": "^7.0.6",
51
50
  "jsonpath-plus": "^10.3.0",
52
51
  "lodash": "^4.17.21",
53
52
  "mqtt": "^4.3.8",
54
53
  "node-cron": "^3.0.3",
55
- "ora": "^9.4.0",
54
+ "ora": "^5.4.1",
56
55
  "picocolors": "^1.1.1",
57
56
  "sequelize": "^6.37.5",
58
57
  "sqlite3": "^5.1.7",
@@ -22,10 +22,10 @@ module.exports = function (defaultFuncs, api, ctx) {
22
22
  return function enableAutoSaveAppState(options) {
23
23
  options = options || {};
24
24
  const filePath = options.filePath || path.join(process.cwd(), "appstate.json");
25
- const interval = options.interval || 10 * 60 * 1000;
25
+ const interval = options.interval || 5 * 60 * 1000; // 5 min default (was 10 min)
26
26
  const saveOnLogin = options.saveOnLogin !== false;
27
27
 
28
- function saveAppState() {
28
+ async function saveAppState() {
29
29
  try {
30
30
  const appState = api.getAppState ? api.getAppState() : null;
31
31
  if (!appState || (Array.isArray(appState) && appState.length === 0)) {
@@ -33,9 +33,20 @@ module.exports = function (defaultFuncs, api, ctx) {
33
33
  }
34
34
  const payload = Array.isArray(appState) ? appState : (appState.appState || appState);
35
35
  fs.writeFileSync(filePath, JSON.stringify(payload, null, 2), "utf8");
36
- } catch (err) {
37
- // Silently ignore write errors (e.g. read-only FS in some environments)
36
+ } catch (_) {
37
+ // Silently ignore file-write errors (e.g. read-only FS)
38
38
  }
39
+
40
+ // Keep the database backup in sync so re-login always uses fresh cookies.
41
+ try {
42
+ const { backupAppStateSQL } = require('../database/appStateBackup');
43
+ const jar = (ctx && ctx.jar) ? ctx.jar : require('../utils').getJar();
44
+ const userID = (ctx && ctx.userID) ? ctx.userID :
45
+ (typeof api.getCurrentUserID === 'function' ? api.getCurrentUserID() : null);
46
+ if (jar && userID) {
47
+ await backupAppStateSQL(jar, userID);
48
+ }
49
+ } catch (_) {}
39
50
  }
40
51
 
41
52
  let immediateTimer = null;
@@ -54,7 +54,7 @@ async function buildAPI(html, jar, netData, globalOptions, fbLinkFunc, errorRetr
54
54
 
55
55
  const dtsgResult = {
56
56
  fb_dtsg: dtsg,
57
- jazoest: `2${Array.from(dtsg).reduce((a, b) => a + b.charCodeAt(0), '')}`,
57
+ jazoest: `2${Array.from(dtsg).reduce((a, b) => a + b.charCodeAt(0), 0)}`,
58
58
  lsd: lsd
59
59
  };
60
60
 
@@ -206,7 +206,19 @@ async function loginHelper(credentials, globalOptions, callback, setOptionsFunc,
206
206
  globalAntiSuspension.enableWarmup();
207
207
  } catch (_) {}
208
208
 
209
- const resp = await utils.get(fbLinkFunc(), jar, null, globalOptions, { noRef: true }).then(utils.saveCookies(jar));
209
+ let resp = await utils.get(fbLinkFunc(), jar, null, globalOptions, { noRef: true }).then(utils.saveCookies(jar));
210
+
211
+ // Auto-dismiss the FBScrapingWarning checkpoint before building the API.
212
+ // If left unhandled this checkpoint escalates to a permanent account ban.
213
+ try {
214
+ const { bypassScrapingWarning } = require('../../utils/checkpointBypass');
215
+ const bypassedResp = await bypassScrapingWarning(jar, globalOptions, null, resp);
216
+ if (bypassedResp) {
217
+ resp = bypassedResp;
218
+ utils.log("LoginHelper", "Session restored after scraping-warning bypass");
219
+ }
220
+ } catch (_) {}
221
+
210
222
  const extractNetData = (html) => {
211
223
  const allScriptsData = [];
212
224
  const scriptRegex = /<script type="application\/json"[^>]*>(.*?)<\/script>/g;
@@ -403,9 +415,11 @@ async function loginHelper(credentials, globalOptions, callback, setOptionsFunc,
403
415
  // Use the lightweight presence endpoint instead of fetching the
404
416
  // full homepage (~400 kB). Returns 200 JSON when authenticated,
405
417
  // 302→login when the session is expired.
418
+ // Do NOT send fb_dtsg_ag= (empty) — real browsers always send a
419
+ // real token here, so an empty value is a bot fingerprint.
406
420
  const resp = await utils.get(
407
- 'https://www.facebook.com/ajax/presence/reconnect.php?reason=14&fb_dtsg_ag=&__a=1',
408
- ctx.jar, null, ctx.globalOptions, { noRef: true }
421
+ 'https://www.facebook.com/ajax/presence/reconnect.php',
422
+ ctx.jar, { reason: '14', __a: '1' }, ctx.globalOptions, { noRef: true, _skipSessionInspect: true }
409
423
  );
410
424
  const html = resp.body || '';
411
425
 
@@ -2,7 +2,7 @@
2
2
  import { ReadStream } from "fs";
3
3
  import { EventEmitter } from "events";
4
4
 
5
- declare module "dhoner-fca" {
5
+ declare module "@lazyneoaz/metachat" {
6
6
  export type UserID = string;
7
7
  export type ThreadID = string;
8
8
  export type MessageID = string;
@@ -83,8 +83,8 @@ class AntiSuspension {
83
83
  this.lastActivity = new Map();
84
84
  this.typing = new Map();
85
85
 
86
- this.messageDelayMs = 100;
87
- this.threadDelayMs = 200;
86
+ this.messageDelayMs = 300;
87
+ this.threadDelayMs = 400;
88
88
  this.loginAttempts = 0;
89
89
  this.maxLoginAttempts = 3;
90
90
  this.loginCooldown = 300000;
@@ -279,9 +279,11 @@ class AntiSuspension {
279
279
  }
280
280
 
281
281
  async addSmartDelay() {
282
- const base = 30 + Math.random() * 70;
283
- const jitter = (Math.random() - 0.5) * 20;
284
- const total = Math.max(20, base + jitter);
282
+ // Minimum realistic inter-request pause short enough not to block
283
+ // high-throughput flows but long enough to avoid sub-100ms bot patterns.
284
+ const base = 500 + Math.random() * 700;
285
+ const jitter = (Math.random() - 0.5) * 150;
286
+ const total = Math.max(350, base + jitter);
285
287
  await new Promise(resolve => setTimeout(resolve, total));
286
288
  }
287
289
 
@@ -293,14 +295,14 @@ class AntiSuspension {
293
295
  const threadCount = this.dailyStats.threadStats.get(String(threadID))?.count || 0;
294
296
  const globalCount = this.dailyStats.messageCount;
295
297
 
296
- let base = 30;
297
- if (globalCount > 800) base = 150;
298
- else if (globalCount > 400) base = 80;
298
+ let base = 120;
299
+ if (globalCount > 800) base = 500;
300
+ else if (globalCount > 400) base = 250;
299
301
 
300
- if (threadCount > 50) base += 50;
302
+ if (threadCount > 50) base += 180;
301
303
 
302
304
  const jitter = Math.random() * base * 0.4;
303
- const total = Math.max(20, base + jitter);
305
+ const total = Math.max(80, base + jitter);
304
306
  await new Promise(resolve => setTimeout(resolve, total));
305
307
  }
306
308
 
@@ -320,7 +322,7 @@ class AntiSuspension {
320
322
 
321
323
  async enforceMessageRate() {
322
324
  await new Promise(resolve =>
323
- setTimeout(resolve, this.messageDelayMs + Math.random() * 50)
325
+ setTimeout(resolve, this.messageDelayMs + Math.random() * 300)
324
326
  );
325
327
  }
326
328
 
@@ -111,18 +111,19 @@ async function inspectResponseForSessionIssues(adapted, ctx) {
111
111
 
112
112
  if (!ctx.auto_login && typeof ctx.performAutoLogin === 'function') {
113
113
  ctx.auto_login = true;
114
+ // Always reset the flag in a finally block so a synchronous throw
115
+ // or a rejected promise can never leave auto_login stuck at true,
116
+ // which would permanently prevent future re-login attempts.
114
117
  try {
115
118
  const ok = await ctx.performAutoLogin();
116
- ctx.auto_login = false;
117
119
  if (!ok) {
118
120
  const err = new Error('Not logged in. Auto re-login failed.');
119
121
  err.error = 'Not logged in.';
120
122
  err.res = body;
121
123
  throw err;
122
124
  }
123
- } catch (autoErr) {
125
+ } finally {
124
126
  ctx.auto_login = false;
125
- throw autoErr;
126
127
  }
127
128
  } else {
128
129
  const err = new Error('Not logged in. Session has expired.');
@@ -0,0 +1,138 @@
1
+ "use strict";
2
+
3
+ const utils = require('./index');
4
+
5
+ /**
6
+ * Detects and auto-dismisses Facebook's FBScrapingWarning checkpoint.
7
+ *
8
+ * This checkpoint (at /checkpoint/601051028565049) is triggered when Facebook
9
+ * suspects automated access. If left unhandled it escalates to a permanent
10
+ * account suspension. The fix is to POST the FBScrapingWarningMutation GraphQL
11
+ * call with doc_id 6339492849481770, which silently acknowledges the warning.
12
+ *
13
+ * @param {object} jar - The cookie jar.
14
+ * @param {object} globalOptions - Global request options.
15
+ * @param {string|null} userID - User ID (extracted from cookies/HTML if omitted).
16
+ * @param {object|null} existingResp - An already-fetched homepage response to inspect
17
+ * first. When provided, a fresh fetch is skipped
18
+ * unless the checkpoint is actually detected.
19
+ * @returns {Promise<object|null>} The refreshed response after bypass, or null if no
20
+ * checkpoint was present.
21
+ */
22
+ async function bypassScrapingWarning(jar, globalOptions, userID = null, existingResp = null) {
23
+ try {
24
+ const toStr = (x) => (typeof x === 'string' ? x : JSON.stringify(x || ""));
25
+
26
+ const isCp = (resp) => {
27
+ if (!resp) return false;
28
+ const body = toStr(resp.body || resp.data || "");
29
+ return (
30
+ body.includes('checkpoint/601051028565049') ||
31
+ body.includes('XCheckpointFBScrapingWarningController')
32
+ );
33
+ };
34
+
35
+ // Use the existing response if provided, otherwise fetch the homepage.
36
+ let initial = existingResp || null;
37
+ if (!initial) {
38
+ initial = await utils.get(
39
+ 'https://www.facebook.com/',
40
+ jar, null, globalOptions,
41
+ { noRef: true, _skipSessionInspect: true }
42
+ );
43
+ utils.saveCookies(jar)(initial);
44
+ }
45
+
46
+ if (!isCp(initial)) {
47
+ return null;
48
+ }
49
+
50
+ utils.warn("BYPASS", "FBScrapingWarning checkpoint detected — auto-dismissing...");
51
+
52
+ const html = toStr(initial.body || initial.data || "");
53
+
54
+ // --- Extract user ID ---
55
+ let uid = userID || null;
56
+ if (!uid) {
57
+ try {
58
+ const cookies = jar.getCookiesSync('https://www.facebook.com');
59
+ const pick = (key) => (cookies.find(c => c.key === key) || {}).value;
60
+ uid = pick('i_user') || pick('c_user') || null;
61
+ } catch (_) {}
62
+ }
63
+ if (!uid) {
64
+ const m = html.match(/"USER_ID"\s*:\s*"(\d+)"/) ||
65
+ html.match(/\["CurrentUserInitialData",\[\],\{[^}]*"USER_ID":"(\d+)"/);
66
+ if (m) uid = m[1];
67
+ }
68
+
69
+ // --- Extract CSRF tokens ---
70
+ const grab = (patterns) => {
71
+ for (const re of patterns) {
72
+ const m = html.match(re);
73
+ if (m && m[1]) return m[1];
74
+ }
75
+ return null;
76
+ };
77
+
78
+ const fb_dtsg = grab([
79
+ /"DTSGInitData",\[\],\{"token":"([^"]+)"/,
80
+ /name="fb_dtsg"\s+value="([^"]+)"/,
81
+ /"DTSGInitialData",\[\],\{"token":"([^"]+)"/
82
+ ]);
83
+ const jazoest = grab([
84
+ /name="jazoest"\s+value="([^"]+)"/,
85
+ /jazoest=(\d+)/
86
+ ]);
87
+ const lsd = grab([
88
+ /"LSD",\[\],\{"token":"([^"]+)"/,
89
+ /name="lsd"\s+value="([^"]+)"/
90
+ ]);
91
+
92
+ if (!fb_dtsg || !uid) {
93
+ utils.warn("BYPASS", `Cannot bypass — missing tokens (uid=${uid ? 'ok' : 'missing'}, fb_dtsg=${fb_dtsg ? 'ok' : 'missing'})`);
94
+ return initial;
95
+ }
96
+
97
+ // --- POST FBScrapingWarningMutation ---
98
+ const form = {
99
+ av: uid,
100
+ fb_dtsg,
101
+ jazoest: jazoest || "",
102
+ lsd: lsd || "",
103
+ fb_api_caller_class: "RelayModern",
104
+ fb_api_req_friendly_name: "FBScrapingWarningMutation",
105
+ variables: "{}",
106
+ server_timestamps: "true",
107
+ doc_id: "6339492849481770"
108
+ };
109
+
110
+ const mutResp = await utils.post(
111
+ 'https://www.facebook.com/api/graphql/',
112
+ jar, form, globalOptions,
113
+ { noRef: true, _skipSessionInspect: true }
114
+ );
115
+ utils.saveCookies(jar)(mutResp);
116
+
117
+ // --- Re-fetch homepage to confirm checkpoint is gone ---
118
+ const refreshed = await utils.get(
119
+ 'https://www.facebook.com/',
120
+ jar, null, globalOptions,
121
+ { noRef: true, _skipSessionInspect: true }
122
+ );
123
+ utils.saveCookies(jar)(refreshed);
124
+
125
+ if (isCp(refreshed)) {
126
+ utils.warn("BYPASS", "Checkpoint still present after FBScrapingWarningMutation — may need manual intervention");
127
+ } else {
128
+ utils.log("BYPASS", "FBScrapingWarning checkpoint dismissed. Session restored.");
129
+ }
130
+
131
+ return refreshed;
132
+ } catch (err) {
133
+ utils.error("BYPASS", `bypassScrapingWarning error: ${err && err.message ? err.message : String(err)}`);
134
+ return null;
135
+ }
136
+ }
137
+
138
+ module.exports = { bypassScrapingWarning };
@@ -95,6 +95,11 @@ class TokenRefreshManager {
95
95
  probeCtx
96
96
  );
97
97
 
98
+ // Save any rotated cookies Facebook returns (fr, xs, etc.) so the
99
+ // jar stays fresh. Dropping rotated cookies causes Facebook to
100
+ // treat the session as stale and forces a logout.
101
+ try { utils.saveCookies(ctx.jar)(resp); } catch (_) {}
102
+
98
103
  const body = resp.body;
99
104
  if (!body) return false;
100
105
 
@@ -140,13 +145,29 @@ class TokenRefreshManager {
140
145
  const RETRY_DELAYS = [2000, 5000, 10000];
141
146
 
142
147
  try {
143
- const resp = await utils.get(fbLink, ctx.jar, null, ctx.globalOptions, { noRef: true });
144
-
145
- const html = resp.body;
148
+ let resp = await utils.get(fbLink, ctx.jar, null, ctx.globalOptions, { noRef: true });
149
+ utils.saveCookies(ctx.jar)(resp);
150
+
151
+ let html = resp.body;
146
152
  if (!html) {
147
153
  throw new Error("Empty response from Facebook");
148
154
  }
149
155
 
156
+ // Auto-dismiss FBScrapingWarning before any other checks
157
+ const isScrapingWarning = html.includes('XCheckpointFBScrapingWarningController') ||
158
+ html.includes('checkpoint/601051028565049');
159
+ if (isScrapingWarning) {
160
+ try {
161
+ const { bypassScrapingWarning } = require('./checkpointBypass');
162
+ const bypassed = await bypassScrapingWarning(ctx.jar, ctx.globalOptions, ctx.userID, resp);
163
+ if (bypassed) {
164
+ resp = bypassed;
165
+ html = resp.body || html;
166
+ utils.log("TokenRefresh", "Scraping-warning checkpoint dismissed during token refresh");
167
+ }
168
+ } catch (_) {}
169
+ }
170
+
150
171
  // Precise check - broad html.includes("login") is a false positive because
151
172
  // Facebook includes the word "login" all over authenticated pages too.
152
173
  const isLoginPage = html.includes('<form id="login_form"') ||
@@ -172,6 +193,9 @@ class TokenRefreshManager {
172
193
  for (let i = 0; i < ctx.fb_dtsg.length; i++) {
173
194
  ctx.ttstamp += ctx.fb_dtsg.charCodeAt(i);
174
195
  }
196
+ // Recalculate jazoest whenever fb_dtsg changes — it must stay in sync.
197
+ // jazoest = "2" + sum-of-char-codes (numeric sum, not concatenation).
198
+ ctx.jazoest = `2${Array.from(ctx.fb_dtsg).reduce((a, b) => a + b.charCodeAt(0), 0)}`;
175
199
  } else {
176
200
  throw new Error("Failed to extract fb_dtsg token");
177
201
  }
@@ -193,12 +217,19 @@ class TokenRefreshManager {
193
217
 
194
218
  this.lastRefresh = Date.now();
195
219
  this.failureCount = 0;
220
+
221
+ // Persist fresh cookies and update the DB backup so re-login
222
+ // attempts always use the newest session state.
196
223
  try {
197
224
  if (globalAutoReLoginManager && globalAutoReLoginManager.isEnabled && globalAutoReLoginManager.isEnabled()) {
198
225
  const appState = utils.getAppState(ctx.jar);
199
226
  globalAutoReLoginManager.updateAppState(appState);
200
227
  }
201
228
  } catch (_) {}
229
+ try {
230
+ const { backupAppStateSQL } = require('../database/appStateBackup');
231
+ await backupAppStateSQL(ctx.jar, ctx.userID);
232
+ } catch (_) {}
202
233
  return true;
203
234
  } catch (error) {
204
235
  this.failureCount++;