@lazyneoaz/testfca 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 +2 -2
- package/prebuilt/linux-x64-gnu/messagix.so +0 -0
- package/scripts/build-go.mjs +8 -1
- package/scripts/download-prebuilt.mjs +6 -17
- package/scripts/postinstall.mjs +17 -35
- package/src/apis/getThreadInfo.js +82 -10
- package/src/apis/listenMqtt.js +225 -36
- package/src/apis/sendMessage.js +70 -9
- package/src/utils/axios.js +3 -0
- package/src/utils/clients.js +8 -0
- package/src/utils/formatters.js +7 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyneoaz/testfca",
|
|
3
|
-
"version": "1.0.
|
|
3
|
+
"version": "1.0.3",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"description": "Advanced Facebook Chat API client for building Messenger bots — supports real-time messaging, thread management, MQTT, session stability, anti-automation protection, and real E2EE via Signal Protocol.",
|
|
6
6
|
"main": "index.js",
|
|
@@ -84,7 +84,7 @@
|
|
|
84
84
|
"typescript": "^5.8.3"
|
|
85
85
|
},
|
|
86
86
|
"engines": {
|
|
87
|
-
"node": ">=
|
|
87
|
+
"node": ">=20.18.1"
|
|
88
88
|
},
|
|
89
89
|
"scripts": {
|
|
90
90
|
"prepack": "echo 'Preparing package for npm...'",
|
|
Binary file
|
package/scripts/build-go.mjs
CHANGED
|
@@ -17,7 +17,14 @@ function runGo(args) {
|
|
|
17
17
|
const res = spawnSync(process.env.GO_BIN || "go", args, {
|
|
18
18
|
cwd: bridgeDir,
|
|
19
19
|
stdio: "inherit",
|
|
20
|
-
env: {
|
|
20
|
+
env: {
|
|
21
|
+
...process.env,
|
|
22
|
+
CGO_ENABLED: "1",
|
|
23
|
+
// Prevent Go from downloading a newer toolchain when go.mod specifies
|
|
24
|
+
// a version higher than what is locally installed. The local compiler
|
|
25
|
+
// should always be used in sandboxed / offline environments.
|
|
26
|
+
GOTOOLCHAIN: "local",
|
|
27
|
+
},
|
|
21
28
|
});
|
|
22
29
|
if (res.error) {
|
|
23
30
|
console.error(`[${name}] Failed to spawn Go: ${res.error.message}`);
|
|
@@ -9,24 +9,13 @@ import { packageJson as pkg } from "./package.mjs";
|
|
|
9
9
|
|
|
10
10
|
const __dirname = dirname(fileURLToPath(import.meta.url));
|
|
11
11
|
|
|
12
|
-
function defaultRepoSlug() {
|
|
13
|
-
const repo = pkg.repository;
|
|
14
|
-
if (typeof repo === "string") {
|
|
15
|
-
const m = repo.match(/github:(.+)/i);
|
|
16
|
-
if (m) return m[1];
|
|
17
|
-
if (/^[\w-]+\/[\w.-]+$/.test(repo)) return repo;
|
|
18
|
-
} else if (repo && typeof repo === "object" && repo.url) {
|
|
19
|
-
const m = repo.url.match(/github\.com[:/]+([^#]+?)(?:\.git)?$/i);
|
|
20
|
-
if (m) return m[1];
|
|
21
|
-
}
|
|
22
|
-
return "NeoKEX/nkxfca";
|
|
23
|
-
}
|
|
24
|
-
|
|
25
12
|
function buildBaseURL() {
|
|
26
|
-
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
13
|
+
// yumi-team/meta-messenger.js hosts prebuilt messagix binaries for all
|
|
14
|
+
// platforms except linux-x64-gnu, which ships directly in this package's
|
|
15
|
+
// prebuilt/ directory and is therefore never fetched from here.
|
|
16
|
+
const repo = "yumi-team/meta-messenger.js";
|
|
17
|
+
const tag = "v1.1.3";
|
|
18
|
+
return `https://github.com/${repo}/releases/download/${tag}`;
|
|
30
19
|
}
|
|
31
20
|
|
|
32
21
|
function httpGet(url, redirectCount = 0) {
|
package/scripts/postinstall.mjs
CHANGED
|
@@ -1,6 +1,5 @@
|
|
|
1
|
-
import { spawnSync } from "node:child_process";
|
|
2
1
|
import { existsSync } from "node:fs";
|
|
3
|
-
import { copyFile, mkdir
|
|
2
|
+
import { copyFile, mkdir } from "node:fs/promises";
|
|
4
3
|
import { dirname, join } from "node:path";
|
|
5
4
|
import { fileURLToPath } from "node:url";
|
|
6
5
|
|
|
@@ -49,13 +48,8 @@ async function run() {
|
|
|
49
48
|
console.log(`[${pkg.name}] Checking local prebuilt at: ${prebuilt}`);
|
|
50
49
|
if (await copyIfExists(prebuilt, buildOut)) {
|
|
51
50
|
console.log(`[${pkg.name}] Using local prebuilt for ${triplet}`);
|
|
52
|
-
|
|
53
|
-
|
|
54
|
-
await rm(prebuiltDir, { recursive: true, force: true });
|
|
55
|
-
} catch {
|
|
56
|
-
//
|
|
57
|
-
}
|
|
58
|
-
}
|
|
51
|
+
// Keep the prebuilt copy so reinstalls (e.g. npm rebuild, CI caches) don't
|
|
52
|
+
// lose the patched binary and fall back to downloading an unpatched one.
|
|
59
53
|
console.log(`[${pkg.name}] postinstall completed in ${Date.now() - startTime}ms`);
|
|
60
54
|
return;
|
|
61
55
|
}
|
|
@@ -77,32 +71,20 @@ async function run() {
|
|
|
77
71
|
);
|
|
78
72
|
}
|
|
79
73
|
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
} catch (err) {
|
|
95
|
-
console.warn(`[${pkg.name}] Local build failed:`, err?.message || String(err));
|
|
96
|
-
}
|
|
97
|
-
}
|
|
98
|
-
|
|
99
|
-
console.warn(`[${pkg.name}] No prebuilt found (local/remote) and no local build completed.`);
|
|
100
|
-
console.warn(`[${pkg.name}] Expected triplet: ${triplet}, file: messagix-${triplet}.${ext}`);
|
|
101
|
-
console.warn(
|
|
102
|
-
`[${pkg.name}] You can:\n` +
|
|
103
|
-
" - set MESSAGIX_BUILD_FROM_SOURCE=true and re-run install\n" +
|
|
104
|
-
" - or build manually with: npm run build:go",
|
|
105
|
-
);
|
|
74
|
+
console.warn(`[${pkg.name}] ──────────────────────────────────────────────────────────`);
|
|
75
|
+
console.warn(`[${pkg.name}] No prebuilt E2EE bridge found for your platform.`);
|
|
76
|
+
console.warn(`[${pkg.name}] Platform detected: ${triplet}`);
|
|
77
|
+
console.warn(`[${pkg.name}]`);
|
|
78
|
+
console.warn(`[${pkg.name}] Supported out-of-the-box:`);
|
|
79
|
+
console.warn(`[${pkg.name}] linux-x64-gnu (ships inside the npm package)`);
|
|
80
|
+
console.warn(`[${pkg.name}] linux-x64-musl / darwin-x64 / darwin-arm64 / win32-x64`);
|
|
81
|
+
console.warn(`[${pkg.name}] (downloaded automatically from yumi-team/meta-messenger.js)`);
|
|
82
|
+
console.warn(`[${pkg.name}]`);
|
|
83
|
+
console.warn(`[${pkg.name}] If the download failed, check your internet connection and retry:`);
|
|
84
|
+
console.warn(`[${pkg.name}] npm install`);
|
|
85
|
+
console.warn(`[${pkg.name}]`);
|
|
86
|
+
console.warn(`[${pkg.name}] Non-E2EE features (sendMessage, listen, etc.) work without the bridge.`);
|
|
87
|
+
console.warn(`[${pkg.name}] ──────────────────────────────────────────────────────────`);
|
|
106
88
|
}
|
|
107
89
|
|
|
108
90
|
run()
|
|
@@ -40,6 +40,65 @@ function formatEventReminders(reminder) {
|
|
|
40
40
|
* @returns {Object | null} A formatted thread object or null if data is invalid.
|
|
41
41
|
* @throws {Error} If Facebook returns a GraphQL error
|
|
42
42
|
*/
|
|
43
|
+
/**
|
|
44
|
+
* Returns a minimal synthetic thread-info object for E2EE DM threads that
|
|
45
|
+
* GraphQL does not expose. Goat Bot needs certain fields to avoid crashing.
|
|
46
|
+
*/
|
|
47
|
+
function buildE2EEThreadInfo(threadID, botUserID) {
|
|
48
|
+
const tid = String(threadID);
|
|
49
|
+
const participants = [tid];
|
|
50
|
+
if (botUserID && String(botUserID) !== tid) participants.push(String(botUserID));
|
|
51
|
+
return {
|
|
52
|
+
threadID: tid,
|
|
53
|
+
threadName: "",
|
|
54
|
+
name: "",
|
|
55
|
+
participantIDs: participants,
|
|
56
|
+
userInfo: participants.map(id => ({
|
|
57
|
+
id, name: "", firstName: "", vanity: "", url: "",
|
|
58
|
+
thumbSrc: "", profileUrl: "", gender: 0,
|
|
59
|
+
type: "User", isFriend: false, isBirthday: false,
|
|
60
|
+
})),
|
|
61
|
+
unreadCount: 0,
|
|
62
|
+
messageCount: 0,
|
|
63
|
+
timestamp: String(Date.now()),
|
|
64
|
+
serverTimestamp: String(Date.now()),
|
|
65
|
+
muteUntil: null,
|
|
66
|
+
isGroup: false,
|
|
67
|
+
threadType: 1,
|
|
68
|
+
isSubscribed: true,
|
|
69
|
+
isArchived: false,
|
|
70
|
+
folder: "INBOX",
|
|
71
|
+
cannotReplyReason: null,
|
|
72
|
+
canReply: true,
|
|
73
|
+
eventReminders: null,
|
|
74
|
+
emoji: null,
|
|
75
|
+
color: null,
|
|
76
|
+
threadTheme: null,
|
|
77
|
+
nicknames: {},
|
|
78
|
+
adminIDs: [],
|
|
79
|
+
approvalMode: false,
|
|
80
|
+
approvalQueue: [],
|
|
81
|
+
reactionsMuteMode: "reactions_not_muted",
|
|
82
|
+
mentionsMuteMode: "mentions_not_muted",
|
|
83
|
+
isPinProtected: false,
|
|
84
|
+
relatedPageThread: null,
|
|
85
|
+
snippet: null,
|
|
86
|
+
snippetSender: null,
|
|
87
|
+
snippetAttachments: [],
|
|
88
|
+
imageSrc: null,
|
|
89
|
+
isCanonicalUser: true,
|
|
90
|
+
isCanonical: true,
|
|
91
|
+
recipientsLoadable: true,
|
|
92
|
+
hasEmailParticipant: false,
|
|
93
|
+
readOnly: false,
|
|
94
|
+
lastMessageTimestamp: null,
|
|
95
|
+
lastMessageType: "message",
|
|
96
|
+
lastReadTimestamp: null,
|
|
97
|
+
inviteLink: { enable: false, link: null },
|
|
98
|
+
_isE2EESynthetic: true,
|
|
99
|
+
};
|
|
100
|
+
}
|
|
101
|
+
|
|
43
102
|
function formatThreadGraphQLResponse(data) {
|
|
44
103
|
// Check for GraphQL errors and throw with details instead of silently returning null
|
|
45
104
|
if (data.errors) {
|
|
@@ -52,14 +111,15 @@ function formatThreadGraphQLResponse(data) {
|
|
|
52
111
|
utils.error("formatThreadGraphQLResponse", error);
|
|
53
112
|
throw error;
|
|
54
113
|
}
|
|
55
|
-
|
|
114
|
+
|
|
56
115
|
const messageThread = data.message_thread;
|
|
57
116
|
if (!messageThread) {
|
|
117
|
+
// E2EE DM threads are not exposed via GraphQL — signal the caller to use synthetic fallback
|
|
58
118
|
const error = new Error("No message_thread in GraphQL response - thread may not exist or access may be restricted");
|
|
59
119
|
Object.assign(error, {
|
|
60
|
-
details: "The GraphQL query returned successfully but contained no message_thread data"
|
|
120
|
+
details: "The GraphQL query returned successfully but contained no message_thread data",
|
|
121
|
+
isE2EEThread: true,
|
|
61
122
|
});
|
|
62
|
-
utils.error("formatThreadGraphQLResponse", error);
|
|
63
123
|
throw error;
|
|
64
124
|
}
|
|
65
125
|
|
|
@@ -185,7 +245,7 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
185
245
|
if (!ctx.validator.validateIDArray(threadIDs, ctx.validator.isValidThreadID)) {
|
|
186
246
|
throw new Error("Invalid thread ID(s)");
|
|
187
247
|
}
|
|
188
|
-
|
|
248
|
+
|
|
189
249
|
let form = {};
|
|
190
250
|
threadIDs.forEach((t, i) => {
|
|
191
251
|
form["o" + i] = {
|
|
@@ -217,7 +277,7 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
217
277
|
const threadInfos = {};
|
|
218
278
|
for (let i = resData.length - 2; i >= 0; i--) {
|
|
219
279
|
const res = resData[i];
|
|
220
|
-
|
|
280
|
+
|
|
221
281
|
// Check for error_results and throw instead of silently continuing
|
|
222
282
|
if (res.error_results) {
|
|
223
283
|
const error = new Error(`Facebook returned error_results for thread query: ${res.error_results} errors`);
|
|
@@ -228,10 +288,10 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
228
288
|
utils.error("getThreadInfo", error);
|
|
229
289
|
throw error;
|
|
230
290
|
}
|
|
231
|
-
|
|
291
|
+
|
|
232
292
|
const oKey = Object.keys(res)[0];
|
|
233
293
|
const responseData = res[oKey];
|
|
234
|
-
|
|
294
|
+
|
|
235
295
|
// Check for errors in the response object
|
|
236
296
|
if (responseData.errors || responseData.error_results) {
|
|
237
297
|
const details = responseData.errors
|
|
@@ -246,10 +306,22 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
246
306
|
utils.error("getThreadInfo", error);
|
|
247
307
|
throw error;
|
|
248
308
|
}
|
|
249
|
-
|
|
250
|
-
|
|
309
|
+
|
|
310
|
+
let threadInfo;
|
|
311
|
+
try {
|
|
312
|
+
threadInfo = formatThreadGraphQLResponse(responseData.data);
|
|
313
|
+
} catch (fmtErr) {
|
|
314
|
+
if (fmtErr.isE2EEThread) {
|
|
315
|
+
// E2EE DM thread — GraphQL has no data for it, use synthetic fallback
|
|
316
|
+
const tid = threadIDs[threadIDs.length - 1 - i] || threadIDs[0];
|
|
317
|
+
utils.log("getThreadInfo", "E2EE DM thread " + tid + " not in GraphQL, using synthetic fallback.");
|
|
318
|
+
threadInfo = buildE2EEThreadInfo(tid, ctx.userID);
|
|
319
|
+
} else {
|
|
320
|
+
throw fmtErr;
|
|
321
|
+
}
|
|
322
|
+
}
|
|
251
323
|
if (threadInfo) {
|
|
252
|
-
threadInfos[threadInfo.threadID ||
|
|
324
|
+
threadInfos[threadInfo.threadID || threadIDs[threadIDs.length - 1 - i]] = threadInfo;
|
|
253
325
|
}
|
|
254
326
|
}
|
|
255
327
|
|
package/src/apis/listenMqtt.js
CHANGED
|
@@ -49,7 +49,7 @@ function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) {
|
|
|
49
49
|
} catch (error) {
|
|
50
50
|
lastError = error;
|
|
51
51
|
if (i === maxRetries - 1) throw lastError;
|
|
52
|
-
|
|
52
|
+
|
|
53
53
|
const delay = Math.min(baseDelay * Math.pow(2, i) + Math.random() * 500, 10000);
|
|
54
54
|
await new Promise(resolve => setTimeout(resolve, delay));
|
|
55
55
|
}
|
|
@@ -206,16 +206,16 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
|
|
|
206
206
|
|
|
207
207
|
if (ctx.globalOptions.proxy) options.wsOptions.agent = new HttpsProxyAgent(ctx.globalOptions.proxy);
|
|
208
208
|
ctx._mqttLastConnectAttemptAt = Date.now();
|
|
209
|
-
|
|
209
|
+
|
|
210
210
|
// Create WebSocket stream - using exact fca-unofficial implementation
|
|
211
211
|
let mqttClient;
|
|
212
|
-
|
|
212
|
+
|
|
213
213
|
try {
|
|
214
214
|
const mqtt = require('mqtt');
|
|
215
215
|
const WebSocket = require('ws');
|
|
216
216
|
const { PassThrough, Writable } = require('stream');
|
|
217
217
|
const Duplexify = require('duplexify');
|
|
218
|
-
|
|
218
|
+
|
|
219
219
|
// Exact buildProxy from fca-unofficial
|
|
220
220
|
function buildProxy() {
|
|
221
221
|
let target = null;
|
|
@@ -268,7 +268,7 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
|
|
|
268
268
|
};
|
|
269
269
|
return Proxy;
|
|
270
270
|
}
|
|
271
|
-
|
|
271
|
+
|
|
272
272
|
// Exact buildStream from fca-unofficial
|
|
273
273
|
function buildStream(opts, ws, Proxy) {
|
|
274
274
|
const readable = new PassThrough();
|
|
@@ -280,18 +280,18 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
|
|
|
280
280
|
let attached = false;
|
|
281
281
|
let style = "prop";
|
|
282
282
|
let closed = false;
|
|
283
|
-
|
|
283
|
+
|
|
284
284
|
const toBuffer = d => {
|
|
285
285
|
if (Buffer.isBuffer(d)) return d;
|
|
286
286
|
if (d instanceof ArrayBuffer) return Buffer.from(d);
|
|
287
287
|
if (ArrayBuffer.isView(d)) return Buffer.from(d.buffer, d.byteOffset, d.byteLength);
|
|
288
288
|
return Buffer.from(String(d));
|
|
289
289
|
};
|
|
290
|
-
|
|
290
|
+
|
|
291
291
|
const swapToNoopWritable = () => {
|
|
292
292
|
try { Stream.setWritable(NoopWritable); } catch { }
|
|
293
293
|
};
|
|
294
|
-
|
|
294
|
+
|
|
295
295
|
const onOpen = () => {
|
|
296
296
|
if (closed) return;
|
|
297
297
|
Proxy.setTarget(ws);
|
|
@@ -316,16 +316,16 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
|
|
|
316
316
|
}
|
|
317
317
|
}, 10000);
|
|
318
318
|
};
|
|
319
|
-
|
|
319
|
+
|
|
320
320
|
const onMessage = data => {
|
|
321
321
|
lastActivity = Date.now();
|
|
322
322
|
readable.write(toBuffer(style === "dom" && data && data.data !== undefined ? data.data : data));
|
|
323
323
|
};
|
|
324
|
-
|
|
324
|
+
|
|
325
325
|
const onPong = () => {
|
|
326
326
|
lastActivity = Date.now();
|
|
327
327
|
};
|
|
328
|
-
|
|
328
|
+
|
|
329
329
|
const cleanup = () => {
|
|
330
330
|
if (closed) return;
|
|
331
331
|
closed = true;
|
|
@@ -346,18 +346,18 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
|
|
|
346
346
|
}
|
|
347
347
|
readable.end();
|
|
348
348
|
};
|
|
349
|
-
|
|
349
|
+
|
|
350
350
|
const onError = err => {
|
|
351
351
|
cleanup();
|
|
352
352
|
Stream.destroy(err);
|
|
353
353
|
};
|
|
354
|
-
|
|
354
|
+
|
|
355
355
|
const onClose = () => {
|
|
356
356
|
cleanup();
|
|
357
357
|
Stream.end();
|
|
358
358
|
if (!Stream.destroyed) Stream.destroy();
|
|
359
359
|
};
|
|
360
|
-
|
|
360
|
+
|
|
361
361
|
const attach = w => {
|
|
362
362
|
if (attached || !w) return;
|
|
363
363
|
attached = true;
|
|
@@ -382,7 +382,7 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
|
|
|
382
382
|
w.onclose = onClose;
|
|
383
383
|
}
|
|
384
384
|
};
|
|
385
|
-
|
|
385
|
+
|
|
386
386
|
const detach = w => {
|
|
387
387
|
if (!attached || !w) return;
|
|
388
388
|
attached = false;
|
|
@@ -404,24 +404,24 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
|
|
|
404
404
|
w.onclose = null;
|
|
405
405
|
}
|
|
406
406
|
};
|
|
407
|
-
|
|
407
|
+
|
|
408
408
|
attach(ws);
|
|
409
409
|
if (ws && ws.readyState === 1) onOpen();
|
|
410
|
-
|
|
410
|
+
|
|
411
411
|
Stream.on("prefinish", swapToNoopWritable);
|
|
412
412
|
Stream.on("finish", cleanup);
|
|
413
413
|
Stream.on("close", cleanup);
|
|
414
414
|
Proxy.on("close", swapToNoopWritable);
|
|
415
|
-
|
|
415
|
+
|
|
416
416
|
return Stream;
|
|
417
417
|
}
|
|
418
|
-
|
|
418
|
+
|
|
419
419
|
// Create MQTT client exactly like fca-unofficial
|
|
420
420
|
mqttClient = new mqtt.Client(
|
|
421
421
|
() => buildStream(options, new WebSocket(host, options.wsOptions), buildProxy()),
|
|
422
422
|
options
|
|
423
423
|
);
|
|
424
|
-
|
|
424
|
+
|
|
425
425
|
mqttClient.publishSync = mqttClient.publish.bind(mqttClient);
|
|
426
426
|
mqttClient.publish = (topic, message, opts = {}, callback = () => {}) => new Promise((resolve, reject) => {
|
|
427
427
|
mqttClient.publishSync(topic, message, opts, (err, data) => {
|
|
@@ -543,10 +543,188 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
|
|
|
543
543
|
queue.sync_token = ctx.syncToken;
|
|
544
544
|
}
|
|
545
545
|
mqttClient.publish(topic, JSON.stringify(queue), { qos: 1, retain: false });
|
|
546
|
+
|
|
547
|
+
// Also subscribe to E2EE DM sync group (sync_group 2) for direct messages
|
|
548
|
+
const e2eeQueue = {
|
|
549
|
+
sync_api_version: 11,
|
|
550
|
+
max_deltas_able_to_process: 200,
|
|
551
|
+
delta_batch_size: 200,
|
|
552
|
+
encoding: "JSON",
|
|
553
|
+
entity_fbid: ctx.userID,
|
|
554
|
+
initial_titan_sequence_id: ctx.lastSeqId,
|
|
555
|
+
device_params: null,
|
|
556
|
+
sync_group: 2
|
|
557
|
+
};
|
|
558
|
+
const e2eeTopic = ctx.e2eeSyncToken ? "/messenger_sync_get_diffs" : "/messenger_sync_create_queue";
|
|
559
|
+
if (ctx.e2eeSyncToken) {
|
|
560
|
+
e2eeQueue.last_seq_id = ctx.lastSeqId;
|
|
561
|
+
e2eeQueue.sync_token = ctx.e2eeSyncToken;
|
|
562
|
+
}
|
|
563
|
+
mqttClient.publish(e2eeTopic, JSON.stringify(e2eeQueue), { qos: 1, retain: false });
|
|
564
|
+
|
|
546
565
|
mqttClient.publish("/foreground_state", JSON.stringify({ foreground: chatOn }), { qos: 1 });
|
|
547
566
|
mqttClient.publish("/set_client_settings", JSON.stringify({ make_user_available_when_in_foreground: true }), { qos: 1 });
|
|
548
|
-
|
|
549
|
-
utils.log("MQTT", "Queue setup messages sent");
|
|
567
|
+
|
|
568
|
+
utils.log("MQTT", "Queue setup messages sent (sync_group 1 + 2)");
|
|
569
|
+
|
|
570
|
+
// E2EE bridge polling loop — receives incoming DMs via messagix.so
|
|
571
|
+
if (api.e2ee && api.e2ee.isAvailable() && !ctx._e2eePollingActive) {
|
|
572
|
+
ctx._e2eePollingActive = true;
|
|
573
|
+
(async function bridgePollLoop() {
|
|
574
|
+
try {
|
|
575
|
+
const status = api.e2ee.isConnected();
|
|
576
|
+
if (!status.connected) {
|
|
577
|
+
utils.log("E2EE", "Connecting bridge for DM receive...");
|
|
578
|
+
await api.e2ee.connect();
|
|
579
|
+
utils.log("E2EE", "Bridge transport connected.");
|
|
580
|
+
}
|
|
581
|
+
if (!api.e2ee.isConnected().e2eeConnected) {
|
|
582
|
+
utils.log("E2EE", "Registering E2EE device keys...");
|
|
583
|
+
await api.e2ee.connectE2EE();
|
|
584
|
+
utils.log("E2EE", "E2EE ready. Starting DM poll loop.");
|
|
585
|
+
} else {
|
|
586
|
+
utils.log("E2EE", "Bridge already E2EE-ready. Starting DM poll loop.");
|
|
587
|
+
}
|
|
588
|
+
} catch (bridgeErr) {
|
|
589
|
+
utils.warn("E2EE", "Bridge connect failed (DMs via MQTT only):", bridgeErr && bridgeErr.message ? bridgeErr.message : bridgeErr);
|
|
590
|
+
ctx._e2eePollingActive = false;
|
|
591
|
+
return;
|
|
592
|
+
}
|
|
593
|
+
while (ctx._e2eePollingActive) {
|
|
594
|
+
try {
|
|
595
|
+
// Yield to event loop before blocking poll (same pattern as yumi-team)
|
|
596
|
+
await new Promise(resolve => setImmediate(resolve));
|
|
597
|
+
if (!ctx._e2eePollingActive) break;
|
|
598
|
+
|
|
599
|
+
// Poll with 1 s timeout — returns a typed event object, NOT an array
|
|
600
|
+
const ev = await api.e2ee.pollEvents(1000);
|
|
601
|
+
if (!ctx._e2eePollingActive) break;
|
|
602
|
+
|
|
603
|
+
// No event / empty result
|
|
604
|
+
if (!ev) continue;
|
|
605
|
+
|
|
606
|
+
const evType = (ev.type || '').toLowerCase();
|
|
607
|
+
|
|
608
|
+
// ── timeout: no event arrived during the poll window ──
|
|
609
|
+
if (evType === 'timeout') continue;
|
|
610
|
+
|
|
611
|
+
// ── closed: Go bridge shut down ──
|
|
612
|
+
if (evType === 'closed') {
|
|
613
|
+
utils.warn("E2EE", "Bridge closed. Stopping DM poll loop.");
|
|
614
|
+
ctx._e2eePollingActive = false;
|
|
615
|
+
break;
|
|
616
|
+
}
|
|
617
|
+
|
|
618
|
+
// ── permanent error (e.g. session invalid) ──
|
|
619
|
+
if (evType === 'error') {
|
|
620
|
+
const msg = ev.data && ev.data.message ? ev.data.message : JSON.stringify(ev.data);
|
|
621
|
+
utils.warn("E2EE", "Bridge error event:", msg);
|
|
622
|
+
if (ev.data && ev.data.code === 1) {
|
|
623
|
+
ctx._e2eePollingActive = false;
|
|
624
|
+
break;
|
|
625
|
+
}
|
|
626
|
+
continue;
|
|
627
|
+
}
|
|
628
|
+
|
|
629
|
+
// ── E2EE connection confirmed by the bridge ──
|
|
630
|
+
if (evType === 'e2eeconnected') {
|
|
631
|
+
utils.log("E2EE", "E2EE fully connected (bridge event).");
|
|
632
|
+
continue;
|
|
633
|
+
}
|
|
634
|
+
|
|
635
|
+
// ── Incoming E2EE DM ─────────────────────────────────
|
|
636
|
+
if (evType === 'e2eemessage') {
|
|
637
|
+
const d = ev.data;
|
|
638
|
+
if (!d) continue;
|
|
639
|
+
// threadId may be 0 for pure-JID threads — fall back to chatJid
|
|
640
|
+
const chatJid = d.chatJid || '';
|
|
641
|
+
const senderJid = d.senderJid || '';
|
|
642
|
+
const threadID = String(d.threadId || chatJid.replace(/@.*/, '') || '');
|
|
643
|
+
const senderID = String(d.senderId || senderJid.replace(/@.*/, '') || '');
|
|
644
|
+
if (!threadID || !senderID) {
|
|
645
|
+
utils.log("E2EE", "e2eeMessage missing IDs — raw:", JSON.stringify(d).slice(0, 300));
|
|
646
|
+
continue;
|
|
647
|
+
}
|
|
648
|
+
if (senderID === ctx.userID.toString() && !ctx.globalOptions.selfListen) continue;
|
|
649
|
+
|
|
650
|
+
utils.log("E2EE", "Incoming E2EE DM from", senderID, "→ thread", threadID, "body:", JSON.stringify(d.text || '').slice(0, 80));
|
|
651
|
+
|
|
652
|
+
const fmtMsg = {
|
|
653
|
+
type: 'message',
|
|
654
|
+
senderID: utils.formatID(senderID),
|
|
655
|
+
body: d.text || '',
|
|
656
|
+
threadID: utils.formatID(threadID),
|
|
657
|
+
messageID: d.id || '',
|
|
658
|
+
isGroup: false,
|
|
659
|
+
attachments: [],
|
|
660
|
+
mentions: {},
|
|
661
|
+
timestamp: Number(d.timestampMs || Date.now()),
|
|
662
|
+
isUnread: true,
|
|
663
|
+
};
|
|
664
|
+
if (ctx.globalOptions.autoMarkDelivery) {
|
|
665
|
+
try { api.markAsDelivered(fmtMsg.threadID, fmtMsg.messageID); } catch (_) {}
|
|
666
|
+
}
|
|
667
|
+
if (ctx.globalOptions.autoMarkRead) {
|
|
668
|
+
try { api.markAsRead(fmtMsg.threadID); } catch (_) {}
|
|
669
|
+
}
|
|
670
|
+
globalCallback(null, fmtMsg);
|
|
671
|
+
continue;
|
|
672
|
+
}
|
|
673
|
+
|
|
674
|
+
// ── Incoming regular (non-E2EE) message via bridge ───
|
|
675
|
+
if (evType === 'message') {
|
|
676
|
+
const d = ev.data;
|
|
677
|
+
if (!d) continue;
|
|
678
|
+
const threadID = String(d.threadId || '');
|
|
679
|
+
const senderID = String(d.senderId || '');
|
|
680
|
+
if (!threadID || !senderID) continue;
|
|
681
|
+
if (senderID === ctx.userID.toString() && !ctx.globalOptions.selfListen) continue;
|
|
682
|
+
globalCallback(null, {
|
|
683
|
+
type: 'message',
|
|
684
|
+
senderID: utils.formatID(senderID),
|
|
685
|
+
body: d.text || '',
|
|
686
|
+
threadID: utils.formatID(threadID),
|
|
687
|
+
messageID: d.id || '',
|
|
688
|
+
isGroup: !!(d.isGroup),
|
|
689
|
+
attachments: [],
|
|
690
|
+
mentions: {},
|
|
691
|
+
timestamp: Number(d.timestampMs || Date.now()),
|
|
692
|
+
isUnread: true,
|
|
693
|
+
});
|
|
694
|
+
continue;
|
|
695
|
+
}
|
|
696
|
+
|
|
697
|
+
// ── E2EE reaction ────────────────────────────────────
|
|
698
|
+
if (evType === 'e2eereaction') {
|
|
699
|
+
if (!ctx.globalOptions.listenEvents) continue;
|
|
700
|
+
const d = ev.data;
|
|
701
|
+
if (!d) continue;
|
|
702
|
+
const chatJid = d.chatJid || '';
|
|
703
|
+
const senderJid = d.senderJid || '';
|
|
704
|
+
globalCallback(null, {
|
|
705
|
+
type: 'message_reaction',
|
|
706
|
+
threadID: utils.formatID(chatJid.replace(/@.*/, '')),
|
|
707
|
+
messageID: d.messageId || '',
|
|
708
|
+
reaction: d.reaction || '',
|
|
709
|
+
senderID: utils.formatID(senderJid.replace(/@.*/, '')),
|
|
710
|
+
offlineThreadingId: null,
|
|
711
|
+
timestamp: Date.now(),
|
|
712
|
+
isGroup: false,
|
|
713
|
+
});
|
|
714
|
+
continue;
|
|
715
|
+
}
|
|
716
|
+
|
|
717
|
+
// ready / reconnected / disconnected / deviceDataChanged / raw → ignore silently
|
|
718
|
+
|
|
719
|
+
} catch (pollErr) {
|
|
720
|
+
if (!ctx._e2eePollingActive) break;
|
|
721
|
+
utils.warn("E2EE", "pollEvents error:", pollErr && pollErr.message ? pollErr.message : pollErr);
|
|
722
|
+
await new Promise(r => setTimeout(r, 3000));
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
utils.log("E2EE", "Poll loop stopped.");
|
|
726
|
+
})();
|
|
727
|
+
}
|
|
550
728
|
|
|
551
729
|
// Disable T_MS timeout to prevent connection cycling
|
|
552
730
|
if (ctx._tmsTimeout) {
|
|
@@ -564,7 +742,7 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
|
|
|
564
742
|
}
|
|
565
743
|
delete ctx.tmsWait;
|
|
566
744
|
};
|
|
567
|
-
|
|
745
|
+
|
|
568
746
|
// Immediately mark as ready since we're connected
|
|
569
747
|
if (ctx.tmsWait && typeof ctx.tmsWait === "function") ctx.tmsWait();
|
|
570
748
|
}));
|
|
@@ -592,7 +770,11 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
|
|
|
592
770
|
|
|
593
771
|
if (jsonMessage.firstDeltaSeqId && jsonMessage.syncToken) {
|
|
594
772
|
ctx.lastSeqId = jsonMessage.firstDeltaSeqId;
|
|
595
|
-
ctx.syncToken
|
|
773
|
+
if (!ctx.syncToken) {
|
|
774
|
+
ctx.syncToken = jsonMessage.syncToken;
|
|
775
|
+
} else if (!ctx.e2eeSyncToken && jsonMessage.syncToken !== ctx.syncToken) {
|
|
776
|
+
ctx.e2eeSyncToken = jsonMessage.syncToken;
|
|
777
|
+
}
|
|
596
778
|
}
|
|
597
779
|
if (jsonMessage.lastIssuedSeqId) {
|
|
598
780
|
ctx.lastSeqId = parseInt(jsonMessage.lastIssuedSeqId);
|
|
@@ -600,8 +782,11 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
|
|
|
600
782
|
|
|
601
783
|
if (jsonMessage.deltas) {
|
|
602
784
|
for (const delta of jsonMessage.deltas) {
|
|
785
|
+
utils.log("MQTT_DELTA", "class=" + (delta.class || "none") + " threadFbId=" + (delta.messageMetadata?.threadKey?.threadFbId || "null") + " otherUserFbId=" + (delta.messageMetadata?.threadKey?.otherUserFbId || "null") + " body=" + (delta.body !== undefined ? JSON.stringify(String(delta.body).slice(0,40)) : "undefined"));
|
|
603
786
|
parseDelta(defaultFuncs, api, ctx, globalCallback, { delta });
|
|
604
787
|
}
|
|
788
|
+
} else if (jsonMessage.firstDeltaSeqId) {
|
|
789
|
+
utils.log("MQTT_DELTA", "Queue ACK: syncToken=" + (jsonMessage.syncToken ? jsonMessage.syncToken.slice(0,20) + "..." : "none") + " firstDeltaSeqId=" + jsonMessage.firstDeltaSeqId);
|
|
605
790
|
}
|
|
606
791
|
} else if (topic === "/thread_typing" || topic === "/orca_typing_notifications") {
|
|
607
792
|
const typ = {
|
|
@@ -657,7 +842,7 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
|
|
|
657
842
|
ctx._mqttQuickCloseCount = 0;
|
|
658
843
|
if (!ctx._mqttReauthing && globalAutoReLoginManager && globalAutoReLoginManager.isEnabled && globalAutoReLoginManager.isEnabled()) {
|
|
659
844
|
ctx._mqttReauthing = true;
|
|
660
|
-
|
|
845
|
+
|
|
661
846
|
// Try to refresh tokens first before full re-login
|
|
662
847
|
try {
|
|
663
848
|
if (api && api.tokenRefreshManager && typeof api.tokenRefreshManager.refreshTokens === 'function') {
|
|
@@ -674,7 +859,7 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
|
|
|
674
859
|
} catch (refreshErr) {
|
|
675
860
|
utils.warn("MQTT", "Token refresh failed, proceeding with full re-login:", refreshErr.message);
|
|
676
861
|
}
|
|
677
|
-
|
|
862
|
+
|
|
678
863
|
globalAutoReLoginManager.handleSessionExpiry(api, 'https://www.facebook.com', "MQTT quick close loop")
|
|
679
864
|
.then((ok) => {
|
|
680
865
|
ctx._mqttReauthing = false;
|
|
@@ -738,7 +923,7 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
|
|
|
738
923
|
ctx._mqttConnected = false;
|
|
739
924
|
if (!ctx._ending && !ctx._cycling && ctx.globalOptions.autoReconnect) {
|
|
740
925
|
try { mqttClient.end(true); } catch (_) { }
|
|
741
|
-
|
|
926
|
+
|
|
742
927
|
// Try token refresh before reconnecting
|
|
743
928
|
try {
|
|
744
929
|
if (api && api.tokenRefreshManager && typeof api.tokenRefreshManager.refreshTokens === 'function') {
|
|
@@ -746,7 +931,7 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
|
|
|
746
931
|
await api.tokenRefreshManager.refreshTokens(ctx, defaultFuncs, 'https://www.facebook.com');
|
|
747
932
|
}
|
|
748
933
|
} catch (_) { /* Ignore refresh errors, will proceed with normal reconnect */ }
|
|
749
|
-
|
|
934
|
+
|
|
750
935
|
// Schedule a reconnect — without this the bot silently stays offline.
|
|
751
936
|
const baseDelay = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000;
|
|
752
937
|
ctx._reconnectAttempts = (ctx._reconnectAttempts || 0) + 1;
|
|
@@ -799,10 +984,10 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
|
|
|
799
984
|
if (isPermanentFailure) {
|
|
800
985
|
ctx._permanentFailure = true;
|
|
801
986
|
}
|
|
802
|
-
|
|
987
|
+
|
|
803
988
|
const msg = detail || reason;
|
|
804
989
|
utils.error("AUTH", `Authentication error -> ${reason}: ${msg}`);
|
|
805
|
-
|
|
990
|
+
|
|
806
991
|
if (typeof globalCallback === "function") {
|
|
807
992
|
globalCallback({
|
|
808
993
|
type: "account_inactive",
|
|
@@ -860,7 +1045,7 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
|
|
|
860
1045
|
function postSafe(...args) {
|
|
861
1046
|
const lastArg = args[args.length - 1];
|
|
862
1047
|
const hasCallback = typeof lastArg === 'function';
|
|
863
|
-
|
|
1048
|
+
|
|
864
1049
|
if (hasCallback) {
|
|
865
1050
|
const originalCallback = args[args.length - 1];
|
|
866
1051
|
args[args.length - 1] = function(err, ...cbArgs) {
|
|
@@ -958,19 +1143,19 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
|
|
|
958
1143
|
utils.log("MQTT", "Getting sequence ID...");
|
|
959
1144
|
ctx.t_mqttCalled = false;
|
|
960
1145
|
const resData = await defaultFuncs.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
961
|
-
|
|
1146
|
+
|
|
962
1147
|
if (utils.getType(resData) !== "Array") {
|
|
963
1148
|
throw { error: "Not logged in" };
|
|
964
1149
|
}
|
|
965
1150
|
if (!Array.isArray(resData) || !resData.length) {
|
|
966
1151
|
throw { error: "getSeqID: empty response" };
|
|
967
1152
|
}
|
|
968
|
-
|
|
1153
|
+
|
|
969
1154
|
const lastRes = resData[resData.length - 1];
|
|
970
1155
|
if (lastRes && lastRes.successful_results === 0) {
|
|
971
1156
|
throw { error: "getSeqID: no successful results" };
|
|
972
1157
|
}
|
|
973
|
-
|
|
1158
|
+
|
|
974
1159
|
const syncSeqId = resData[0] && resData[0].o0 && resData[0].o0.data && resData[0].o0.data.viewer && resData[0].o0.data.viewer.message_threads && resData[0].o0.data.viewer.message_threads.sync_sequence_id;
|
|
975
1160
|
if (syncSeqId) {
|
|
976
1161
|
ctx.lastSeqId = syncSeqId;
|
|
@@ -983,13 +1168,13 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
|
|
|
983
1168
|
} catch (err) {
|
|
984
1169
|
const detail = (err && err.detail && err.detail.message) ? ` | detail=${err.detail.message}` : "";
|
|
985
1170
|
const msg = ((err && err.error) || (err && err.message) || String(err || "")) + detail;
|
|
986
|
-
|
|
1171
|
+
|
|
987
1172
|
if (/blocked the login|checkpoint|security check|authentication.*fail|auth.*fail|login.*block|account.*lock|verification.*requir|banned|disabled/i.test(msg)) {
|
|
988
1173
|
utils.error("MQTT", "Auth error in getSeqID: Session/Login blocked (permanent)");
|
|
989
1174
|
ctx._seqIdFailCount = 0;
|
|
990
1175
|
return emitAuthError("login_blocked", msg);
|
|
991
1176
|
}
|
|
992
|
-
|
|
1177
|
+
|
|
993
1178
|
throw err; // Re-throw for retry mechanism
|
|
994
1179
|
}
|
|
995
1180
|
}, 3, 1500);
|
|
@@ -1063,10 +1248,12 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
|
|
|
1063
1248
|
ctx.mqttClient = undefined;
|
|
1064
1249
|
ctx.lastSeqId = null;
|
|
1065
1250
|
ctx.syncToken = undefined;
|
|
1251
|
+
ctx.e2eeSyncToken = undefined;
|
|
1066
1252
|
ctx.t_mqttCalled = false;
|
|
1067
1253
|
ctx._ending = false;
|
|
1068
1254
|
ctx._mqttConnected = false;
|
|
1069
1255
|
ctx._seqIdFailCount = 0;
|
|
1256
|
+
ctx._e2eePollingActive = false;
|
|
1070
1257
|
next && next();
|
|
1071
1258
|
};
|
|
1072
1259
|
try {
|
|
@@ -1104,6 +1291,8 @@ module.exports = (defaultFuncs, api, ctx, opts) => {
|
|
|
1104
1291
|
utils.log("MQTT", "Stop requested");
|
|
1105
1292
|
globalCallback = identity;
|
|
1106
1293
|
ctx._listeningActive = false;
|
|
1294
|
+
ctx._e2eePollingActive = false;
|
|
1295
|
+
try { if (api.e2ee && ctx._e2eeBridgeHandle != null) api.e2ee.disconnect(); } catch (_) {}
|
|
1107
1296
|
|
|
1108
1297
|
if (ctx._autoCycleTimer) {
|
|
1109
1298
|
clearInterval(ctx._autoCycleTimer);
|
package/src/apis/sendMessage.js
CHANGED
|
@@ -318,19 +318,80 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
318
318
|
} catch (_) {}
|
|
319
319
|
}
|
|
320
320
|
|
|
321
|
+
// Helper: auto-connect E2EE bridge if available but not yet connected
|
|
322
|
+
async function ensureBridgeConnected() {
|
|
323
|
+
if (!api.e2ee || !api.e2ee.isAvailable()) return false;
|
|
324
|
+
try {
|
|
325
|
+
const status = api.e2ee.isConnected();
|
|
326
|
+
if (status.e2eeConnected) return true;
|
|
327
|
+
if (!status.connected) await api.e2ee.connect();
|
|
328
|
+
if (!api.e2ee.isConnected().e2eeConnected) await api.e2ee.connectE2EE();
|
|
329
|
+
return !!api.e2ee.isConnected().e2eeConnected;
|
|
330
|
+
} catch (err) {
|
|
331
|
+
utils.warn("sendMessage", "E2EE bridge connect failed:", err && err.message ? err.message : err);
|
|
332
|
+
return false;
|
|
333
|
+
}
|
|
334
|
+
}
|
|
335
|
+
|
|
321
336
|
try {
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
337
|
+
if (isSingleUser) {
|
|
338
|
+
// E2EE DMs: bridge → MQTT → HTTP
|
|
339
|
+
const msgBody = typeof msg === 'string' ? msg : (msg.body || '');
|
|
340
|
+
const hasAttachment = msg && typeof msg === 'object' && (msg.attachment || msg.sticker || msg.url);
|
|
341
|
+
const chatJid = String(threadID) + '@msgr';
|
|
342
|
+
|
|
343
|
+
// 1. Bridge path (text messages only; attachments handled by later paths)
|
|
344
|
+
if (!hasAttachment && msgBody) {
|
|
345
|
+
const bridgeReady = await ensureBridgeConnected();
|
|
346
|
+
if (bridgeReady) {
|
|
347
|
+
try {
|
|
348
|
+
const bridgeResult = await api.e2ee.sendE2EEMessage(chatJid, msgBody, replyToMessage || undefined, undefined);
|
|
349
|
+
utils.log("sendMessage", "DM sent via E2EE bridge to " + chatJid);
|
|
350
|
+
return callback(null, bridgeResult || { messageID: null, threadID: String(threadID) });
|
|
351
|
+
} catch (bridgeErr) {
|
|
352
|
+
utils.warn("sendMessage", "Bridge E2EE send failed for " + chatJid + ", trying MQTT:", bridgeErr && bridgeErr.message ? bridgeErr.message : bridgeErr);
|
|
353
|
+
}
|
|
354
|
+
}
|
|
355
|
+
}
|
|
356
|
+
|
|
357
|
+
// 2. MQTT path
|
|
358
|
+
if (api.sendMessageMqtt) {
|
|
359
|
+
try {
|
|
360
|
+
const mqttRes = await api.sendMessageMqtt(msg, threadID, replyToMessage);
|
|
361
|
+
utils.log("sendMessage", "DM sent via MQTT to thread " + threadID);
|
|
362
|
+
return callback(null, mqttRes);
|
|
363
|
+
} catch (mqttErr) {
|
|
364
|
+
utils.warn("sendMessage", "MQTT send failed for DM " + threadID + ", falling back to HTTP:", mqttErr && mqttErr.message ? mqttErr.message : mqttErr);
|
|
365
|
+
}
|
|
366
|
+
}
|
|
367
|
+
|
|
368
|
+
// 3. HTTP fallback
|
|
326
369
|
try {
|
|
327
|
-
const
|
|
328
|
-
callback(null,
|
|
329
|
-
} catch (
|
|
330
|
-
|
|
370
|
+
const result = await sendContent(form, threadID, isSingleUser, messageAndOTID);
|
|
371
|
+
callback(null, result);
|
|
372
|
+
} catch (httpErr) {
|
|
373
|
+
utils.error("sendMessage", "All send paths failed for DM " + threadID, httpErr);
|
|
374
|
+
callback(httpErr);
|
|
331
375
|
}
|
|
332
376
|
} else {
|
|
333
|
-
|
|
377
|
+
// Groups: HTTP first, MQTT fallback
|
|
378
|
+
try {
|
|
379
|
+
const result = await sendContent(form, threadID, isSingleUser, messageAndOTID);
|
|
380
|
+
callback(null, result);
|
|
381
|
+
} catch (primaryErr) {
|
|
382
|
+
utils.warn("sendMessage", "HTTP send failed for group " + threadID + ", trying MQTT:", primaryErr && primaryErr.message ? primaryErr.message : primaryErr);
|
|
383
|
+
if (api.sendMessageMqtt) {
|
|
384
|
+
try {
|
|
385
|
+
const mqttRes = await api.sendMessageMqtt(msg, threadID, replyToMessage);
|
|
386
|
+
callback(null, mqttRes);
|
|
387
|
+
} catch (fallbackErr) {
|
|
388
|
+
utils.error("sendMessage", "Both HTTP and MQTT failed for group " + threadID, fallbackErr);
|
|
389
|
+
callback(primaryErr);
|
|
390
|
+
}
|
|
391
|
+
} else {
|
|
392
|
+
callback(primaryErr);
|
|
393
|
+
}
|
|
394
|
+
}
|
|
334
395
|
}
|
|
335
396
|
} finally {
|
|
336
397
|
if (typingTimeout) clearTimeout(typingTimeout);
|
package/src/utils/axios.js
CHANGED
|
@@ -8,6 +8,7 @@ const FormData = require("form-data");
|
|
|
8
8
|
const { getHeaders } = require("./headers");
|
|
9
9
|
const { getType } = require("./constants");
|
|
10
10
|
const { globalRateLimiter } = require("./rateLimiter");
|
|
11
|
+
const { globalAntiSuspension } = require("./antiSuspension");
|
|
11
12
|
|
|
12
13
|
const jar = new CookieJar();
|
|
13
14
|
const client = wrapper(axios.create({ jar }));
|
|
@@ -112,6 +113,8 @@ async function inspectResponseForSessionIssues(adapted, ctx) {
|
|
|
112
113
|
typeof body === 'object' && body !== null && body.error === 1357001;
|
|
113
114
|
|
|
114
115
|
if (isLoginBlocked) {
|
|
116
|
+
// Trip the circuit breaker to stop retries while the account is flagged.
|
|
117
|
+
globalAntiSuspension.tripCircuitBreaker('login_blocked (1357001)', 45 * 60 * 1000);
|
|
115
118
|
const err = new Error('Facebook blocked the login.');
|
|
116
119
|
err.error = 'login_blocked';
|
|
117
120
|
err.res = body;
|
package/src/utils/clients.js
CHANGED
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
const { makeParsable, log, warn } = require("./constants");
|
|
4
4
|
const { globalRateLimiter, configureRateLimiter } = require("./rateLimiter");
|
|
5
|
+
const { globalAntiSuspension } = require("./antiSuspension");
|
|
5
6
|
|
|
6
7
|
/**
|
|
7
8
|
* Formats a cookie array into a string for use in a cookie jar.
|
|
@@ -151,10 +152,17 @@ function parseAndCheckLogin(ctx, http, retryCount = 0) {
|
|
|
151
152
|
err.errorType = res.error === 1357004 ? "CHECKPOINT" : res.error === 1357031 ? "LOCKED" : "BLOCKED";
|
|
152
153
|
err.requiresReLogin = res.error === 1357004 || res.error === 1357031;
|
|
153
154
|
warn("Account Status", `${ACCOUNT_ERROR_CODES[res.error]} (Code: ${res.error})`);
|
|
155
|
+
// Trip the anti-suspension circuit breaker immediately for blocking errors.
|
|
156
|
+
// This prevents the bot from hammering Facebook while the account is flagged.
|
|
157
|
+
if (res.error === 1357001 || res.error === 1357033 || res.error === 2056003) {
|
|
158
|
+
const cooldownMs = res.error === 1357001 ? 45 * 60 * 1000 : 20 * 60 * 1000;
|
|
159
|
+
globalAntiSuspension.tripCircuitBreaker(ACCOUNT_ERROR_CODES[res.error], cooldownMs);
|
|
160
|
+
}
|
|
154
161
|
throw err;
|
|
155
162
|
}
|
|
156
163
|
|
|
157
164
|
if (res.error === 1357001 || (res.errorSummary && res.errorSummary.includes("blocked"))) {
|
|
165
|
+
globalAntiSuspension.tripCircuitBreaker("Facebook blocked the login (errorSummary)", 45 * 60 * 1000);
|
|
158
166
|
const err = new Error("Facebook blocked the login");
|
|
159
167
|
err.error = "Not logged in.";
|
|
160
168
|
err.errorType = "BLOCKED";
|
package/src/utils/formatters.js
CHANGED
|
@@ -1196,6 +1196,13 @@ function formatID(id) {
|
|
|
1196
1196
|
err.error = "Not logged in.";
|
|
1197
1197
|
err.requiresReLogin = true;
|
|
1198
1198
|
err.loginBlocked = res.error === 1357004;
|
|
1199
|
+
// Trip circuit breaker on blocking/locking to stop retries.
|
|
1200
|
+
if (res.error === 1357001) {
|
|
1201
|
+
try {
|
|
1202
|
+
const { globalAntiSuspension } = require('./antiSuspension');
|
|
1203
|
+
globalAntiSuspension.tripCircuitBreaker('login_blocked (1357001 via formatters)', 45 * 60 * 1000);
|
|
1204
|
+
} catch (_) {}
|
|
1205
|
+
}
|
|
1199
1206
|
throw err;
|
|
1200
1207
|
}
|
|
1201
1208
|
|