@eryxenx/fca 1.0.0 → 1.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 +1 -1
- package/src/api/action/dismissScrapingWarning.js +50 -0
- package/src/api/socket/e2ee/index.js +0 -37
- package/src/api/socket/sendMessage.js +17 -0
- package/src/utils/antiSuspension.js +219 -0
- package/src/utils/cookies.js +17 -0
- package/src/utils/loginParser/parseAndCheckLogin.js +21 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@eryxenx/fca",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
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: "24406519995698862",
|
|
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
|
+
};
|
|
@@ -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
|
+
};
|
package/src/utils/cookies.js
CHANGED
|
@@ -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: "24406519995698862",
|
|
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";
|