@eryxenx/fca 1.0.0 → 1.0.4

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.
@@ -872,6 +872,19 @@ function loginHelper(appState, Cookie, email, password, globalOptions, callback)
872
872
  };
873
873
  if (appState || Cookie) {
874
874
  const initial = await get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
875
+ // Sanity-check the response actually looks like a real FB page load
876
+ // for the current UA (checked via a marker only present in that
877
+ // rendering). If not, the appstate/UA combo may be mismatched from
878
+ // how it was originally captured — retry once with a known-good UA
879
+ // rather than pushing forward with a session that already looks
880
+ // inconsistent to Facebook.
881
+ const bodyStr = typeof initial?.data === "string" ? initial.data : String(initial?.data ?? "");
882
+ if (!/MPageLoadClientMetrics/.test(bodyStr)) {
883
+ logger("Initial page load doesn't match expected UA rendering — retrying with fallback UA", "warn");
884
+ globalOptions.userAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/125.0.0.0 Safari/537.36";
885
+ const retryInitial = await get("https://www.facebook.com/", jar, null, globalOptions).then(saveCookies(jar));
886
+ return (await ctx.bypassAutomation(retryInitial, jar)) || retryInitial;
887
+ }
875
888
  return (await ctx.bypassAutomation(initial, jar)) || initial;
876
889
  }
877
890
  const hydrated = await hydrateJarFromDB(null);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@eryxenx/fca",
3
- "version": "1.0.0",
3
+ "version": "1.0.4",
4
4
  "description": "Facebook Chat API by EryXenX | Stable • Auto Re-login • Full E2EE Support — send messages, media, reactions & more in encrypted chats, hassle-free",
5
5
  "main": "index.js",
6
6
  "types": "index.d.ts",
@@ -0,0 +1,50 @@
1
+ "use strict";
2
+
3
+ const log = require("../../../func/logAdapter");
4
+ const { parseAndCheckLogin } = require("../../utils/client");
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return function dismissScrapingWarning(callback) {
8
+ let resolveFunc = function () {};
9
+ let rejectFunc = function () {};
10
+ const returnPromise = new Promise(function (resolve, reject) {
11
+ resolveFunc = resolve;
12
+ rejectFunc = reject;
13
+ });
14
+
15
+ if (!callback) {
16
+ callback = function (err, data) {
17
+ if (err) return rejectFunc(err);
18
+ resolveFunc(data);
19
+ };
20
+ }
21
+
22
+ const form = {
23
+ av: ctx.userID,
24
+ __user: ctx.userID,
25
+ fb_dtsg: ctx.fb_dtsg || "",
26
+ fb_api_caller_class: "RelayModern",
27
+ fb_api_req_friendly_name: "FBScrapingWarningMutation",
28
+ server_timestamps: "true",
29
+ doc_id: "6339492849481770",
30
+ variables: "{}"
31
+ };
32
+
33
+ defaultFuncs
34
+ .post("https://www.facebook.com/api/graphql/", ctx.jar, form)
35
+ .then(parseAndCheckLogin(ctx, defaultFuncs))
36
+ .then(function (resData) {
37
+ if (resData && resData.errors) throw resData;
38
+ log.info("dismissScrapingWarning", "Dismissed automated-behavior warning");
39
+ return callback(null, { success: true });
40
+ })
41
+ .catch(function (err) {
42
+ // Non-fatal: worst case the warning just stays on-screen, same as
43
+ // if a human never tapped Dismiss.
44
+ log.warn("dismissScrapingWarning", err && err.message ? err.message : String(err));
45
+ return callback(err);
46
+ });
47
+
48
+ return returnPromise;
49
+ };
50
+ };
@@ -3,6 +3,7 @@
3
3
  var utils = require("../../utils/nexca-utils");
4
4
  var logger = require("../../utils/nexca-logger");
5
5
  var bluebird = require("bluebird");
6
+ var { globalAntiSuspension } = require("../../utils/antiSuspension");
6
7
 
