hridoy-ultimate 9.0.2 → 9.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.
- package/CHANGELOG.md +45 -0
- package/DOCS.md +14 -6
- package/README.md +3 -2
- package/index.d.ts +5 -1
- package/module/config.js +14 -2
- package/module/login.js +15 -5
- package/module/loginHelper.js +48 -9
- package/package.json +18 -12
- package/src/api/action/enableAutoSaveAppState.js +8 -5
- package/src/api/action/getSafetyStatus.js +27 -0
- package/src/api/action/logout.js +70 -59
- package/src/api/action/logoutFromFacebook.js +86 -0
- package/src/api/action/refreshFb_dtsg.js +1 -1
- package/src/api/messaging/changeGroupImage.js +1 -0
- package/src/api/socket/OldMessage.js +4 -1
- package/src/api/socket/core/connectMqtt.js +18 -1
- package/src/api/socket/core/getSeqID.js +13 -2
- package/src/api/socket/listenMqtt.js +24 -15
- package/src/api/socket/sendMessage.js +4 -1
- package/src/api/users/getUserID.js +1 -0
- package/src/app/createFcaClient.js +1 -0
- package/src/utils/antiSuspension.js +84 -24
- package/src/utils/cookies.js +29 -1
- package/src/utils/fingerprint.js +75 -36
- package/src/utils/loginParser/parseAndCheckLogin.js +7 -18
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* logoutFromFacebook - REAL Facebook logout.
|
|
5
|
+
*
|
|
6
|
+
* This POSTs facebook.com/logout.php with the session cookies. Facebook then
|
|
7
|
+
* invalidates that session everywhere it is used, so the same account is also
|
|
8
|
+
* logged out in your normal browser. It is NOT called by api.logout() unless you
|
|
9
|
+
* opt in with `allowFacebookLogout: true` in fca-config.json. Call it directly
|
|
10
|
+
* only when you really want to end the Facebook session.
|
|
11
|
+
*/
|
|
12
|
+
|
|
13
|
+
const log = require("../../../func/logAdapter");
|
|
14
|
+
const { parseAndCheckLogin } = require("../../utils/client");
|
|
15
|
+
const { getFrom } = require("../../utils/constants");
|
|
16
|
+
const { saveCookies } = require("../../utils/client");
|
|
17
|
+
module.exports = function(defaultFuncs, api, ctx) {
|
|
18
|
+
return function logoutFromFacebook(callback) {
|
|
19
|
+
let resolveFunc = function() {};
|
|
20
|
+
let rejectFunc = function() {};
|
|
21
|
+
const returnPromise = new Promise(function(resolve, reject) {
|
|
22
|
+
resolveFunc = resolve;
|
|
23
|
+
rejectFunc = reject;
|
|
24
|
+
});
|
|
25
|
+
|
|
26
|
+
if (!callback) {
|
|
27
|
+
callback = function(err, friendList) {
|
|
28
|
+
if (err) {
|
|
29
|
+
return rejectFunc(err);
|
|
30
|
+
}
|
|
31
|
+
resolveFunc(friendList);
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const form = {
|
|
36
|
+
pmid: "0"
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
defaultFuncs
|
|
40
|
+
.post(
|
|
41
|
+
"https://www.facebook.com/bluebar/modern_settings_menu/?help_type=364455653583099&show_contextual_help=1",
|
|
42
|
+
ctx.jar,
|
|
43
|
+
form
|
|
44
|
+
)
|
|
45
|
+
.then(parseAndCheckLogin(ctx, defaultFuncs))
|
|
46
|
+
.then(function(resData) {
|
|
47
|
+
const elem = resData.jsmods.instances[0][2][0].filter(function(v) {
|
|
48
|
+
return v.value === "logout";
|
|
49
|
+
})[0];
|
|
50
|
+
|
|
51
|
+
const html = resData.jsmods.markup.filter(function(v) {
|
|
52
|
+
return v[0] === elem.markup.__m;
|
|
53
|
+
})[0][1].__html;
|
|
54
|
+
|
|
55
|
+
const form = {
|
|
56
|
+
fb_dtsg: getFrom(html, '"fb_dtsg" value="', '"'),
|
|
57
|
+
ref: getFrom(html, '"ref" value="', '"'),
|
|
58
|
+
h: getFrom(html, '"h" value="', '"')
|
|
59
|
+
};
|
|
60
|
+
|
|
61
|
+
return defaultFuncs
|
|
62
|
+
.post("https://www.facebook.com/logout.php", ctx.jar, form)
|
|
63
|
+
.then(saveCookies(ctx.jar));
|
|
64
|
+
})
|
|
65
|
+
.then(function(res) {
|
|
66
|
+
if (!res.headers) {
|
|
67
|
+
throw { error: "An error occurred when logging out." };
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
return defaultFuncs
|
|
71
|
+
.get(res.headers.location, ctx.jar)
|
|
72
|
+
.then(saveCookies(ctx.jar));
|
|
73
|
+
})
|
|
74
|
+
.then(function() {
|
|
75
|
+
ctx.loggedIn = false;
|
|
76
|
+
log.info("logoutFromFacebook", "Logged out of Facebook (this also ends the browser session).");
|
|
77
|
+
callback();
|
|
78
|
+
})
|
|
79
|
+
.catch(function(err) {
|
|
80
|
+
log.error("logoutFromFacebook", err);
|
|
81
|
+
return callback(err);
|
|
82
|
+
});
|
|
83
|
+
|
|
84
|
+
return returnPromise;
|
|
85
|
+
};
|
|
86
|
+
};
|
|
@@ -12,7 +12,7 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
12
12
|
}
|
|
13
13
|
if (!obj) obj = {};
|
|
14
14
|
if (getType(obj) !== "Object") {
|
|
15
|
-
throw new
|
|
15
|
+
throw new Error("The first parameter must be an object or a callback function");
|
|
16
16
|
}
|
|
17
17
|
let resolveFunc, rejectFunc;
|
|
18
18
|
const returnPromise = new Promise((resolve, reject) => {
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { generateOfflineThreadingID } = require("../../utils/format");
|
|
4
4
|
const log = require("../../../func/logAdapter");
|
|
5
|
+
const { parseAndCheckLogin } = require("../../utils/client");
|
|
5
6
|
|
|
6
7
|
module.exports = function (defaultFuncs, api, ctx) {
|
|
7
8
|
function handleUpload(image) {
|
|
@@ -119,7 +119,10 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
119
119
|
form.signatureID = utils.getGUID().replace(/-/g, "").slice(0, 8);
|
|
120
120
|
const doSend = () => sendContent(form, threadID, isSingleUser, messageAndOTID, callback);
|
|
121
121
|
if (!ctx.globalOptions || ctx.globalOptions.antiBan !== false) {
|
|
122
|
-
globalAntiSuspension.prepareBeforeMessage(threadID, form.body || "").then(doSend).catch(
|
|
122
|
+
globalAntiSuspension.prepareBeforeMessage(threadID, form.body || "").then(doSend).catch(err => {
|
|
123
|
+
if (typeof callback === "function") return callback(err);
|
|
124
|
+
return undefined;
|
|
125
|
+
});
|
|
123
126
|
} else {
|
|
124
127
|
doSend();
|
|
125
128
|
}
|
|
@@ -16,9 +16,25 @@ module.exports = function createListenMqtt(deps) {
|
|
|
16
16
|
|
|
17
17
|
return function listenMqtt(defaultFuncs, api, ctx, globalCallback) {
|
|
18
18
|
|
|
19
|
+
// Reconnects used to be a fixed 2 s forever: a flapping or refused socket meant a new
|
|
20
|
+
// WebSocket handshake to edge-chat every 2 seconds. Now exponential with jitter, capped
|
|
21
|
+
// at 5 min, reset as soon as the broker delivers /t_ms again, with a hard budget.
|
|
22
|
+
const MQTT_MAX_FAILS = 12;
|
|
23
|
+
const MQTT_MAX_DELAY_MS = 5 * 60 * 1000;
|
|
19
24
|
function scheduleReconnect(delayMs) {
|
|
20
25
|
const d = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || DEFAULT_RECONNECT_DELAY_MS;
|
|
21
|
-
const
|
|
26
|
+
const base = typeof delayMs === "number" ? delayMs : d;
|
|
27
|
+
const streak = ctx._mqttFailStreak || 0;
|
|
28
|
+
if (!ctx._reconnectTimer && !ctx._ending) {
|
|
29
|
+
if (streak >= MQTT_MAX_FAILS) {
|
|
30
|
+
ctx._mqttFailStreak = 0;
|
|
31
|
+
logger(`mqtt reconnect budget used up (${MQTT_MAX_FAILS} attempts without a working connection) - stopping; host app decides`, "error");
|
|
32
|
+
try { globalCallback({ type: "stop_listen", error: "MQTT reconnect budget exhausted" }, null); } catch (_) { }
|
|
33
|
+
return;
|
|
34
|
+
}
|
|
35
|
+
ctx._mqttFailStreak = streak + 1;
|
|
36
|
+
}
|
|
37
|
+
const ms = Math.round(Math.min(base * Math.pow(2, streak), MQTT_MAX_DELAY_MS) * (0.85 + Math.random() * 0.3));
|
|
22
38
|
if (ctx._reconnectTimer) {
|
|
23
39
|
logger("mqtt reconnect already scheduled", "warn");
|
|
24
40
|
return; // debounce
|
|
@@ -180,6 +196,7 @@ module.exports = function createListenMqtt(deps) {
|
|
|
180
196
|
ctx._rTimeout = rTimeout;
|
|
181
197
|
|
|
182
198
|
ctx.tmsWait = function () {
|
|
199
|
+
ctx._mqttFailStreak = 0; // broker is delivering again: connection is healthy
|
|
183
200
|
if (rTimeout) {
|
|
184
201
|
clearTimeout(rTimeout);
|
|
185
202
|
rTimeout = null;
|
|
@@ -254,9 +254,12 @@ module.exports = function createGetSeqID(deps) {
|
|
|
254
254
|
|
|
255
255
|
throw { error: "Not logged in", originalResponse: resData };
|
|
256
256
|
}
|
|
257
|
-
if (!Array.isArray(resData) || !resData.length)
|
|
257
|
+
if (!Array.isArray(resData) || !resData.length) throw new Error("getSeqID: empty response");
|
|
258
258
|
const lastRes = resData[resData.length - 1];
|
|
259
|
-
|
|
259
|
+
// Used to `return` silently here: no listener was started and nothing retried, so the
|
|
260
|
+
// bot sat "running" but deaf until the 5-minute heartbeat noticed. Now it is a
|
|
261
|
+
// retryable error (bounded back-off in listenMqtt's getSeqIDWrapper).
|
|
262
|
+
if (lastRes && lastRes.successful_results === 0) throw new Error("getSeqID: Facebook returned no successful results");
|
|
260
263
|
|
|
261
264
|
const syncSeqId = resData[0]?.o0?.data?.viewer?.message_threads?.sync_sequence_id;
|
|
262
265
|
if (syncSeqId) {
|
|
@@ -266,12 +269,20 @@ module.exports = function createGetSeqID(deps) {
|
|
|
266
269
|
ctx.lastSeqId = ctx.lastSeqId || 0;
|
|
267
270
|
logger("getSeqID: sync_sequence_id not found, fallback lastSeqId=" + ctx.lastSeqId, "warn");
|
|
268
271
|
}
|
|
272
|
+
ctx.loggedIn = true; // emitAuth() sets this false; a working session must set it back
|
|
269
273
|
listenMqtt(defaultFuncs, api, ctx, globalCallback);
|
|
270
274
|
})
|
|
271
275
|
.catch(async err => {
|
|
272
276
|
const detail = (err && err.detail && err.detail.message) ? ` | detail=${err.detail.message}` : "";
|
|
273
277
|
const msg = ((err && err.error) || (err && err.message) || String(err || "")) + detail;
|
|
274
278
|
|
|
279
|
+
// A human has to dismiss Facebook's automated-behavior warning. Not a dead session and
|
|
280
|
+
// not retryable: stop cleanly and tell the host app with its own reason.
|
|
281
|
+
if (/automation_warning|automated-behavior warning/i.test(msg) || (err && err.error === "automation_warning")) {
|
|
282
|
+
logger(`getSeqID: ${msg} - NOT retrying`, "warn");
|
|
283
|
+
return emitAuth(ctx, api, globalCallback, "automation_warning", msg);
|
|
284
|
+
}
|
|
285
|
+
|
|
275
286
|
// Definitive server-side session invalidation: error 1357001
|
|
276
287
|
// ("Log in to continue") or an explicit login block. The session is
|
|
277
288
|
// dead, not desynced — retrying within seconds (and especially
|
|
@@ -96,6 +96,18 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
|
|
|
96
96
|
|
|
97
97
|
let conf = mqttConf(ctx, opts);
|
|
98
98
|
|
|
99
|
+
// Consecutive getSeqID failures. Used to retry every fixed 2 s forever, which is a request
|
|
100
|
+
// storm against Facebook on any persistent error (and exactly what "automation" detection
|
|
101
|
+
// looks for). Now: exponential back-off with jitter, capped, and a hard budget after which
|
|
102
|
+
// the host app is told ({type:"stop_listen"}) instead of looping.
|
|
103
|
+
let seqFailStreak = 0;
|
|
104
|
+
const SEQ_MAX_FAILS = 8;
|
|
105
|
+
const SEQ_MAX_DELAY_MS = 5 * 60 * 1000;
|
|
106
|
+
function seqRetryDelay(streak) {
|
|
107
|
+
const raw = Math.min(conf.reconnectDelayMs * Math.pow(2, streak - 1), SEQ_MAX_DELAY_MS);
|
|
108
|
+
return Math.round(raw * (0.85 + Math.random() * 0.3));
|
|
109
|
+
}
|
|
110
|
+
|
|
99
111
|
function getSeqIDWrapper() {
|
|
100
112
|
if (ctx._ending && !ctx._cycling) {
|
|
101
113
|
logger("mqtt getSeqID skipped - ending", "warn");
|
|
@@ -118,6 +130,7 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
|
|
|
118
130
|
.then(() => {
|
|
119
131
|
logger("mqtt getSeqID done", "info");
|
|
120
132
|
ctx._cycling = false;
|
|
133
|
+
seqFailStreak = 0;
|
|
121
134
|
})
|
|
122
135
|
.catch(e => {
|
|
123
136
|
ctx._cycling = false;
|
|
@@ -127,8 +140,15 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
|
|
|
127
140
|
if (ctx._ending) return;
|
|
128
141
|
// Retry after delay if autoReconnect is enabled
|
|
129
142
|
if (ctx.globalOptions.autoReconnect) {
|
|
130
|
-
|
|
131
|
-
|
|
143
|
+
seqFailStreak++;
|
|
144
|
+
if (seqFailStreak > SEQ_MAX_FAILS) {
|
|
145
|
+
logger(`mqtt getSeqID failed ${SEQ_MAX_FAILS} times in a row - giving up (host app decides what happens next)`, "error");
|
|
146
|
+
seqFailStreak = 0;
|
|
147
|
+
try { globalCallback({ type: "stop_listen", error: errMsg }, null); } catch (_) { }
|
|
148
|
+
return;
|
|
149
|
+
}
|
|
150
|
+
const d = seqRetryDelay(seqFailStreak);
|
|
151
|
+
logger(`mqtt getSeqID will retry in ${d}ms (failure ${seqFailStreak}/${SEQ_MAX_FAILS})`, "warn");
|
|
132
152
|
setTimeout(() => {
|
|
133
153
|
if (!ctx._ending) getSeqIDWrapper();
|
|
134
154
|
}, d);
|
|
@@ -280,9 +300,8 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
|
|
|
280
300
|
|
|
281
301
|
function startSessionKeeper() {
|
|
282
302
|
if (ctx._sessionKeeperInterval) return;
|
|
283
|
-
// Self-rescheduling timeout with ±
|
|
284
|
-
//
|
|
285
|
-
// trivial to fingerprint.
|
|
303
|
+
// Self-rescheduling timeout with ±1 h jitter around 6 h, instead of a
|
|
304
|
+
// clockwork-exact interval — perfectly periodic traffic is trivial to fingerprint.
|
|
286
305
|
const BASE_MS = 6 * 60 * 60 * 1000;
|
|
287
306
|
const JITTER_MS = 60 * 60 * 1000;
|
|
288
307
|
const scheduleNext = function () {
|
|
@@ -480,13 +499,3 @@ module.exports = function (defaultFuncs, api, ctx, opts) {
|
|
|
480
499
|
};
|
|
481
500
|
};
|
|
482
501
|
|
|
483
|
-
|
|
484
|
-
// mqtt stability patch
|
|
485
|
-
try {
|
|
486
|
-
if (typeof client !== "undefined" && client) {
|
|
487
|
-
client.on("offline", () => console.log("[FCA] MQTT offline"));
|
|
488
|
-
client.on("reconnect", () => console.log("[FCA] MQTT reconnecting"));
|
|
489
|
-
client.on("close", () => console.log("[FCA] MQTT closed"));
|
|
490
|
-
client.on("error", (e) => console.log("[FCA] MQTT error:", e?.message || e));
|
|
491
|
-
}
|
|
492
|
-
} catch (e) {}
|
|
@@ -254,7 +254,10 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
254
254
|
try {
|
|
255
255
|
await globalAntiSuspension.prepareBeforeMessage(threadID, msg.body || "");
|
|
256
256
|
} catch (err) {
|
|
257
|
-
logger("
|
|
257
|
+
logger.error("sendMessage", "Outbound send blocked by safety guard: " + (err && err.message ? err.message : err));
|
|
258
|
+
if (callback) callback(err);
|
|
259
|
+
else reject(err);
|
|
260
|
+
return promise;
|
|
258
261
|
}
|
|
259
262
|
}
|
|
260
263
|
|
|
@@ -88,6 +88,7 @@ function createFallbackNamespaces(api) {
|
|
|
88
88
|
getCurrentUserID: bindOptionalMethod(api, "getCurrentUserID"),
|
|
89
89
|
handleFriendRequest: bindOptionalMethod(api, "handleFriendRequest"),
|
|
90
90
|
logout: bindOptionalMethod(api, "logout"),
|
|
91
|
+
logoutFromFacebook: bindOptionalMethod(api, "logoutFromFacebook"),
|
|
91
92
|
refreshDtsg: bindOptionalMethod(api, "refreshFb_dtsg"),
|
|
92
93
|
changeBlockedStatus: bindOptionalMethod(api, "changeBlockedStatus"),
|
|
93
94
|
setOptions: bindOptionalMethod(api, "setOptions"),
|
|
@@ -6,17 +6,21 @@ const log = {
|
|
|
6
6
|
};
|
|
7
7
|
|
|
8
8
|
// Phrases Facebook responses use when it thinks this account is being
|
|
9
|
-
// operated by a bot / script rather than a human
|
|
10
|
-
//
|
|
11
|
-
//
|
|
12
|
-
|
|
9
|
+
// operated by a bot / script rather than a human, or when it is restricting it.
|
|
10
|
+
//
|
|
11
|
+
// HARD signals mean the account itself is flagged / locked / needs a human
|
|
12
|
+
// (checkpoint, automated-behavior warning, disabled, identity check). On these
|
|
13
|
+
// the guard FAILS CLOSED: outbound sending stops until the process is restarted
|
|
14
|
+
// after a manual review of the account.
|
|
15
|
+
//
|
|
16
|
+
// SOFT signals are rate-limit style responses ("too many requests", "rate
|
|
17
|
+
// limited", "temporarily blocked" ...). They are usually transient, so they only
|
|
18
|
+
// count as strikes and trigger the slow-down cooldown. Treating them as hard
|
|
19
|
+
// signals made one passing 429-style error silence the bot for good.
|
|
20
|
+
const HARD_SUSPICION_PHRASES = [
|
|
13
21
|
"checkpoint",
|
|
14
22
|
"automated behavior",
|
|
15
23
|
"automated_behavior",
|
|
16
|
-
"unusual activity",
|
|
17
|
-
"unusual_activity",
|
|
18
|
-
"suspicious activity",
|
|
19
|
-
"action_required",
|
|
20
24
|
"account_locked",
|
|
21
25
|
"account locked",
|
|
22
26
|
"account has been disabled",
|
|
@@ -26,6 +30,17 @@ const SUSPICION_PHRASES = [
|
|
|
26
30
|
"bot detected",
|
|
27
31
|
"bot_detected",
|
|
28
32
|
"not a human",
|
|
33
|
+
"verify your account",
|
|
34
|
+
"confirm your identity",
|
|
35
|
+
"confirm it's you",
|
|
36
|
+
"security check required"
|
|
37
|
+
];
|
|
38
|
+
|
|
39
|
+
const SOFT_SUSPICION_PHRASES = [
|
|
40
|
+
"unusual activity",
|
|
41
|
+
"unusual_activity",
|
|
42
|
+
"suspicious activity",
|
|
43
|
+
"action_required",
|
|
29
44
|
"spam detected",
|
|
30
45
|
"spam_detected",
|
|
31
46
|
"looks like spam",
|
|
@@ -35,15 +50,14 @@ const SUSPICION_PHRASES = [
|
|
|
35
50
|
"too_many_requests",
|
|
36
51
|
"temporarily blocked",
|
|
37
52
|
"feature temporarily unavailable",
|
|
38
|
-
"verify your account",
|
|
39
|
-
"confirm your identity",
|
|
40
|
-
"confirm it's you",
|
|
41
|
-
"security check required",
|
|
42
53
|
"policy violation",
|
|
43
54
|
"action blocked",
|
|
44
55
|
"you're blocked from"
|
|
45
56
|
];
|
|
46
57
|
|
|
58
|
+
// Kept for backwards compatibility with anything that imported the old list.
|
|
59
|
+
const SUSPICION_PHRASES = HARD_SUSPICION_PHRASES.concat(SOFT_SUSPICION_PHRASES);
|
|
60
|
+
|
|
47
61
|
/**
|
|
48
62
|
* Tracks send volume and paces outbound traffic so it doesn't look like a
|
|
49
63
|
* script firing messages as fast as the network allows. Three layers:
|
|
@@ -72,13 +86,18 @@ class SendPacer {
|
|
|
72
86
|
this.dayWindowStart = Date.now();
|
|
73
87
|
this.daySendCount = 0;
|
|
74
88
|
|
|
89
|
+
this.accountSafetyBlocked = false;
|
|
90
|
+
this.accountSafetyReason = null;
|
|
91
|
+
|
|
75
92
|
this.cooldown = {
|
|
76
93
|
active: false,
|
|
77
94
|
since: null,
|
|
78
95
|
durationMs: 45 * 60 * 1000,
|
|
79
96
|
strikes: 0,
|
|
80
|
-
strikesToActivate: 2
|
|
97
|
+
strikesToActivate: 2,
|
|
98
|
+
strikeWindowMs: 30 * 60 * 1000
|
|
81
99
|
};
|
|
100
|
+
this.softStrikeTimes = [];
|
|
82
101
|
|
|
83
102
|
this._gcTimer = setInterval(() => this._rollWindows(), 60 * 1000);
|
|
84
103
|
if (this._gcTimer.unref) this._gcTimer.unref();
|
|
@@ -108,12 +127,30 @@ class SendPacer {
|
|
|
108
127
|
return this._inWarmup() ? this.warmupHourlyCap : this.normalHourlyCap;
|
|
109
128
|
}
|
|
110
129
|
|
|
111
|
-
/**
|
|
112
|
-
|
|
113
|
-
|
|
130
|
+
/**
|
|
131
|
+
* Records that a suspicious signal was seen.
|
|
132
|
+
* hard=true (default): fail closed - stop sending until a human reviews the account.
|
|
133
|
+
* hard=false: count a strike; two strikes inside strikeWindowMs start the cooldown.
|
|
134
|
+
*/
|
|
135
|
+
noteSuspicionSignal(source, hard) {
|
|
136
|
+
const isHard = hard !== false;
|
|
137
|
+
if (isHard) {
|
|
138
|
+
// Fail closed: once Facebook signals checkpoint/automation/disabled status,
|
|
139
|
+
// do not continue sending or retrying in this process. Human review is required.
|
|
140
|
+
if (!this.accountSafetyBlocked) {
|
|
141
|
+
log.warn("AntiSuspension", `Outbound sending paused: ${source || "Facebook security signal"}. Review the account manually, then restart the bot.`);
|
|
142
|
+
}
|
|
143
|
+
this.accountSafetyBlocked = true;
|
|
144
|
+
this.accountSafetyReason = String(source || "Facebook security signal");
|
|
145
|
+
return;
|
|
146
|
+
}
|
|
147
|
+
const now = Date.now();
|
|
148
|
+
this.softStrikeTimes = this.softStrikeTimes.filter(t => now - t < this.cooldown.strikeWindowMs);
|
|
149
|
+
this.softStrikeTimes.push(now);
|
|
150
|
+
this.cooldown.strikes = this.softStrikeTimes.length;
|
|
114
151
|
if (!this.cooldown.active && this.cooldown.strikes >= this.cooldown.strikesToActivate) {
|
|
115
152
|
this.cooldown.active = true;
|
|
116
|
-
this.cooldown.since =
|
|
153
|
+
this.cooldown.since = now;
|
|
117
154
|
log.warn("AntiSuspension", `Cooldown activated after repeated suspicion signals (last: ${source || "unknown"}). Backing off for ${(this.cooldown.durationMs / 60000).toFixed(0)} min.`);
|
|
118
155
|
}
|
|
119
156
|
}
|
|
@@ -122,9 +159,17 @@ class SendPacer {
|
|
|
122
159
|
scanForSuspicion(text) {
|
|
123
160
|
if (!text || typeof text !== "string") return false;
|
|
124
161
|
const lower = text.toLowerCase();
|
|
125
|
-
const
|
|
126
|
-
if (
|
|
127
|
-
|
|
162
|
+
const hardHit = HARD_SUSPICION_PHRASES.find(p => lower.includes(p));
|
|
163
|
+
if (hardHit) {
|
|
164
|
+
this.noteSuspicionSignal(hardHit, true);
|
|
165
|
+
return true;
|
|
166
|
+
}
|
|
167
|
+
const softHit = SOFT_SUSPICION_PHRASES.find(p => lower.includes(p));
|
|
168
|
+
if (softHit) {
|
|
169
|
+
this.noteSuspicionSignal(softHit, false);
|
|
170
|
+
return true;
|
|
171
|
+
}
|
|
172
|
+
return false;
|
|
128
173
|
}
|
|
129
174
|
|
|
130
175
|
/** Inspects a thrown error's message for suspicion phrases. */
|
|
@@ -138,6 +183,7 @@ class SendPacer {
|
|
|
138
183
|
if (Date.now() - this.cooldown.since >= this.cooldown.durationMs) {
|
|
139
184
|
this.cooldown.active = false;
|
|
140
185
|
this.cooldown.strikes = 0;
|
|
186
|
+
this.softStrikeTimes = [];
|
|
141
187
|
this.cooldown.since = null;
|
|
142
188
|
return false;
|
|
143
189
|
}
|
|
@@ -151,6 +197,11 @@ class SendPacer {
|
|
|
151
197
|
* rather than blocking a real message.
|
|
152
198
|
*/
|
|
153
199
|
async waitForTurn(threadID) {
|
|
200
|
+
if (this.accountSafetyBlocked) {
|
|
201
|
+
const err = new Error(`Outbound messaging paused after Facebook security signal: ${this.accountSafetyReason}. Review the account manually before restarting.`);
|
|
202
|
+
err.code = "ACCOUNT_SAFETY_PAUSED";
|
|
203
|
+
throw err;
|
|
204
|
+
}
|
|
154
205
|
try {
|
|
155
206
|
this._rollWindows();
|
|
156
207
|
|
|
@@ -185,8 +236,10 @@ class SendPacer {
|
|
|
185
236
|
this.daySendCount++;
|
|
186
237
|
const threadCount = (this.threadSendCounts.get(key) || 0) + 1;
|
|
187
238
|
this.threadSendCounts.set(key, threadCount);
|
|
188
|
-
} catch {
|
|
189
|
-
//
|
|
239
|
+
} catch (err) {
|
|
240
|
+
// Safety-related errors must not be swallowed.
|
|
241
|
+
if (err && err.code === "ACCOUNT_SAFETY_PAUSED") throw err;
|
|
242
|
+
log.warn("AntiSuspension", `Pacing check failed: ${err && err.message ? err.message : String(err)}`);
|
|
190
243
|
}
|
|
191
244
|
}
|
|
192
245
|
|
|
@@ -201,7 +254,9 @@ class SendPacer {
|
|
|
201
254
|
hourlyCap: this._hourlyCap(),
|
|
202
255
|
daySendCount: this.daySendCount,
|
|
203
256
|
dailyCap: this.dailyCap,
|
|
204
|
-
coolingDown: this.isCoolingDown()
|
|
257
|
+
coolingDown: this.isCoolingDown(),
|
|
258
|
+
safetyBlocked: this.accountSafetyBlocked,
|
|
259
|
+
safetyReason: this.accountSafetyReason
|
|
205
260
|
};
|
|
206
261
|
}
|
|
207
262
|
}
|
|
@@ -210,10 +265,15 @@ const pacer = new SendPacer();
|
|
|
210
265
|
|
|
211
266
|
module.exports = {
|
|
212
267
|
SendPacer,
|
|
268
|
+
HARD_SUSPICION_PHRASES,
|
|
269
|
+
SOFT_SUSPICION_PHRASES,
|
|
270
|
+
SUSPICION_PHRASES,
|
|
213
271
|
globalAntiSuspension: {
|
|
214
272
|
prepareBeforeMessage: (threadID) => pacer.waitForTurn(threadID),
|
|
215
273
|
detectSuspensionSignal: (text) => pacer.scanForSuspicion(text),
|
|
216
274
|
checkAccountHealth: (err) => pacer.noteSendError(err),
|
|
217
|
-
getStats: () => pacer.stats()
|
|
275
|
+
getStats: () => pacer.stats(),
|
|
276
|
+
isAccountSafetyBlocked: () => pacer.accountSafetyBlocked,
|
|
277
|
+
getAccountSafetyReason: () => pacer.accountSafetyReason
|
|
218
278
|
}
|
|
219
279
|
};
|
package/src/utils/cookies.js
CHANGED
|
@@ -2,6 +2,28 @@
|
|
|
2
2
|
|
|
3
3
|
// Cookie helpers extracted from client.js
|
|
4
4
|
|
|
5
|
+
const AUTH_COOKIES = new Set(["c_user", "xs", "i_user", "datr", "sb", "fr"]);
|
|
6
|
+
|
|
7
|
+
// True for a Set-Cookie header that deletes one of the login cookies
|
|
8
|
+
// (empty / "deleted" value, Max-Age<=0, or an expiry in the past).
|
|
9
|
+
function isAuthCookieDeletion(setCookieHeader) {
|
|
10
|
+
const first = String(setCookieHeader || "").split(";")[0];
|
|
11
|
+
const eq = first.indexOf("=");
|
|
12
|
+
if (eq <= 0) return false;
|
|
13
|
+
const name = first.slice(0, eq).trim();
|
|
14
|
+
if (!AUTH_COOKIES.has(name)) return false;
|
|
15
|
+
const value = first.slice(eq + 1).trim().replace(/^"|"$/g, "");
|
|
16
|
+
if (value === "" || /^deleted$/i.test(value)) return true;
|
|
17
|
+
const maxAge = /;\s*max-age\s*=\s*(-?\d+)/i.exec(setCookieHeader);
|
|
18
|
+
if (maxAge && parseInt(maxAge[1], 10) <= 0) return true;
|
|
19
|
+
const expires = /;\s*expires\s*=\s*([^;]+)/i.exec(setCookieHeader);
|
|
20
|
+
if (expires) {
|
|
21
|
+
const t = Date.parse(expires[1]);
|
|
22
|
+
if (!Number.isNaN(t) && t <= Date.now()) return true;
|
|
23
|
+
}
|
|
24
|
+
return false;
|
|
25
|
+
}
|
|
26
|
+
|
|
5
27
|
function saveCookies(jar) {
|
|
6
28
|
return res => {
|
|
7
29
|
try {
|
|
@@ -29,6 +51,11 @@ function saveCookies(jar) {
|
|
|
29
51
|
const mirrored = c.replace(/domain=\.?facebook\.com/i, "domain=.messenger.com");
|
|
30
52
|
jar.setCookieSync(mirrored, "https://www.messenger.com");
|
|
31
53
|
} else if (/domain=\.?messenger\.com/i.test(c)) {
|
|
54
|
+
// A messenger.com "delete this cookie" answer must never wipe the matching
|
|
55
|
+
// facebook.com login cookie in our jar: that would silently kill a perfectly
|
|
56
|
+
// good facebook.com session (the cookie values are mirrored, so messenger.com
|
|
57
|
+
// may well reject them while facebook.com is fine).
|
|
58
|
+
if (isAuthCookieDeletion(c)) continue;
|
|
32
59
|
const mirrored = c.replace(/domain=\.?messenger\.com/i, "domain=.facebook.com");
|
|
33
60
|
jar.setCookieSync(mirrored, "https://www.facebook.com");
|
|
34
61
|
}
|
|
@@ -80,6 +107,7 @@ function getAppState(jar) {
|
|
|
80
107
|
|
|
81
108
|
module.exports = {
|
|
82
109
|
saveCookies,
|
|
83
|
-
getAppState
|
|
110
|
+
getAppState,
|
|
111
|
+
isAuthCookieDeletion
|
|
84
112
|
};
|
|
85
113
|
|