@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 +1 -1
- package/src/apis/listenMqtt.js +32 -9
- package/src/utils/axios.js +15 -1
- package/src/utils/clients.js +87 -34
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyneoaz/metachat",
|
|
3
|
-
"version": "1.0.
|
|
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.",
|
package/src/apis/listenMqtt.js
CHANGED
|
@@ -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
|
-
|
|
813
|
-
|
|
814
|
-
|
|
815
|
-
|
|
816
|
-
|
|
817
|
-
utils.error("MQTT",
|
|
818
|
-
|
|
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;
|
package/src/utils/axios.js
CHANGED
|
@@ -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;
|
package/src/utils/clients.js
CHANGED
|
@@ -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
|
-
|
|
111
|
-
ctx.jar.
|
|
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' && (
|
|
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
|
-
//
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
//
|
|
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.
|
|
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
|
-
|
|
287
|
-
|
|
288
|
-
|
|
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 = {
|