7
8
  var allowedProperties = {
8
9
  attachment: true, url: true, sticker: true, emoji: true,
@@ -31,7 +32,7 @@ module.exports = function (defaultFuncs, api, ctx) {
31
32
  }
32
33
  bluebird.all(uploads)
33
34
  .then(resData => callback(null, resData))
34
- .catch(err => { logger.error("OldMessage.upload", err); callback(err); });
35
+ .catch(err => { globalAntiSuspension.checkAccountHealth(err); logger.error("OldMessage.upload", err); callback(err); });
35
36
  }
36
37
 
37
38
  function getUrl(url, callback) {
@@ -72,7 +73,10 @@ module.exports = function (defaultFuncs, api, ctx) {
72
73
  defaultFuncs.post("https://www.facebook.com/messaging/send/", ctx.jar, form)
73
74
  .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
74
75
  .then(resData => {
75
- if (!resData.payload) throw resData;
76
+ if (!resData.payload) {
77
+ globalAntiSuspension.checkAccountHealth(resData);
78
+ throw resData;
79
+ }
76
80
  var messageID = (resData.payload.actions && resData.payload.actions[0] &&
77
81
  resData.payload.actions[0].message_id) || null;
78
82
  var threadID2 = resData.payload.thread_fbid ||
@@ -83,7 +87,11 @@ module.exports = function (defaultFuncs, api, ctx) {
83
87
  timestamp: resData.payload.timestamp
84
88
  });
85
89
  })
86
- .catch(err => { logger.error("OldMessage.send", err); callback(err); });
90
+ .catch(err => {
91
+ globalAntiSuspension.checkAccountHealth(err);
92
+ logger.error("OldMessage.send", err);
93
+ callback(err);
94
+ });
87
95
  }
88
96
 
89
97
  function send(form, threadID, isSingleUser, callback) {
@@ -109,7 +117,12 @@ module.exports = function (defaultFuncs, api, ctx) {
109
117
  form.manual_retry_cnt = 0;
110
118
  form.has_attachment = false;
111
119
  form.signatureID = utils.getGUID().replace(/-/g, "").slice(0, 8);
112
- sendContent(form, threadID, isSingleUser, messageAndOTID, callback);
120
+ const doSend = () => sendContent(form, threadID, isSingleUser, messageAndOTID, callback);
121
+ if (!ctx.globalOptions || ctx.globalOptions.antiBan !== false) {
122
+ globalAntiSuspension.prepareBeforeMessage(threadID, form.body || "").then(doSend).catch(doSend);
123
+ } else {
124
+ doSend();
125
+ }
113
126
  }
114
127
 
115
128
  return function OldMessage(msg, threadID, callback, replyToMessage, isSingleUser) {
@@ -212,21 +212,7 @@ class E2EEBridge {
212
212
  }
213
213
 
214
214
  const isReply = !!(msg.replyTo && msg.replyTo.messageId);
215
- if (isReply) {
216
- try {
217
- console.log("[E2EE-DEBUG] msg.replyTo raw:", JSON.stringify(msg.replyTo, (k, v) => typeof v === "bigint" ? v.toString() : Buffer.isBuffer(v) ? `<Buffer ${v.length}b>` : v));
218
- } catch (_) { console.log("[E2EE-DEBUG] msg.replyTo (raw, non-serializable):", msg.replyTo); }
219
- }
220
- if (msg.kind && msg.kind !== "text") {
221
- try {
222
- console.log("[E2EE-DEBUG] msg.kind=" + msg.kind + " msg.media keys:", msg.media ? Object.keys(msg.media) : null);
223
- } catch (_) {}
224
- }
225
215
  if (msg.kind === "reaction") {
226
- try {
227
- console.log("[E2EE-DEBUG] FULL reaction msg dump:", JSON.stringify(msg, (k, v) => typeof v === "bigint" ? v.toString() : Buffer.isBuffer(v) ? `<Buffer ${v.length}b>` : v, 2));
228
- } catch (_) { console.log("[E2EE-DEBUG] FULL reaction msg (non-serializable):", msg); }
229
-
230
216
  this._messageCallback(null, {
231
217
  type: "message_reaction",
232
218
  threadID: normalizedThreadId,
@@ -324,7 +310,6 @@ class E2EEBridge {
324
310
  // Incoming E2EE reactions — needed for onReaction handlers (e.g. a
325
311
  // reaction-triggered unsend feature) to fire in encrypted threads.
326
312
  this.client.onEvent("e2ee_reaction", (r) => {
327
- console.log("[E2EE-DEBUG] e2ee_reaction fired:", JSON.stringify(r, (k, v) => typeof v === "bigint" ? v.toString() : v));
328
313
  if (!this._messageCallback) return;
329
314
  const threadID = r.chatJid ? String(r.chatJid).split("@")[0].split(".")[0] : "";
330
315
  const senderID = r.senderId || (r.senderJid ? String(r.senderJid).split(".")[0] : "");
@@ -356,23 +341,6 @@ class E2EEBridge {
356
341
  this.connected = true;
357
342
  logger.success("E2EE", "E2EE active — Signal Protocol / Noise WebSocket (vendored)");
358
343
 
359
- // Diagnostic: log memory + internal cache sizes every 10 min so a
360
- // future OOM can be correlated against actual growth data instead of
361
- // guessing which structure is leaking.
362
- if (!this._memDiagInterval) {
363
- this._memDiagInterval = setInterval(() => {
364
- try {
365
- const mem = process.memoryUsage();
366
- const fmtMB = (b) => (b / 1024 / 1024).toFixed(1) + "MB";
367
- console.log(`[MEM-DIAG] rss=${fmtMB(mem.rss)} heapUsed=${fmtMB(mem.heapUsed)} heapTotal=${fmtMB(mem.heapTotal)} external=${fmtMB(mem.external)} arrayBuffers=${fmtMB(mem.arrayBuffers || 0)}`);
368
- console.log(`[MEM-DIAG] caches: mediaCache=${this._mediaCache ? this._mediaCache.size : 0} msgThreadMap=${this._msgThreadMap ? this._msgThreadMap.size : 0} msgTextCache=${this._msgTextCache ? this._msgTextCache.size : 0} senderJidMap=${this._senderJidMap ? this._senderJidMap.size : 0} seenGroupMsgIds=${this._seenGroupMsgIds ? this._seenGroupMsgIds.size : 0} knownThreads=${this._knownE2EEThreads ? this._knownE2EEThreads.size : 0} knownGroups=${this._knownE2EEGroups ? this._knownE2EEGroups.size : 0} localMediaServerCache=${localMediaServer.getCacheSize()}`);
369
- } catch (err) {
370
- console.log("[MEM-DIAG] logging failed:", err && err.message ? err.message : err);
371
- }
372
- }, 10 * 60 * 1000);
373
- this._memDiagInterval.unref();
374
- }
375
-
376
344
  // GROUP E2EE only: the vendor engine's own group Sender-Key decrypt
377
345
  // has an unresolved bug (repeated "missing sender key state" /
378
346
  // "ciphertext version too old" errors). The native mautrix-go engine
@@ -566,8 +534,6 @@ class E2EEBridge {
566
534
 
567
535
  const dims = mimeType.startsWith("image/") ? _getImageDimensions(data, mimeType) : null;
568
536
 
569
- console.log(`[E2EEBridge] sendMessage attachment (native engine): fileName=${fileName}, mimeType=${mimeType}, size=${data.length} bytes, dims=${dims ? dims.width + "x" + dims.height : "n/a"}, threadId=${threadId}`);
570
-
571
537
  let mediaType;
572
538
  if (mimeType.startsWith("image/")) mediaType = "image";
573
539
  else if (mimeType.startsWith("video/")) mediaType = "video";
@@ -606,9 +572,6 @@ class E2EEBridge {
606
572
  else if (mediaType === "audio") result = await this.client.sendAudio(input);
607
573
  else result = await this.client.sendFile(input);
608
574
  }
609
- try {
610
- console.log(`[E2EEBridge] send result:`, JSON.stringify(result, (k, v) => typeof v === "bigint" ? v.toString() : v));
611
- } catch (_) { console.log(`[E2EEBridge] send result (non-serializable):`, result); }
612
575
  if (result && result.messageId) {
613
576
  boundedSet(this._msgThreadMap, String(result.messageId), threadId, 500);
614
577
  // Cache our own sent media so replies to it (e.g. "/imgur" replying
@@ -2,6 +2,7 @@
2
2
 
3
3
  var utils = require("../../utils/nexca-utils");
4
4
  var logger = require("../../utils/nexca-logger");
5
+ var { globalAntiSuspension } = require("../../utils/antiSuspension");
5
6
 
6
7
  var ALLOWED = {
7
8
  attachment: true, url: true, sticker: true, emoji: true,
@@ -244,6 +245,19 @@ module.exports = function (defaultFuncs, api, ctx) {
244
245
  isSingleUser = ctx.threadTypes[String(threadID)] === 'dm';
245
246
  }
246
247
 
248
+ // Rate-limit / pace / circuit-break outbound sends to avoid looking
249
+ // like automated bulk traffic to Facebook. Opt-out via
250
+ // globalOptions.antiBan === false; on by default. This never throws
251
+ // in normal operation (it only adds delay), the try/catch is
252
+ // defensive so a bug in here can never block a real send.
253
+ if (!ctx.globalOptions || ctx.globalOptions.antiBan !== false) {
254
+ try {
255
+ await globalAntiSuspension.prepareBeforeMessage(threadID, msg.body || "");
256
+ } catch (err) {
257
+ logger("AntiSuspension prepareBeforeMessage error: " + (err && err.message ? err.message : err), "warn");
258
+ }
259
+ }
260
+
247
261
  // DM sends on E2EE threads — skip MQTT entirely, always.
248
262
  // Facebook does not deliver plaintext MQTT bodies into an E2EE (Signal Protocol)
249
263
  // thread — this applies to TEXT replies just as much as attachments. Previously
@@ -266,6 +280,7 @@ module.exports = function (defaultFuncs, api, ctx) {
266
280
  if (callback) callback(null, wrapped);
267
281
  else resolve(wrapped);
268
282
  } catch (e2eeErr) {
283
+ globalAntiSuspension.checkAccountHealth(e2eeErr);
269
284
  logger.error("sendMessage", "E2EE DM send failed: " + (e2eeErr.message || e2eeErr));
270
285
  if (callback) callback(e2eeErr);
271
286
  else reject(e2eeErr);
@@ -283,6 +298,7 @@ module.exports = function (defaultFuncs, api, ctx) {
283
298
  if (callback) callback(null, omResult);
284
299
  else resolve(omResult);
285
300
  } catch (omErr) {
301
+ globalAntiSuspension.checkAccountHealth(omErr);
286
302
  logger.error("sendMessage", "DM attachment via OldMessage failed: " + (omErr.error || omErr.message || omErr));
287
303
  if (callback) callback(omErr);
288
304
  else reject(omErr);
@@ -303,6 +319,7 @@ module.exports = function (defaultFuncs, api, ctx) {
303
319
  if (callback) callback(null, fbResult);
304
320
  else resolve(fbResult);
305
321
  } catch (fbErr) {
322
+ globalAntiSuspension.checkAccountHealth(fbErr);
306
323
  logger.error("sendMessage", fbErr.error || fbErr.message || fbErr);
307
324
  if (callback) callback(fbErr);
308
325
  else reject(fbErr);
@@ -0,0 +1,219 @@
1
+ "use strict";
2
+
3
+ const log = {
4
+ warn: (tag, msg) => console.warn(`[${tag}] ${msg}`),
5
+ info: (tag, msg) => console.log(`[${tag}] ${msg}`)
6
+ };
7
+
8
+ // Phrases Facebook responses use when it thinks this account is being
9
+ // operated by a bot / script rather than a human. If any of these show up
10
+ // in a response body or an error message, we treat it as a signal that
11
+ // current traffic looks automated and should slow down / back off.
12
+ const SUSPICION_PHRASES = [
13
+ "checkpoint",
14
+ "automated behavior",
15
+ "automated_behavior",
16
+ "unusual activity",
17
+ "unusual_activity",
18
+ "suspicious activity",
19
+ "action_required",
20
+ "account_locked",
21
+ "account locked",
22
+ "account has been disabled",
23
+ "account has been suspended",
24
+ "account suspended",
25
+ "account banned",
26
+ "bot detected",
27
+ "bot_detected",
28
+ "not a human",
29
+ "spam detected",
30
+ "spam_detected",
31
+ "looks like spam",
32
+ "rate limited",
33
+ "rate_limit",
34
+ "too many requests",
35
+ "too_many_requests",
36
+ "temporarily blocked",
37
+ "feature temporarily unavailable",
38
+ "verify your account",
39
+ "confirm your identity",
40
+ "confirm it's you",
41
+ "security check required",
42
+ "policy violation",
43
+ "action blocked",
44
+ "you're blocked from"
45
+ ];
46
+
47
+ /**
48
+ * Tracks send volume and paces outbound traffic so it doesn't look like a
49
+ * script firing messages as fast as the network allows. Three layers:
50
+ *
51
+ * - Pacing: a small delay before every send, and a slightly larger one
52
+ * between consecutive sends to the same thread.
53
+ * - Volume caps: soft hourly/daily ceilings; a fresh login starts under a
54
+ * stricter "warmup" ceiling for a while before easing to the normal one.
55
+ * - Cooldown: if Facebook's response text matches a suspicion phrase (or
56
+ * a send throws an error containing one), sending gets throttled harder
57
+ * for a while instead of continuing at full speed.
58
+ */
59
+ class SendPacer {
60
+ constructor() {
61
+ this.perThreadLastSendAt = new Map();
62
+ this.threadSendCounts = new Map();
63
+
64
+ this.startedAt = Date.now();
65
+ this.warmupWindowMs = 15 * 60 * 1000;
66
+ this.warmupHourlyCap = 40;
67
+ this.normalHourlyCap = 350;
68
+ this.dailyCap = 2500;
69
+
70
+ this.hourWindowStart = Date.now();
71
+ this.hourSendCount = 0;
72
+ this.dayWindowStart = Date.now();
73
+ this.daySendCount = 0;
74
+
75
+ this.cooldown = {
76
+ active: false,
77
+ since: null,
78
+ durationMs: 45 * 60 * 1000,
79
+ strikes: 0,
80
+ strikesToActivate: 2
81
+ };
82
+
83
+ this._gcTimer = setInterval(() => this._rollWindows(), 60 * 1000);
84
+ if (this._gcTimer.unref) this._gcTimer.unref();
85
+ }
86
+
87
+ destroy() {
88
+ if (this._gcTimer) clearInterval(this._gcTimer);
89
+ }
90
+
91
+ _rollWindows() {
92
+ const now = Date.now();
93
+ if (now - this.hourWindowStart >= 60 * 60 * 1000) {
94
+ this.hourWindowStart = now;
95
+ this.hourSendCount = 0;
96
+ }
97
+ if (now - this.dayWindowStart >= 24 * 60 * 60 * 1000) {
98
+ this.dayWindowStart = now;
99
+ this.daySendCount = 0;
100
+ }
101
+ }
102
+
103
+ _inWarmup() {
104
+ return (Date.now() - this.startedAt) < this.warmupWindowMs;
105
+ }
106
+
107
+ _hourlyCap() {
108
+ return this._inWarmup() ? this.warmupHourlyCap : this.normalHourlyCap;
109
+ }
110
+
111
+ /** Records that a suspicious signal was seen; may activate the cooldown. */
112
+ noteSuspicionSignal(source) {
113
+ this.cooldown.strikes++;
114
+ if (!this.cooldown.active && this.cooldown.strikes >= this.cooldown.strikesToActivate) {
115
+ this.cooldown.active = true;
116
+ this.cooldown.since = Date.now();
117
+ log.warn("AntiSuspension", `Cooldown activated after repeated suspicion signals (last: ${source || "unknown"}). Backing off for ${(this.cooldown.durationMs / 60000).toFixed(0)} min.`);
118
+ }
119
+ }
120
+
121
+ /** Same check, but scans free text (an API response body, an error message). */
122
+ scanForSuspicion(text) {
123
+ if (!text || typeof text !== "string") return false;
124
+ const lower = text.toLowerCase();
125
+ const hit = SUSPICION_PHRASES.find(p => lower.includes(p));
126
+ if (hit) this.noteSuspicionSignal(hit);
127
+ return !!hit;
128
+ }
129
+
130
+ /** Inspects a thrown error's message for suspicion phrases. */
131
+ noteSendError(err) {
132
+ const msg = (err && (err.message || err.error)) ? String(err.message || err.error) : "";
133
+ this.scanForSuspicion(msg);
134
+ }
135
+
136
+ isCoolingDown() {
137
+ if (!this.cooldown.active) return false;
138
+ if (Date.now() - this.cooldown.since >= this.cooldown.durationMs) {
139
+ this.cooldown.active = false;
140
+ this.cooldown.strikes = 0;
141
+ this.cooldown.since = null;
142
+ return false;
143
+ }
144
+ return true;
145
+ }
146
+
147
+ /**
148
+ * Call this right before every send. Resolves once it's safe to go —
149
+ * may internally wait (pacing delay, volume backoff, cooldown backoff).
150
+ * Never rejects; on any internal problem it just resolves immediately
151
+ * rather than blocking a real message.
152
+ */
153
+ async waitForTurn(threadID) {
154
+ try {
155
+ this._rollWindows();
156
+
157
+ if (this.isCoolingDown()) {
158
+ await this._sleep(2000 + Math.random() * 3000);
159
+ }
160
+
161
+ const cap = this._hourlyCap();
162
+ if (this.hourSendCount >= cap) {
163
+ log.warn("AntiSuspension", `Hourly send cap reached (${this.hourSendCount}/${cap}${this._inWarmup() ? ", warmup" : ""}). Slowing down.`);
164
+ await this._sleep(4000 + Math.random() * 4000);
165
+ }
166
+ if (this.daySendCount >= this.dailyCap) {
167
+ log.warn("AntiSuspension", `Daily send cap reached (${this.daySendCount}/${this.dailyCap}). Slowing down.`);
168
+ await this._sleep(4000 + Math.random() * 4000);
169
+ }
170
+
171
+ const key = String(threadID);
172
+ const lastAt = this.perThreadLastSendAt.get(key) || 0;
173
+ const sinceLast = Date.now() - lastAt;
174
+ const minGap = 400 + Math.random() * 200;
175
+ if (sinceLast < minGap) {
176
+ await this._sleep(minGap - sinceLast);
177
+ }
178
+
179
+ // A small, always-present jitter so consecutive sends never
180
+ // look perfectly back-to-back even across different threads.
181
+ await this._sleep(80 + Math.random() * 150);
182
+
183
+ this.perThreadLastSendAt.set(key, Date.now());
184
+ this.hourSendCount++;
185
+ this.daySendCount++;
186
+ const threadCount = (this.threadSendCounts.get(key) || 0) + 1;
187
+ this.threadSendCounts.set(key, threadCount);
188
+ } catch {
189
+ // Pacing must never be the reason a real message fails to send.
190
+ }
191
+ }
192
+
193
+ _sleep(ms) {
194
+ return new Promise(resolve => setTimeout(resolve, Math.max(0, ms)));
195
+ }
196
+
197
+ stats() {
198
+ return {
199
+ warmup: this._inWarmup(),
200
+ hourSendCount: this.hourSendCount,
201
+ hourlyCap: this._hourlyCap(),
202
+ daySendCount: this.daySendCount,
203
+ dailyCap: this.dailyCap,
204
+ coolingDown: this.isCoolingDown()
205
+ };
206
+ }
207
+ }
208
+
209
+ const pacer = new SendPacer();
210
+
211
+ module.exports = {
212
+ SendPacer,
213
+ globalAntiSuspension: {
214
+ prepareBeforeMessage: (threadID) => pacer.waitForTurn(threadID),
215
+ detectSuspensionSignal: (text) => pacer.scanForSuspicion(text),
216
+ checkAccountHealth: (err) => pacer.noteSendError(err),
217
+ getStats: () => pacer.stats()
218
+ }
219
+ };
@@ -18,6 +18,23 @@ function saveCookies(jar) {
18
18
  } catch {
19
19
  // ignore per-cookie errors
20
20
  }
21
+ // Mirror facebook.com <-> messenger.com so a session refresh that
22
+ // only touches one domain's Set-Cookie doesn't leave the other
23
+ // domain holding a stale cookie. Requests to messenger.com (e.g.
24
+ // the periodic session-keeper ping) otherwise end up looking
25
+ // logged-out even while the facebook.com session is fine, which
26
+ // is itself an inconsistency Facebook's detection can key off.
27
+ try {
28
+ if (/domain=\.?facebook\.com/i.test(c)) {
29
+ const mirrored = c.replace(/domain=\.?facebook\.com/i, "domain=.messenger.com");
30
+ jar.setCookieSync(mirrored, "https://www.messenger.com");
31
+ } else if (/domain=\.?messenger\.com/i.test(c)) {
32
+ const mirrored = c.replace(/domain=\.?messenger\.com/i, "domain=.facebook.com");
33
+ jar.setCookieSync(mirrored, "https://www.facebook.com");
34
+ }
35
+ } catch {
36
+ // ignore per-cookie mirroring errors
37
+ }
21
38
  }
22
39
  }
23
40
  } catch {
@@ -4,6 +4,7 @@ const logger = require("../../../func/logger");
4
4
  const { makeParsable } = require("./textUtils");
5
5
  const { delay, createEmit, headerOf, buildUrl, formatCookie } = require("./helpers");
6
6
  const { createMaybeAutoLogin } = require("./autoLogin");
7
+ const { globalAntiSuspension } = require("../antiSuspension");
7
8
 
8
9
  /**
9
10
  * Trả về hàm async (res) => parsed | throw.
@@ -183,7 +184,25 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
183
184
  resStr.includes("XCheckpointFBScrapingWarningController") ||
184
185
  resStr.includes("601051028565049")
185
186
  ) {
187
+ globalAntiSuspension.detectSuspensionSignal("automated behavior checkpoint scraping warning");
186
188
  emit("checkpoint", { type: "scraping_warning", res: resData });
189
+ // Dismiss it the same way a human tapping "Dismiss" on that screen
190
+ // would — an unacknowledged warning left sitting is worse than one
191
+ // that's been cleared. Fire-and-forget: this must never block or
192
+ // fail the actual response being parsed.
193
+ http
194
+ .post("https://www.facebook.com/api/graphql/", ctx.jar, {
195
+ av: ctx.userID,
196
+ __user: ctx.userID,
197
+ fb_dtsg: ctx.fb_dtsg || "",
198
+ fb_api_caller_class: "RelayModern",
199
+ fb_api_req_friendly_name: "FBScrapingWarningMutation",
200
+ server_timestamps: "true",
201
+ doc_id: "6339492849481770",
202
+ variables: "{}"
203
+ }, ctx.globalOptions, ctx)
204
+ .then(() => logger("Dismissed automated-behavior warning", "info"))
205
+ .catch((e) => logger(`Dismissing automated-behavior warning failed (non-fatal): ${e && e.message ? e.message : String(e)}`, "warn"));
187
206
  return await maybeAutoLogin(resData, res?.config);
188
207
  }
189
208
  if (
@@ -193,6 +212,7 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
193
212
  return await maybeAutoLogin(resData, res?.config);
194
213
  }
195
214
  if (resStr.includes("1501092823525282")) {
215
+ globalAntiSuspension.detectSuspensionSignal("checkpoint 282");
196
216
  logger("Bot checkpoint 282 detected, please check the account!", "error");
197
217
  const err = new Error("Checkpoint 282 detected");
198
218
  err.error = "checkpoint_282";
@@ -202,6 +222,7 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
202
222
  throw err;
203
223
  }
204
224
  if (resStr.includes("828281030927956")) {
225
+ globalAntiSuspension.detectSuspensionSignal("checkpoint 956");
205
226
  logger("Bot checkpoint 956 detected, please check the account!", "error");
206
227
  const err = new Error("Checkpoint 956 detected");
207
228
  err.error = "checkpoint_956";