fca-phantom 1.0.0
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/LICENSE +58 -0
- package/README.md +534 -0
- package/index.js +35 -0
- package/package.json +101 -0
- package/phantom/core/builder/bootstrap.js +334 -0
- package/phantom/core/builder/config.js +78 -0
- package/phantom/core/builder/forge.js +113 -0
- package/phantom/core/builder/ignite.js +386 -0
- package/phantom/core/builder/options.js +61 -0
- package/phantom/core/engine.js +71 -0
- package/phantom/core/reactor.js +2 -0
- package/phantom/datastore/appState.js +2 -0
- package/phantom/datastore/appStateBackup.js +34 -0
- package/phantom/datastore/models/cipher/e2ee.js +48 -0
- package/phantom/datastore/models/cipher/vault.js +153 -0
- package/phantom/datastore/models/index.js +3 -0
- package/phantom/datastore/models/matrix/auth.js +151 -0
- package/phantom/datastore/models/matrix/cache.js +3 -0
- package/phantom/datastore/models/matrix/checker.js +2 -0
- package/phantom/datastore/models/matrix/clients.js +2 -0
- package/phantom/datastore/models/matrix/constants.js +2 -0
- package/phantom/datastore/models/matrix/credentials.js +2 -0
- package/phantom/datastore/models/matrix/cycle.js +2 -0
- package/phantom/datastore/models/matrix/gate.js +282 -0
- package/phantom/datastore/models/matrix/ghost.js +332 -0
- package/phantom/datastore/models/matrix/headers.js +193 -0
- package/phantom/datastore/models/matrix/heartbeat.js +298 -0
- package/phantom/datastore/models/matrix/identity.js +235 -0
- package/phantom/datastore/models/matrix/logger.js +271 -0
- package/phantom/datastore/models/matrix/monitor.js +2 -0
- package/phantom/datastore/models/matrix/net.js +316 -0
- package/phantom/datastore/models/matrix/response.js +193 -0
- package/phantom/datastore/models/matrix/revive.js +255 -0
- package/phantom/datastore/models/matrix/signals.js +2 -0
- package/phantom/datastore/models/matrix/store.js +263 -0
- package/phantom/datastore/models/matrix/telemetry.js +272 -0
- package/phantom/datastore/models/matrix/tools.js +93 -0
- package/phantom/datastore/models/matrix/transform/cookieParser.js +2 -0
- package/phantom/datastore/models/matrix/transform/cookies.js +114 -0
- package/phantom/datastore/models/matrix/transform/index.js +203 -0
- package/phantom/datastore/models/matrix/validator.js +157 -0
- package/phantom/datastore/models/types/index.d.ts +498 -0
- package/phantom/datastore/schema.js +167 -0
- package/phantom/datastore/session.js +129 -0
- package/phantom/datastore/threads.js +22 -0
- package/phantom/datastore/users.js +26 -0
- package/phantom/dispatch/addExternalModule.js +239 -0
- package/phantom/dispatch/addUserToGroup.js +161 -0
- package/phantom/dispatch/changeAdminStatus.js +142 -0
- package/phantom/dispatch/changeArchivedStatus.js +135 -0
- package/phantom/dispatch/changeAvatar.js +123 -0
- package/phantom/dispatch/changeBio.js +86 -0
- package/phantom/dispatch/changeBlockedStatus.js +86 -0
- package/phantom/dispatch/changeGroupImage.js +145 -0
- package/phantom/dispatch/changeThreadColor.js +172 -0
- package/phantom/dispatch/changeThreadEmoji.js +130 -0
- package/phantom/dispatch/comment.js +136 -0
- package/phantom/dispatch/createAITheme.js +333 -0
- package/phantom/dispatch/createNewGroup.js +99 -0
- package/phantom/dispatch/createPoll.js +148 -0
- package/phantom/dispatch/deleteMessage.js +131 -0
- package/phantom/dispatch/deleteThread.js +155 -0
- package/phantom/dispatch/e2ee.js +101 -0
- package/phantom/dispatch/editMessage.js +158 -0
- package/phantom/dispatch/emoji.js +143 -0
- package/phantom/dispatch/fetchThemeData.js +233 -0
- package/phantom/dispatch/follow.js +111 -0
- package/phantom/dispatch/forwardMessage.js +110 -0
- package/phantom/dispatch/friend.js +189 -0
- package/phantom/dispatch/gcmember.js +138 -0
- package/phantom/dispatch/gcname.js +131 -0
- package/phantom/dispatch/gcrule.js +111 -0
- package/phantom/dispatch/getAccess.js +109 -0
- package/phantom/dispatch/getBotInfo.js +81 -0
- package/phantom/dispatch/getBotInitialData.js +110 -0
- package/phantom/dispatch/getFriendsList.js +118 -0
- package/phantom/dispatch/getMessage.js +199 -0
- package/phantom/dispatch/getTheme.js +199 -0
- package/phantom/dispatch/getThemeInfo.js +160 -0
- package/phantom/dispatch/getThreadHistory.js +139 -0
- package/phantom/dispatch/getThreadInfo.js +153 -0
- package/phantom/dispatch/getThreadList.js +132 -0
- package/phantom/dispatch/getThreadPictures.js +93 -0
- package/phantom/dispatch/getUserID.js +147 -0
- package/phantom/dispatch/getUserInfo.js +513 -0
- package/phantom/dispatch/getUserInfoV2.js +146 -0
- package/phantom/dispatch/handleMessageRequest.js +50 -0
- package/phantom/dispatch/httpGet.js +63 -0
- package/phantom/dispatch/httpPost.js +89 -0
- package/phantom/dispatch/httpPostFormData.js +69 -0
- package/phantom/dispatch/listenMqtt.js +1236 -0
- package/phantom/dispatch/listenSpeed.js +179 -0
- package/phantom/dispatch/logout.js +93 -0
- package/phantom/dispatch/markAsDelivered.js +92 -0
- package/phantom/dispatch/markAsRead.js +119 -0
- package/phantom/dispatch/markAsReadAll.js +215 -0
- package/phantom/dispatch/markAsSeen.js +70 -0
- package/phantom/dispatch/mqttDeltaValue.js +278 -0
- package/phantom/dispatch/muteThread.js +253 -0
- package/phantom/dispatch/nickname.js +132 -0
- package/phantom/dispatch/notes.js +263 -0
- package/phantom/dispatch/pinMessage.js +238 -0
- package/phantom/dispatch/produceMetaTheme.js +335 -0
- package/phantom/dispatch/realtime.js +291 -0
- package/phantom/dispatch/removeUserFromGroup.js +248 -0
- package/phantom/dispatch/resolvePhotoUrl.js +217 -0
- package/phantom/dispatch/searchForThread.js +258 -0
- package/phantom/dispatch/sendMessage.js +354 -0
- package/phantom/dispatch/sendMessageMqtt.js +249 -0
- package/phantom/dispatch/sendTypingIndicator.js +206 -0
- package/phantom/dispatch/setMessageReaction.js +188 -0
- package/phantom/dispatch/setMessageReactionMqtt.js +248 -0
- package/phantom/dispatch/setThreadTheme.js +330 -0
- package/phantom/dispatch/setThreadThemeMqtt.js +207 -0
- package/phantom/dispatch/share.js +200 -0
- package/phantom/dispatch/shareContact.js +216 -0
- package/phantom/dispatch/stickers.js +395 -0
- package/phantom/dispatch/story.js +240 -0
- package/phantom/dispatch/theme.js +296 -0
- package/phantom/dispatch/unfriend.js +199 -0
- package/phantom/dispatch/unsendMessage.js +124 -0
|
@@ -0,0 +1,1236 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
3
|
+
const mqtt = require('mqtt');
|
|
4
|
+
const HttpsProxyAgent = require('https-proxy-agent');
|
|
5
|
+
const EventEmitter = require('events');
|
|
6
|
+
const { parseDelta } = require('./mqttDeltaValue');
|
|
7
|
+
const { globalReviveManager } = require('../datastore/models/matrix/revive');
|
|
8
|
+
|
|
9
|
+
// NOTE: form and getSeqID are intentionally kept local to each factory
|
|
10
|
+
// invocation (see module.exports below). Do NOT hoist them to module scope —
|
|
11
|
+
// doing so causes cross-context contamination when the factory is called
|
|
12
|
+
// multiple times (e.g. after auto re-login).
|
|
13
|
+
|
|
14
|
+
const topics = [
|
|
15
|
+
"/ls_req", "/ls_resp", "/legacy_web", "/webrtc", "/rtc_multi", "/onevc", "/br_sr", "/sr_res",
|
|
16
|
+
"/t_ms", "/thread_typing", "/orca_typing_notifications", "/notify_disconnect",
|
|
17
|
+
"/orca_presence", "/inbox", "/mercury", "/messaging_events",
|
|
18
|
+
"/orca_message_notifications", "/pp", "/webrtc_response"
|
|
19
|
+
];
|
|
20
|
+
|
|
21
|
+
// Optimized constants for better performance
|
|
22
|
+
const MQTT_MAX_BACKOFF = 15000;
|
|
23
|
+
const MQTT_JITTER_MAX = 600;
|
|
24
|
+
const MQTT_QUICK_CLOSE_WINDOW_MS = 1200;
|
|
25
|
+
const MQTT_QUICK_CLOSE_THRESHOLD = 3;
|
|
26
|
+
const DEFAULT_RECONNECT_DELAY_MS = 1500;
|
|
27
|
+
const T_MS_WAIT_TIMEOUT_MS = 6000;
|
|
28
|
+
|
|
29
|
+
function getRandomReconnectTime() {
|
|
30
|
+
const min = 15 * 60 * 1000;
|
|
31
|
+
const max = 30 * 60 * 1000;
|
|
32
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
function generateUUID() {
|
|
36
|
+
return 'xxxxxxxx-xxxx-4xxx-yxxx-xxxxxxxxxxxx'.replace(/[xy]/g, function(c) {
|
|
37
|
+
var r = Math.random() * 16 | 0, v = c == 'x' ? r : (r & 0x3 | 0x8);
|
|
38
|
+
return v.toString(16);
|
|
39
|
+
});
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
// Enhanced retry mechanism with exponential backoff
|
|
43
|
+
function retryWithBackoff(fn, maxRetries = 3, baseDelay = 1000) {
|
|
44
|
+
return async function(...args) {
|
|
45
|
+
let lastError;
|
|
46
|
+
for (let i = 0; i < maxRetries; i++) {
|
|
47
|
+
try {
|
|
48
|
+
return await fn.apply(this, args);
|
|
49
|
+
} catch (error) {
|
|
50
|
+
lastError = error;
|
|
51
|
+
if (i === maxRetries - 1) throw lastError;
|
|
52
|
+
|
|
53
|
+
const delay = Math.min(baseDelay * Math.pow(2, i) + Math.random() * 500, 10000);
|
|
54
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
throw lastError;
|
|
58
|
+
};
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
function calculate(previousTimestamp, currentTimestamp){
|
|
62
|
+
return Math.floor(previousTimestamp + (currentTimestamp - previousTimestamp) + 250);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
function computeBackoffDelay(ctx, baseDelay, maxBackoff, jitterMax) {
|
|
66
|
+
const attempt = ctx._reconnectAttempts || 0;
|
|
67
|
+
const base = Number.isFinite(baseDelay) && baseDelay > 0 ? baseDelay : DEFAULT_RECONNECT_DELAY_MS;
|
|
68
|
+
const max = Number.isFinite(maxBackoff) && maxBackoff > 0 ? maxBackoff : MQTT_MAX_BACKOFF;
|
|
69
|
+
const jitterCap = Number.isFinite(jitterMax) && jitterMax >= 0 ? jitterMax : MQTT_JITTER_MAX;
|
|
70
|
+
const backoff = Math.min(base * Math.pow(1.5, attempt), max); // Reduced exponent for faster recovery
|
|
71
|
+
const jitter = Math.floor(Math.random() * jitterCap);
|
|
72
|
+
return Math.round(backoff + jitter);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
/**
|
|
76
|
+
* @param {Object} ctx
|
|
77
|
+
* @param {Object} api
|
|
78
|
+
* @param {string} threadID
|
|
79
|
+
*/
|
|
80
|
+
function markAsRead(ctx, api, threadID) {
|
|
81
|
+
if (ctx.globalOptions.autoMarkRead && threadID) {
|
|
82
|
+
api.markAsRead(threadID, (err) => {
|
|
83
|
+
if (err) utils.error("autoMarkRead", err);
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
/**
|
|
89
|
+
* @param {Object} defaultFuncs
|
|
90
|
+
* @param {Object} api
|
|
91
|
+
* @param {Object} ctx
|
|
92
|
+
* @param {Function} globalCallback
|
|
93
|
+
* @param {Function} scheduleReconnect
|
|
94
|
+
* @param {Function} emitAuthError - Passed from the factory closure so auth errors can be emitted correctly
|
|
95
|
+
*/
|
|
96
|
+
async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconnect, emitAuthError) {
|
|
97
|
+
function isEndingLikeError(msg) {
|
|
98
|
+
return /No subscription existed|client disconnecting|socket hang up|ECONNRESET/i.test(msg || "");
|
|
99
|
+
}
|
|
100
|
+
function guard(label, fn) {
|
|
101
|
+
return (...args) => {
|
|
102
|
+
try {
|
|
103
|
+
return fn(...args);
|
|
104
|
+
} catch (err) {
|
|
105
|
+
utils.error("MQTT", `${label} handler error:`, err && err.message ? err.message : err);
|
|
106
|
+
}
|
|
107
|
+
};
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
if (ctx._reconnectTimer) {
|
|
111
|
+
clearTimeout(ctx._reconnectTimer);
|
|
112
|
+
ctx._reconnectTimer = null;
|
|
113
|
+
}
|
|
114
|
+
if (ctx._tmsTimeout) {
|
|
115
|
+
clearTimeout(ctx._tmsTimeout);
|
|
116
|
+
ctx._tmsTimeout = null;
|
|
117
|
+
}
|
|
118
|
+
if (ctx._mqttWatchdog) {
|
|
119
|
+
clearInterval(ctx._mqttWatchdog);
|
|
120
|
+
ctx._mqttWatchdog = null;
|
|
121
|
+
}
|
|
122
|
+
if (ctx.mqttClient) {
|
|
123
|
+
try { ctx.mqttClient.removeAllListeners(); } catch (_) { }
|
|
124
|
+
try { ctx.mqttClient.end(true); } catch (_) { }
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const chatOn = ctx.globalOptions.online;
|
|
128
|
+
const region = ctx.region;
|
|
129
|
+
const foreground = false;
|
|
130
|
+
const sessionID = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) + 1;
|
|
131
|
+
const cid = ctx.clientID;
|
|
132
|
+
const cachedUA = ctx.globalOptions.cachedUserAgent || ctx.globalOptions.userAgent;
|
|
133
|
+
const username = {
|
|
134
|
+
u: ctx.userID,
|
|
135
|
+
s: sessionID,
|
|
136
|
+
chat_on: chatOn,
|
|
137
|
+
fg: false,
|
|
138
|
+
d: cid,
|
|
139
|
+
ct: 'websocket',
|
|
140
|
+
aid: 219994525426954,
|
|
141
|
+
aids: null,
|
|
142
|
+
mqtt_sid: '',
|
|
143
|
+
cp: 3,
|
|
144
|
+
ecp: 10,
|
|
145
|
+
st: [],
|
|
146
|
+
pm: [],
|
|
147
|
+
dc: '',
|
|
148
|
+
no_auto_fg: true,
|
|
149
|
+
gas: null,
|
|
150
|
+
pack: [],
|
|
151
|
+
p: null,
|
|
152
|
+
php_override: ""
|
|
153
|
+
};
|
|
154
|
+
const cookies = ctx.jar.getCookiesSync('https://www.facebook.com').join('; ');
|
|
155
|
+
let host;
|
|
156
|
+
if (ctx.mqttEndpoint) {
|
|
157
|
+
host = `${ctx.mqttEndpoint}&sid=${sessionID}&cid=${cid}`;
|
|
158
|
+
} else if (region) {
|
|
159
|
+
host = `wss://edge-chat.facebook.com/chat?region=${region.toLowerCase()}&sid=${sessionID}&cid=${cid}`;
|
|
160
|
+
} else {
|
|
161
|
+
host = `wss://edge-chat.facebook.com/chat?sid=${sessionID}&cid=${cid}`;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
utils.log("Connecting to MQTT...", host);
|
|
165
|
+
|
|
166
|
+
const cachedSecChUa = ctx.globalOptions.cachedSecChUa || '"Google Chrome";v="136", "Not;A=Brand";v="99", "Chromium";v="136"';
|
|
167
|
+
const cachedSecChUaPlatform = ctx.globalOptions.cachedSecChUaPlatform || '"Windows"';
|
|
168
|
+
const cachedLocale = ctx.globalOptions.cachedLocale || 'en-US,en;q=0.9';
|
|
169
|
+
|
|
170
|
+
// Generate a unique client ID per session like a real browser would
|
|
171
|
+
const mqttClientId = 'mqttwsclient_' + Math.random().toString(36).slice(2, 10) + Date.now().toString(36);
|
|
172
|
+
const options = {
|
|
173
|
+
clientId: mqttClientId,
|
|
174
|
+
protocolId: "MQIsdp",
|
|
175
|
+
protocolVersion: 3,
|
|
176
|
+
username: JSON.stringify(username),
|
|
177
|
+
clean: true,
|
|
178
|
+
wsOptions: {
|
|
179
|
+
headers: {
|
|
180
|
+
Cookie: cookies,
|
|
181
|
+
Origin: "https://www.facebook.com",
|
|
182
|
+
"User-Agent": ctx.globalOptions.userAgent || "Mozilla/5.0",
|
|
183
|
+
Referer: "https://www.facebook.com/",
|
|
184
|
+
Host: "edge-chat.facebook.com",
|
|
185
|
+
Connection: "Upgrade",
|
|
186
|
+
Pragma: "no-cache",
|
|
187
|
+
"Cache-Control": "no-cache",
|
|
188
|
+
Upgrade: "websocket",
|
|
189
|
+
"Sec-WebSocket-Version": "13",
|
|
190
|
+
"Accept-Encoding": "gzip, deflate, br",
|
|
191
|
+
"Accept-Language": cachedLocale,
|
|
192
|
+
'Sec-Ch-Ua': cachedSecChUa,
|
|
193
|
+
'Sec-Ch-Ua-Mobile': '?0',
|
|
194
|
+
'Sec-Ch-Ua-Platform': cachedSecChUaPlatform,
|
|
195
|
+
'Sec-WebSocket-Extensions': 'permessage-deflate; client_max_window_bits'
|
|
196
|
+
},
|
|
197
|
+
origin: 'https://www.facebook.com',
|
|
198
|
+
protocolVersion: 13,
|
|
199
|
+
binaryType: 'arraybuffer'
|
|
200
|
+
},
|
|
201
|
+
keepalive: 60,
|
|
202
|
+
reschedulePings: true,
|
|
203
|
+
connectTimeout: 30000,
|
|
204
|
+
reconnectPeriod: 0
|
|
205
|
+
};
|
|
206
|
+
|
|
207
|
+
if (ctx.globalOptions.proxy) options.wsOptions.agent = new HttpsProxyAgent(ctx.globalOptions.proxy);
|
|
208
|
+
ctx._mqttLastConnectAttemptAt = Date.now();
|
|
209
|
+
|
|
210
|
+
// Create WebSocket stream - using exact fca-unofficial implementation
|
|
211
|
+
let mqttClient;
|
|
212
|
+
|
|
213
|
+
try {
|
|
214
|
+
const mqtt = require('mqtt');
|
|
215
|
+
const WebSocket = require('ws');
|
|
216
|
+
const { PassThrough, Writable } = require('stream');
|
|
217
|
+
const Duplexify = require('duplexify');
|
|
218
|
+
|
|
219
|
+
// Exact buildProxy from fca-unofficial
|
|
220
|
+
function buildProxy() {
|
|
221
|
+
let target = null;
|
|
222
|
+
let ended = false;
|
|
223
|
+
const Proxy = new Writable({
|
|
224
|
+
autoDestroy: true,
|
|
225
|
+
write(chunk, enc, cb) {
|
|
226
|
+
if (ended || this.destroyed) return cb();
|
|
227
|
+
const ws = target;
|
|
228
|
+
if (ws && ws.readyState === 1) {
|
|
229
|
+
try {
|
|
230
|
+
ws.send(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk), cb);
|
|
231
|
+
} catch (e) {
|
|
232
|
+
cb(e);
|
|
233
|
+
}
|
|
234
|
+
} else cb();
|
|
235
|
+
},
|
|
236
|
+
writev(chunks, cb) {
|
|
237
|
+
if (ended || this.destroyed) return cb();
|
|
238
|
+
const ws = target;
|
|
239
|
+
if (!ws || ws.readyState !== 1) return cb();
|
|
240
|
+
try {
|
|
241
|
+
for (const { chunk } of chunks) {
|
|
242
|
+
ws.send(Buffer.isBuffer(chunk) ? chunk : Buffer.from(chunk));
|
|
243
|
+
}
|
|
244
|
+
cb();
|
|
245
|
+
} catch (e) {
|
|
246
|
+
cb(e);
|
|
247
|
+
}
|
|
248
|
+
},
|
|
249
|
+
final(cb) {
|
|
250
|
+
ended = true;
|
|
251
|
+
const ws = target;
|
|
252
|
+
target = null;
|
|
253
|
+
if (ws && (ws.readyState === 0 || ws.readyState === 1)) {
|
|
254
|
+
try {
|
|
255
|
+
typeof ws.terminate === "function" ? ws.terminate() : ws.close();
|
|
256
|
+
} catch { }
|
|
257
|
+
}
|
|
258
|
+
cb();
|
|
259
|
+
}
|
|
260
|
+
});
|
|
261
|
+
Proxy.setTarget = ws => {
|
|
262
|
+
if (ended) return;
|
|
263
|
+
target = ws;
|
|
264
|
+
};
|
|
265
|
+
Proxy.hardEnd = () => {
|
|
266
|
+
ended = true;
|
|
267
|
+
target = null;
|
|
268
|
+
};
|
|
269
|
+
return Proxy;
|
|
270
|
+
}
|
|
271
|
+
|
|
272
|
+
// Exact buildStream from fca-unofficial
|
|
273
|
+
function buildStream(opts, ws, Proxy) {
|
|
274
|
+
const readable = new PassThrough();
|
|
275
|
+
const Stream = Duplexify(undefined, undefined, Object.assign({ end: false, autoDestroy: true }, opts));
|
|
276
|
+
const NoopWritable = new Writable({ write(_c, _e, cb) { cb(); } });
|
|
277
|
+
let pingTimer = null;
|
|
278
|
+
let livenessTimer = null;
|
|
279
|
+
let lastActivity = Date.now();
|
|
280
|
+
let attached = false;
|
|
281
|
+
let style = "prop";
|
|
282
|
+
let closed = false;
|
|
283
|
+
|
|
284
|
+
const toBuffer = d => {
|
|
285
|
+
if (Buffer.isBuffer(d)) return d;
|
|
286
|
+
if (d instanceof ArrayBuffer) return Buffer.from(d);
|
|
287
|
+
if (ArrayBuffer.isView(d)) return Buffer.from(d.buffer, d.byteOffset, d.byteLength);
|
|
288
|
+
return Buffer.from(String(d));
|
|
289
|
+
};
|
|
290
|
+
|
|
291
|
+
const swapToNoopWritable = () => {
|
|
292
|
+
try { Stream.setWritable(NoopWritable); } catch { }
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
const onOpen = () => {
|
|
296
|
+
if (closed) return;
|
|
297
|
+
Proxy.setTarget(ws);
|
|
298
|
+
Stream.setWritable(Proxy);
|
|
299
|
+
Stream.setReadable(readable);
|
|
300
|
+
Stream.emit("connect");
|
|
301
|
+
lastActivity = Date.now();
|
|
302
|
+
clearInterval(pingTimer);
|
|
303
|
+
clearInterval(livenessTimer);
|
|
304
|
+
pingTimer = setInterval(() => {
|
|
305
|
+
if (!ws || ws.readyState !== 1) return;
|
|
306
|
+
if (typeof ws.ping === "function") {
|
|
307
|
+
try { ws.ping(); } catch { }
|
|
308
|
+
} else {
|
|
309
|
+
try { ws.send("ping"); } catch { }
|
|
310
|
+
}
|
|
311
|
+
}, 30000);
|
|
312
|
+
livenessTimer = setInterval(() => {
|
|
313
|
+
if (!ws || ws.readyState !== 1) return;
|
|
314
|
+
if (Date.now() - lastActivity > 65000) {
|
|
315
|
+
try { typeof ws.terminate === "function" ? ws.terminate() : ws.close(); } catch { }
|
|
316
|
+
}
|
|
317
|
+
}, 10000);
|
|
318
|
+
};
|
|
319
|
+
|
|
320
|
+
const onMessage = data => {
|
|
321
|
+
lastActivity = Date.now();
|
|
322
|
+
readable.write(toBuffer(style === "dom" && data && data.data !== undefined ? data.data : data));
|
|
323
|
+
};
|
|
324
|
+
|
|
325
|
+
const onPong = () => {
|
|
326
|
+
lastActivity = Date.now();
|
|
327
|
+
};
|
|
328
|
+
|
|
329
|
+
const cleanup = () => {
|
|
330
|
+
if (closed) return;
|
|
331
|
+
closed = true;
|
|
332
|
+
clearInterval(pingTimer);
|
|
333
|
+
clearInterval(livenessTimer);
|
|
334
|
+
pingTimer = null;
|
|
335
|
+
livenessTimer = null;
|
|
336
|
+
Proxy.hardEnd();
|
|
337
|
+
swapToNoopWritable();
|
|
338
|
+
if (ws) {
|
|
339
|
+
detach(ws);
|
|
340
|
+
try {
|
|
341
|
+
if (ws.readyState === 1) {
|
|
342
|
+
typeof ws.terminate === "function" ? ws.terminate() : ws.close();
|
|
343
|
+
}
|
|
344
|
+
} catch { }
|
|
345
|
+
ws = null;
|
|
346
|
+
}
|
|
347
|
+
readable.end();
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
const onError = err => {
|
|
351
|
+
cleanup();
|
|
352
|
+
Stream.destroy(err);
|
|
353
|
+
};
|
|
354
|
+
|
|
355
|
+
const onClose = () => {
|
|
356
|
+
cleanup();
|
|
357
|
+
Stream.end();
|
|
358
|
+
if (!Stream.destroyed) Stream.destroy();
|
|
359
|
+
};
|
|
360
|
+
|
|
361
|
+
const attach = w => {
|
|
362
|
+
if (attached || !w) return;
|
|
363
|
+
attached = true;
|
|
364
|
+
if (typeof w.on === "function" && typeof w.off === "function") {
|
|
365
|
+
style = "node";
|
|
366
|
+
w.on("open", onOpen);
|
|
367
|
+
w.on("message", onMessage);
|
|
368
|
+
w.on("error", onError);
|
|
369
|
+
w.on("close", onClose);
|
|
370
|
+
if (typeof w.on === "function") w.on("pong", onPong);
|
|
371
|
+
} else if (typeof w.addEventListener === "function" && typeof w.removeEventListener === "function") {
|
|
372
|
+
style = "dom";
|
|
373
|
+
w.addEventListener("open", onOpen);
|
|
374
|
+
w.addEventListener("message", onMessage);
|
|
375
|
+
w.addEventListener("error", onError);
|
|
376
|
+
w.addEventListener("close", onClose);
|
|
377
|
+
} else {
|
|
378
|
+
style = "prop";
|
|
379
|
+
w.onopen = onOpen;
|
|
380
|
+
w.onmessage = onMessage;
|
|
381
|
+
w.onerror = onError;
|
|
382
|
+
w.onclose = onClose;
|
|
383
|
+
}
|
|
384
|
+
};
|
|
385
|
+
|
|
386
|
+
const detach = w => {
|
|
387
|
+
if (!attached || !w) return;
|
|
388
|
+
attached = false;
|
|
389
|
+
if (style === "node" && typeof w.off === "function") {
|
|
390
|
+
w.off("open", onOpen);
|
|
391
|
+
w.off("message", onMessage);
|
|
392
|
+
w.off("error", onError);
|
|
393
|
+
w.off("close", onClose);
|
|
394
|
+
if (typeof w.off === "function") w.off("pong", onPong);
|
|
395
|
+
} else if (style === "dom" && typeof w.removeEventListener === "function") {
|
|
396
|
+
w.removeEventListener("open", onOpen);
|
|
397
|
+
w.removeEventListener("message", onMessage);
|
|
398
|
+
w.removeEventListener("error", onError);
|
|
399
|
+
w.removeEventListener("close", onClose);
|
|
400
|
+
} else {
|
|
401
|
+
w.onopen = null;
|
|
402
|
+
w.onmessage = null;
|
|
403
|
+
w.onerror = null;
|
|
404
|
+
w.onclose = null;
|
|
405
|
+
}
|
|
406
|
+
};
|
|
407
|
+
|
|
408
|
+
attach(ws);
|
|
409
|
+
if (ws && ws.readyState === 1) onOpen();
|
|
410
|
+
|
|
411
|
+
Stream.on("prefinish", swapToNoopWritable);
|
|
412
|
+
Stream.on("finish", cleanup);
|
|
413
|
+
Stream.on("close", cleanup);
|
|
414
|
+
Proxy.on("close", swapToNoopWritable);
|
|
415
|
+
|
|
416
|
+
return Stream;
|
|
417
|
+
}
|
|
418
|
+
|
|
419
|
+
// Create MQTT client exactly like fca-unofficial
|
|
420
|
+
mqttClient = new mqtt.Client(
|
|
421
|
+
() => buildStream(options, new WebSocket(host, options.wsOptions), buildProxy()),
|
|
422
|
+
options
|
|
423
|
+
);
|
|
424
|
+
|
|
425
|
+
mqttClient.publishSync = mqttClient.publish.bind(mqttClient);
|
|
426
|
+
mqttClient.publish = (topic, message, opts = {}, callback = () => {}) => new Promise((resolve, reject) => {
|
|
427
|
+
mqttClient.publishSync(topic, message, opts, (err, data) => {
|
|
428
|
+
if (err) {
|
|
429
|
+
callback(err);
|
|
430
|
+
reject(err);
|
|
431
|
+
} else {
|
|
432
|
+
callback(null, data);
|
|
433
|
+
resolve(data);
|
|
434
|
+
}
|
|
435
|
+
});
|
|
436
|
+
});
|
|
437
|
+
ctx.mqttClient = mqttClient;
|
|
438
|
+
|
|
439
|
+
} catch (error) {
|
|
440
|
+
utils.error("MQTT", "Failed to create WebSocket connection:", error.message);
|
|
441
|
+
if (ctx.globalOptions.autoReconnect) {
|
|
442
|
+
const baseDelay = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000;
|
|
443
|
+
scheduleReconnect(baseDelay);
|
|
444
|
+
} else {
|
|
445
|
+
globalCallback({ type: "stop_listen", error: error.message || "WebSocket connection failed" });
|
|
446
|
+
}
|
|
447
|
+
return;
|
|
448
|
+
}
|
|
449
|
+
|
|
450
|
+
mqttClient.on('error', guard("error", (err) => {
|
|
451
|
+
const msg = String(err && err.message ? err.message : err || "");
|
|
452
|
+
|
|
453
|
+
if ((ctx._ending || ctx._cycling) && isEndingLikeError(msg)) {
|
|
454
|
+
utils.log("MQTT", "Expected error during shutdown: " + msg);
|
|
455
|
+
return;
|
|
456
|
+
}
|
|
457
|
+
|
|
458
|
+
if (ctx._tmsTimeout) {
|
|
459
|
+
clearTimeout(ctx._tmsTimeout);
|
|
460
|
+
ctx._tmsTimeout = null;
|
|
461
|
+
}
|
|
462
|
+
if (ctx._mqttWatchdog) {
|
|
463
|
+
clearInterval(ctx._mqttWatchdog);
|
|
464
|
+
ctx._mqttWatchdog = null;
|
|
465
|
+
}
|
|
466
|
+
ctx._mqttConnected = false;
|
|
467
|
+
|
|
468
|
+
if (/Not logged in|Not logged in\.|blocked the login|checkpoint|401|403/i.test(msg)) {
|
|
469
|
+
try { mqttClient.end(true); } catch (_) { }
|
|
470
|
+
try { if (ctx._autoCycleTimer) clearInterval(ctx._autoCycleTimer); } catch (_) { }
|
|
471
|
+
emitAuthError(/blocked|checkpoint/i.test(msg) ? "login_blocked" : "not_logged_in", msg);
|
|
472
|
+
return;
|
|
473
|
+
}
|
|
474
|
+
|
|
475
|
+
utils.error("MQTT error:", msg);
|
|
476
|
+
try { mqttClient.end(true); } catch (_) { }
|
|
477
|
+
|
|
478
|
+
if (ctx._ending || ctx._cycling) return;
|
|
479
|
+
|
|
480
|
+
if (ctx.globalOptions.autoReconnect) {
|
|
481
|
+
const baseDelay = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000;
|
|
482
|
+
ctx._reconnectAttempts = (ctx._reconnectAttempts || 0) + 1;
|
|
483
|
+
const d = computeBackoffDelay(ctx, baseDelay, MQTT_MAX_BACKOFF, MQTT_JITTER_MAX);
|
|
484
|
+
utils.warn("MQTT", `Auto-reconnecting in ${d}ms (attempt ${ctx._reconnectAttempts}) due to error`);
|
|
485
|
+
scheduleReconnect(d);
|
|
486
|
+
} else {
|
|
487
|
+
globalCallback({ type: "stop_listen", error: msg || "Connection refused" });
|
|
488
|
+
}
|
|
489
|
+
}));
|
|
490
|
+
|
|
491
|
+
// Update activity timestamp on every packet received (including PINGRESP).
|
|
492
|
+
// Without this, the watchdog fires on quiet bots even when the connection is
|
|
493
|
+
// healthy — MQTT keepalive pings don't emit the 'message' event.
|
|
494
|
+
mqttClient.on('packetreceive', () => {
|
|
495
|
+
ctx._lastMqttMessageAt = Date.now();
|
|
496
|
+
});
|
|
497
|
+
|
|
498
|
+
mqttClient.on('connect', guard("connect", async () => {
|
|
499
|
+
if (!ctx._mqttConnected) {
|
|
500
|
+
utils.log("MQTT connected successfully");
|
|
501
|
+
ctx._mqttConnected = true;
|
|
502
|
+
}
|
|
503
|
+
ctx._cycling = false;
|
|
504
|
+
ctx._reconnectAttempts = 0;
|
|
505
|
+
ctx._mqttQuickCloseCount = 0;
|
|
506
|
+
if (ctx._reconnectTimer) {
|
|
507
|
+
clearTimeout(ctx._reconnectTimer);
|
|
508
|
+
ctx._reconnectTimer = null;
|
|
509
|
+
}
|
|
510
|
+
ctx.loggedIn = true;
|
|
511
|
+
ctx._lastMqttMessageAt = Date.now();
|
|
512
|
+
if (ctx._mqttWatchdog) {
|
|
513
|
+
clearInterval(ctx._mqttWatchdog);
|
|
514
|
+
ctx._mqttWatchdog = null;
|
|
515
|
+
}
|
|
516
|
+
const watchdogInterval = (ctx._mqttOpt && ctx._mqttOpt.watchdogIntervalMs) || 30000;
|
|
517
|
+
const staleMs = (ctx._mqttOpt && ctx._mqttOpt.staleMs) || 180000;
|
|
518
|
+
ctx._mqttWatchdog = setInterval(() => {
|
|
519
|
+
if (ctx._ending || ctx._cycling || !ctx.globalOptions.autoReconnect) return;
|
|
520
|
+
const last = ctx._lastMqttMessageAt || 0;
|
|
521
|
+
if (last && Date.now() - last > staleMs) {
|
|
522
|
+
utils.warn("MQTT", `No MQTT activity for ${Date.now() - last}ms, cycling connection`);
|
|
523
|
+
try { mqttClient.end(true); } catch (_) { }
|
|
524
|
+
scheduleReconnect((ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000);
|
|
525
|
+
}
|
|
526
|
+
}, watchdogInterval);
|
|
527
|
+
|
|
528
|
+
mqttClient.subscribe(topics, { qos: 1 });
|
|
529
|
+
|
|
530
|
+
// Send queue setup messages immediately (like fca-unofficial)
|
|
531
|
+
const queue = {
|
|
532
|
+
sync_api_version: 11,
|
|
533
|
+
max_deltas_able_to_process: 200,
|
|
534
|
+
delta_batch_size: 200,
|
|
535
|
+
encoding: "JSON",
|
|
536
|
+
entity_fbid: ctx.userID,
|
|
537
|
+
initial_titan_sequence_id: ctx.lastSeqId,
|
|
538
|
+
device_params: null
|
|
539
|
+
};
|
|
540
|
+
const topic = ctx.syncToken ? "/messenger_sync_get_diffs" : "/messenger_sync_create_queue";
|
|
541
|
+
if (ctx.syncToken) {
|
|
542
|
+
queue.last_seq_id = ctx.lastSeqId;
|
|
543
|
+
queue.sync_token = ctx.syncToken;
|
|
544
|
+
}
|
|
545
|
+
mqttClient.publish(topic, JSON.stringify(queue), { qos: 1, retain: false });
|
|
546
|
+
mqttClient.publish("/foreground_state", JSON.stringify({ foreground: chatOn }), { qos: 1 });
|
|
547
|
+
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");
|
|
550
|
+
|
|
551
|
+
// Disable T_MS timeout to prevent connection cycling
|
|
552
|
+
if (ctx._tmsTimeout) {
|
|
553
|
+
clearTimeout(ctx._tmsTimeout);
|
|
554
|
+
ctx._tmsTimeout = null;
|
|
555
|
+
}
|
|
556
|
+
|
|
557
|
+
ctx.tmsWait = function() {
|
|
558
|
+
if (ctx._tmsTimeout) {
|
|
559
|
+
clearTimeout(ctx._tmsTimeout);
|
|
560
|
+
ctx._tmsTimeout = null;
|
|
561
|
+
}
|
|
562
|
+
if (ctx.globalOptions.emitReady) {
|
|
563
|
+
globalCallback(null, { type: "ready", timestamp: Date.now() });
|
|
564
|
+
}
|
|
565
|
+
delete ctx.tmsWait;
|
|
566
|
+
};
|
|
567
|
+
|
|
568
|
+
// Immediately mark as ready since we're connected
|
|
569
|
+
if (ctx.tmsWait && typeof ctx.tmsWait === "function") ctx.tmsWait();
|
|
570
|
+
}));
|
|
571
|
+
|
|
572
|
+
mqttClient.on('message', guard("message", async (topic, message, _packet) => {
|
|
573
|
+
try {
|
|
574
|
+
ctx._lastMqttMessageAt = Date.now();
|
|
575
|
+
let jsonMessage = Buffer.isBuffer(message) ? Buffer.from(message).toString() : message;
|
|
576
|
+
try { jsonMessage = JSON.parse(jsonMessage); } catch (_) { jsonMessage = {}; }
|
|
577
|
+
|
|
578
|
+
if (jsonMessage.type === "jewel_requests_add") {
|
|
579
|
+
globalCallback(null, {
|
|
580
|
+
type: "friend_request_received",
|
|
581
|
+
actorFbId: jsonMessage.from.toString(),
|
|
582
|
+
timestamp: Date.now().toString()
|
|
583
|
+
});
|
|
584
|
+
} else if (jsonMessage.type === "jewel_requests_remove_old") {
|
|
585
|
+
globalCallback(null, {
|
|
586
|
+
type: "friend_request_cancel",
|
|
587
|
+
actorFbId: jsonMessage.from.toString(),
|
|
588
|
+
timestamp: Date.now().toString()
|
|
589
|
+
});
|
|
590
|
+
} else if (topic === "/t_ms") {
|
|
591
|
+
if (ctx.tmsWait && typeof ctx.tmsWait === "function") ctx.tmsWait();
|
|
592
|
+
|
|
593
|
+
if (jsonMessage.firstDeltaSeqId && jsonMessage.syncToken) {
|
|
594
|
+
ctx.lastSeqId = jsonMessage.firstDeltaSeqId;
|
|
595
|
+
ctx.syncToken = jsonMessage.syncToken;
|
|
596
|
+
}
|
|
597
|
+
if (jsonMessage.lastIssuedSeqId) {
|
|
598
|
+
ctx.lastSeqId = parseInt(jsonMessage.lastIssuedSeqId);
|
|
599
|
+
}
|
|
600
|
+
|
|
601
|
+
if (jsonMessage.deltas) {
|
|
602
|
+
for (const delta of jsonMessage.deltas) {
|
|
603
|
+
parseDelta(defaultFuncs, api, ctx, globalCallback, { delta });
|
|
604
|
+
}
|
|
605
|
+
}
|
|
606
|
+
} else if (topic === "/thread_typing" || topic === "/orca_typing_notifications") {
|
|
607
|
+
const typ = {
|
|
608
|
+
type: "typ",
|
|
609
|
+
isTyping: !!jsonMessage.state,
|
|
610
|
+
from: jsonMessage.sender_fbid.toString(),
|
|
611
|
+
threadID: utils.formatID((jsonMessage.thread || jsonMessage.sender_fbid).toString())
|
|
612
|
+
};
|
|
613
|
+
globalCallback(null, typ);
|
|
614
|
+
} else if (topic === "/orca_presence") {
|
|
615
|
+
if (!ctx.globalOptions.updatePresence && jsonMessage.list) {
|
|
616
|
+
for (const data of jsonMessage.list) {
|
|
617
|
+
globalCallback(null, {
|
|
618
|
+
type: "presence",
|
|
619
|
+
userID: String(data.u),
|
|
620
|
+
timestamp: data.l * 1000,
|
|
621
|
+
statuses: data.p
|
|
622
|
+
});
|
|
623
|
+
}
|
|
624
|
+
}
|
|
625
|
+
}
|
|
626
|
+
} catch (ex) {
|
|
627
|
+
utils.error("MQTT message parse error:", ex && ex.message ? ex.message : ex);
|
|
628
|
+
}
|
|
629
|
+
}));
|
|
630
|
+
|
|
631
|
+
mqttClient.on('close', guard("close", async () => {
|
|
632
|
+
utils.warn("MQTT", "Connection closed");
|
|
633
|
+
if (ctx._tmsTimeout) {
|
|
634
|
+
clearTimeout(ctx._tmsTimeout);
|
|
635
|
+
ctx._tmsTimeout = null;
|
|
636
|
+
}
|
|
637
|
+
if (ctx._mqttWatchdog) {
|
|
638
|
+
clearInterval(ctx._mqttWatchdog);
|
|
639
|
+
ctx._mqttWatchdog = null;
|
|
640
|
+
}
|
|
641
|
+
// Save connected state BEFORE clearing it — used for quick-close detection.
|
|
642
|
+
const wasConnected = ctx._mqttConnected;
|
|
643
|
+
ctx._mqttConnected = false;
|
|
644
|
+
if (ctx._ending || ctx._cycling) return;
|
|
645
|
+
|
|
646
|
+
// Quick-close detection: only relevant when we closed before a 'connect'
|
|
647
|
+
// event ever fired (wasConnected is still false from initialization).
|
|
648
|
+
if (!wasConnected) {
|
|
649
|
+
const now = Date.now();
|
|
650
|
+
const lastAttempt = ctx._mqttLastConnectAttemptAt || 0;
|
|
651
|
+
if (lastAttempt && now - lastAttempt <= MQTT_QUICK_CLOSE_WINDOW_MS) {
|
|
652
|
+
ctx._mqttQuickCloseCount = (ctx._mqttQuickCloseCount || 0) + 1;
|
|
653
|
+
} else {
|
|
654
|
+
ctx._mqttQuickCloseCount = 0;
|
|
655
|
+
}
|
|
656
|
+
if (ctx._mqttQuickCloseCount >= MQTT_QUICK_CLOSE_THRESHOLD) {
|
|
657
|
+
ctx._mqttQuickCloseCount = 0;
|
|
658
|
+
if (!ctx._mqttReauthing && globalReviveManager && globalReviveManager.isEnabled && globalReviveManager.isEnabled()) {
|
|
659
|
+
ctx._mqttReauthing = true;
|
|
660
|
+
|
|
661
|
+
// Try to refresh tokens first before full re-login
|
|
662
|
+
try {
|
|
663
|
+
if (api && api._cycleManager && typeof api._cycleManager.refreshTokens === 'function') {
|
|
664
|
+
utils.log("MQTT", "Attempting token refresh before re-login...");
|
|
665
|
+
const refreshed = await api._cycleManager.refreshTokens(ctx, defaultFuncs, 'https://www.facebook.com');
|
|
666
|
+
if (refreshed) {
|
|
667
|
+
utils.log("MQTT", "Token refresh successful, resetting connection state");
|
|
668
|
+
ctx._mqttReauthing = false;
|
|
669
|
+
ctx._reconnectAttempts = 0;
|
|
670
|
+
scheduleReconnect((ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000);
|
|
671
|
+
return;
|
|
672
|
+
}
|
|
673
|
+
}
|
|
674
|
+
} catch (refreshErr) {
|
|
675
|
+
utils.warn("MQTT", "Token refresh failed, proceeding with full re-login:", refreshErr.message);
|
|
676
|
+
}
|
|
677
|
+
|
|
678
|
+
globalReviveManager.handleSessionExpiry(api, 'https://www.facebook.com', "MQTT quick close loop")
|
|
679
|
+
.then((ok) => {
|
|
680
|
+
ctx._mqttReauthing = false;
|
|
681
|
+
if (ok && ctx.globalOptions.autoReconnect) {
|
|
682
|
+
ctx._reconnectAttempts = 0;
|
|
683
|
+
scheduleReconnect((ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000);
|
|
684
|
+
}
|
|
685
|
+
})
|
|
686
|
+
.catch(() => {
|
|
687
|
+
ctx._mqttReauthing = false;
|
|
688
|
+
if (ctx.globalOptions.autoReconnect) {
|
|
689
|
+
scheduleReconnect((ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000);
|
|
690
|
+
}
|
|
691
|
+
});
|
|
692
|
+
// Re-auth handles reconnect in its .then() — do not schedule a
|
|
693
|
+
// second reconnect here or both will race.
|
|
694
|
+
return;
|
|
695
|
+
}
|
|
696
|
+
}
|
|
697
|
+
}
|
|
698
|
+
|
|
699
|
+
if (ctx.globalOptions.autoReconnect) {
|
|
700
|
+
ctx._reconnectAttempts = (ctx._reconnectAttempts || 0) + 1;
|
|
701
|
+
const maxAttempts = (ctx._mqttOpt && ctx._mqttOpt.maxReconnectAttempts) || 100;
|
|
702
|
+
if (ctx._reconnectAttempts > maxAttempts) {
|
|
703
|
+
utils.warn("MQTT", `Max reconnect attempts (${maxAttempts}) reached. Pausing for 10 minutes before retrying.`);
|
|
704
|
+
ctx._reconnectAttempts = 0;
|
|
705
|
+
scheduleReconnect(10 * 60 * 1000);
|
|
706
|
+
return;
|
|
707
|
+
}
|
|
708
|
+
const baseDelay = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000;
|
|
709
|
+
const d = computeBackoffDelay(ctx, baseDelay, MQTT_MAX_BACKOFF, MQTT_JITTER_MAX);
|
|
710
|
+
utils.warn("MQTT", `Reconnecting in ${d}ms (attempt ${ctx._reconnectAttempts}/${maxAttempts})`);
|
|
711
|
+
scheduleReconnect(d);
|
|
712
|
+
}
|
|
713
|
+
}));
|
|
714
|
+
|
|
715
|
+
mqttClient.on('disconnect', guard("disconnect", () => {
|
|
716
|
+
utils.log("MQTT", "Disconnected");
|
|
717
|
+
if (ctx._tmsTimeout) {
|
|
718
|
+
clearTimeout(ctx._tmsTimeout);
|
|
719
|
+
ctx._tmsTimeout = null;
|
|
720
|
+
}
|
|
721
|
+
if (ctx._mqttWatchdog) {
|
|
722
|
+
clearInterval(ctx._mqttWatchdog);
|
|
723
|
+
ctx._mqttWatchdog = null;
|
|
724
|
+
}
|
|
725
|
+
ctx._mqttConnected = false;
|
|
726
|
+
}));
|
|
727
|
+
|
|
728
|
+
mqttClient.on('offline', guard("offline", async () => {
|
|
729
|
+
utils.warn("MQTT", "Connection went offline");
|
|
730
|
+
if (ctx._tmsTimeout) {
|
|
731
|
+
clearTimeout(ctx._tmsTimeout);
|
|
732
|
+
ctx._tmsTimeout = null;
|
|
733
|
+
}
|
|
734
|
+
if (ctx._mqttWatchdog) {
|
|
735
|
+
clearInterval(ctx._mqttWatchdog);
|
|
736
|
+
ctx._mqttWatchdog = null;
|
|
737
|
+
}
|
|
738
|
+
ctx._mqttConnected = false;
|
|
739
|
+
if (!ctx._ending && !ctx._cycling && ctx.globalOptions.autoReconnect) {
|
|
740
|
+
try { mqttClient.end(true); } catch (_) { }
|
|
741
|
+
|
|
742
|
+
// Try token refresh before reconnecting
|
|
743
|
+
try {
|
|
744
|
+
if (api && api._cycleManager && typeof api._cycleManager.refreshTokens === 'function') {
|
|
745
|
+
utils.log("MQTT", "Refreshing tokens before offline reconnect...");
|
|
746
|
+
await api._cycleManager.refreshTokens(ctx, defaultFuncs, 'https://www.facebook.com');
|
|
747
|
+
}
|
|
748
|
+
} catch (_) { /* Ignore refresh errors, will proceed with normal reconnect */ }
|
|
749
|
+
|
|
750
|
+
// Schedule a reconnect — without this the bot silently stays offline.
|
|
751
|
+
const baseDelay = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000;
|
|
752
|
+
ctx._reconnectAttempts = (ctx._reconnectAttempts || 0) + 1;
|
|
753
|
+
const d = computeBackoffDelay(ctx, baseDelay, MQTT_MAX_BACKOFF, MQTT_JITTER_MAX);
|
|
754
|
+
utils.warn("MQTT", `Offline — reconnecting in ${d}ms`);
|
|
755
|
+
scheduleReconnect(d);
|
|
756
|
+
}
|
|
757
|
+
}));
|
|
758
|
+
}
|
|
759
|
+
|
|
760
|
+
const MQTT_DEFAULTS = {
|
|
761
|
+
cycleMs: 30 * 60 * 1000,
|
|
762
|
+
reconnectDelayMs: 5000,
|
|
763
|
+
autoReconnect: true,
|
|
764
|
+
watchdogIntervalMs: 30000,
|
|
765
|
+
// 5 minutes — raised from 3 min. The MQTT keepalive is 60 s, so a healthy
|
|
766
|
+
// idle connection gets a PINGRESP every minute. 3 min was too short for
|
|
767
|
+
// quiet bots (overnight low-traffic periods) and caused constant cycling.
|
|
768
|
+
staleMs: 300000,
|
|
769
|
+
reconnectAfterStop: false,
|
|
770
|
+
maxReconnectAttempts: 1000
|
|
771
|
+
};
|
|
772
|
+
|
|
773
|
+
function mqttConf(ctx, overrides) {
|
|
774
|
+
ctx._mqttOpt = Object.assign({}, MQTT_DEFAULTS, ctx._mqttOpt || {}, overrides || {});
|
|
775
|
+
if (typeof ctx._mqttOpt.autoReconnect === "boolean") {
|
|
776
|
+
ctx.globalOptions.autoReconnect = ctx._mqttOpt.autoReconnect;
|
|
777
|
+
}
|
|
778
|
+
return ctx._mqttOpt;
|
|
779
|
+
}
|
|
780
|
+
|
|
781
|
+
module.exports = (defaultFuncs, api, ctx, opts) => {
|
|
782
|
+
const identity = () => {};
|
|
783
|
+
let globalCallback = identity;
|
|
784
|
+
// Local per-invocation state — must NOT be module-level (see note above).
|
|
785
|
+
let form = {};
|
|
786
|
+
let getSeqID;
|
|
787
|
+
|
|
788
|
+
function emitAuthError(reason, detail) {
|
|
789
|
+
try { if (ctx._autoCycleTimer) clearTimeout(ctx._autoCycleTimer); } catch (_) { }
|
|
790
|
+
try { if (ctx._reconnectTimer) clearTimeout(ctx._reconnectTimer); } catch (_) { }
|
|
791
|
+
try { ctx._ending = true; } catch (_) { }
|
|
792
|
+
try { if (ctx.mqttClient) ctx.mqttClient.end(true); } catch (_) { }
|
|
793
|
+
ctx.mqttClient = undefined;
|
|
794
|
+
ctx.loggedIn = false;
|
|
795
|
+
|
|
796
|
+
// Permanent failures (account blocked, checkpoint) should not auto-recover.
|
|
797
|
+
// Transient "not_logged_in" can recover after the session refreshes itself.
|
|
798
|
+
const isPermanentFailure = /blocked|checkpoint|banned|disabled|account.*lock/i.test(reason + " " + (detail || ""));
|
|
799
|
+
if (isPermanentFailure) {
|
|
800
|
+
ctx._permanentFailure = true;
|
|
801
|
+
}
|
|
802
|
+
|
|
803
|
+
const msg = detail || reason;
|
|
804
|
+
utils.error("AUTH", `Authentication error -> ${reason}: ${msg}`);
|
|
805
|
+
|
|
806
|
+
if (typeof globalCallback === "function") {
|
|
807
|
+
globalCallback({
|
|
808
|
+
type: "account_inactive",
|
|
809
|
+
reason: reason,
|
|
810
|
+
error: msg,
|
|
811
|
+
requiresReLogin: true,
|
|
812
|
+
timestamp: Date.now()
|
|
813
|
+
}, null);
|
|
814
|
+
}
|
|
815
|
+
try {
|
|
816
|
+
if (globalReviveManager && globalReviveManager.isEnabled && globalReviveManager.isEnabled()) {
|
|
817
|
+
globalReviveManager.handleSessionExpiry(api, 'https://www.facebook.com', "Session expired").then((ok) => {
|
|
818
|
+
if (ok && ctx._listeningActive && typeof api.listenMqtt === 'function') {
|
|
819
|
+
try {
|
|
820
|
+
if (typeof api.stopListening === 'function') {
|
|
821
|
+
try { api.stopListening(); } catch (_) {}
|
|
822
|
+
}
|
|
823
|
+
const cb = ctx._lastListenCallback || null;
|
|
824
|
+
if (cb) {
|
|
825
|
+
api.listenMqtt(cb);
|
|
826
|
+
} else {
|
|
827
|
+
api.listenMqtt();
|
|
828
|
+
}
|
|
829
|
+
} catch (_) {}
|
|
830
|
+
} else if (!ok && !isPermanentFailure && ctx.globalOptions && ctx.globalOptions.autoReconnect) {
|
|
831
|
+
// Re-login failed but this is not a permanent block — schedule a
|
|
832
|
+
// long-delay recovery. Facebook sessions often refresh on their own.
|
|
833
|
+
const recoveryDelay = 15 * 60 * 1000;
|
|
834
|
+
utils.warn("AUTH", `Re-login failed, scheduling recovery attempt in ${recoveryDelay / 60000} min`);
|
|
835
|
+
scheduleRecovery(recoveryDelay);
|
|
836
|
+
}
|
|
837
|
+
}).catch(() => {
|
|
838
|
+
if (!isPermanentFailure && ctx.globalOptions && ctx.globalOptions.autoReconnect) {
|
|
839
|
+
const recoveryDelay = 15 * 60 * 1000;
|
|
840
|
+
utils.warn("AUTH", `Re-login error, scheduling recovery in ${recoveryDelay / 60000} min`);
|
|
841
|
+
scheduleRecovery(recoveryDelay);
|
|
842
|
+
}
|
|
843
|
+
});
|
|
844
|
+
} else if (!isPermanentFailure && ctx.globalOptions && ctx.globalOptions.autoReconnect) {
|
|
845
|
+
// No autoReLogin configured — still attempt a long-delay recovery.
|
|
846
|
+
// After 3 days, Facebook sessions may have temporarily expired but will
|
|
847
|
+
// often accept new connections once the session cookie refreshes itself.
|
|
848
|
+
const recoveryDelay = 10 * 60 * 1000;
|
|
849
|
+
utils.warn("AUTH", `No autoReLogin configured, scheduling self-recovery in ${recoveryDelay / 60000} min`);
|
|
850
|
+
scheduleRecovery(recoveryDelay);
|
|
851
|
+
}
|
|
852
|
+
} catch (_) {}
|
|
853
|
+
}
|
|
854
|
+
|
|
855
|
+
function installPostGuard() {
|
|
856
|
+
if (ctx._postGuarded) return defaultFuncs.post;
|
|
857
|
+
const rawPost = defaultFuncs.post && defaultFuncs.post.bind(defaultFuncs);
|
|
858
|
+
if (!rawPost) return defaultFuncs.post;
|
|
859
|
+
|
|
860
|
+
function postSafe(...args) {
|
|
861
|
+
const lastArg = args[args.length - 1];
|
|
862
|
+
const hasCallback = typeof lastArg === 'function';
|
|
863
|
+
|
|
864
|
+
if (hasCallback) {
|
|
865
|
+
const originalCallback = args[args.length - 1];
|
|
866
|
+
args[args.length - 1] = function(err, ...cbArgs) {
|
|
867
|
+
if (err) {
|
|
868
|
+
const msg = (err && err.error) || (err && err.message) || String(err || "");
|
|
869
|
+
if (/Not logged in|Not logged in\.|blocked the login|checkpoint|security check|session.*expir|invalid.*session|authentication.*fail|auth.*fail/i.test(msg)) {
|
|
870
|
+
emitAuthError(
|
|
871
|
+
/blocked|checkpoint|security/i.test(msg) ? "login_blocked" : "not_logged_in",
|
|
872
|
+
msg
|
|
873
|
+
);
|
|
874
|
+
}
|
|
875
|
+
}
|
|
876
|
+
return originalCallback(err, ...cbArgs);
|
|
877
|
+
};
|
|
878
|
+
return rawPost(...args);
|
|
879
|
+
} else {
|
|
880
|
+
const result = rawPost(...args);
|
|
881
|
+
if (result && typeof result.catch === 'function') {
|
|
882
|
+
return result.catch(err => {
|
|
883
|
+
const msg = (err && err.error) || (err && err.message) || String(err || "");
|
|
884
|
+
if (/Not logged in|Not logged in\.|blocked the login|checkpoint|security check|session.*expir|invalid.*session|authentication.*fail|auth.*fail/i.test(msg)) {
|
|
885
|
+
emitAuthError(
|
|
886
|
+
/blocked|checkpoint|security/i.test(msg) ? "login_blocked" : "not_logged_in",
|
|
887
|
+
msg
|
|
888
|
+
);
|
|
889
|
+
}
|
|
890
|
+
throw err;
|
|
891
|
+
});
|
|
892
|
+
}
|
|
893
|
+
return result;
|
|
894
|
+
}
|
|
895
|
+
}
|
|
896
|
+
defaultFuncs.post = postSafe;
|
|
897
|
+
ctx._postGuarded = true;
|
|
898
|
+
utils.log("MQTT", "PostSafe guard installed for anti-automation detection");
|
|
899
|
+
return postSafe;
|
|
900
|
+
}
|
|
901
|
+
|
|
902
|
+
function scheduleReconnect(delayMs) {
|
|
903
|
+
const d = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000;
|
|
904
|
+
const ms = typeof delayMs === "number" ? delayMs : d;
|
|
905
|
+
if (ctx._ending) return;
|
|
906
|
+
if (ctx._reconnectTimer) return;
|
|
907
|
+
utils.warn("MQTT", `Will reconnect in ${ms}ms`);
|
|
908
|
+
ctx._reconnectTimer = setTimeout(() => {
|
|
909
|
+
ctx._reconnectTimer = null;
|
|
910
|
+
getSeqIDWrapper();
|
|
911
|
+
}, ms);
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
// scheduleRecovery bypasses the ctx._ending guard so the bot can recover
|
|
915
|
+
// from auth-error shutdowns that are transient (e.g. 3-day session rotation).
|
|
916
|
+
// It must NOT be called for permanent failures (checkpoint, banned account).
|
|
917
|
+
function scheduleRecovery(delayMs) {
|
|
918
|
+
if (ctx._permanentFailure) {
|
|
919
|
+
utils.warn("MQTT", "Recovery skipped — permanent account failure detected");
|
|
920
|
+
return;
|
|
921
|
+
}
|
|
922
|
+
if (ctx._recoveryTimer) return;
|
|
923
|
+
const ms = typeof delayMs === "number" && delayMs > 0 ? delayMs : 10 * 60 * 1000;
|
|
924
|
+
utils.warn("MQTT", `Recovery scheduled in ${Math.round(ms / 60000)} min — will reset state and retry`);
|
|
925
|
+
ctx._recoveryTimer = setTimeout(() => {
|
|
926
|
+
ctx._recoveryTimer = null;
|
|
927
|
+
if (ctx._permanentFailure) return;
|
|
928
|
+
utils.warn("MQTT", "Recovery attempt: resetting _ending flag and reconnecting");
|
|
929
|
+
ctx._ending = false;
|
|
930
|
+
ctx._reconnectAttempts = 0;
|
|
931
|
+
ctx._seqIdFailCount = 0;
|
|
932
|
+
if (!ctx._reconnectTimer) {
|
|
933
|
+
getSeqIDWrapper();
|
|
934
|
+
}
|
|
935
|
+
}, ms);
|
|
936
|
+
}
|
|
937
|
+
|
|
938
|
+
let conf = mqttConf(ctx, opts);
|
|
939
|
+
installPostGuard();
|
|
940
|
+
|
|
941
|
+
getSeqID = retryWithBackoff(async () => {
|
|
942
|
+
try {
|
|
943
|
+
form = {
|
|
944
|
+
av: ctx.globalOptions.pageID,
|
|
945
|
+
queries: JSON.stringify({
|
|
946
|
+
o0: {
|
|
947
|
+
doc_id: "3336396659757871",
|
|
948
|
+
query_params: {
|
|
949
|
+
limit: 1,
|
|
950
|
+
before: null,
|
|
951
|
+
tags: ["INBOX"],
|
|
952
|
+
includeDeliveryReceipts: false,
|
|
953
|
+
includeSeqID: true
|
|
954
|
+
}
|
|
955
|
+
}
|
|
956
|
+
})
|
|
957
|
+
};
|
|
958
|
+
utils.log("MQTT", "Getting sequence ID...");
|
|
959
|
+
ctx.t_mqttCalled = false;
|
|
960
|
+
const resData = await defaultFuncs.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
961
|
+
|
|
962
|
+
if (utils.getType(resData) !== "Array") {
|
|
963
|
+
throw { error: "Not logged in" };
|
|
964
|
+
}
|
|
965
|
+
if (!Array.isArray(resData) || !resData.length) {
|
|
966
|
+
throw { error: "getSeqID: empty response" };
|
|
967
|
+
}
|
|
968
|
+
|
|
969
|
+
const lastRes = resData[resData.length - 1];
|
|
970
|
+
if (lastRes && lastRes.successful_results === 0) {
|
|
971
|
+
throw { error: "getSeqID: no successful results" };
|
|
972
|
+
}
|
|
973
|
+
|
|
974
|
+
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
|
+
if (syncSeqId) {
|
|
976
|
+
ctx.lastSeqId = syncSeqId;
|
|
977
|
+
ctx._cycling = false;
|
|
978
|
+
utils.log("MQTT", "getSeqID ok -> listenMqtt()");
|
|
979
|
+
listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconnect, emitAuthError);
|
|
980
|
+
} else {
|
|
981
|
+
throw { error: "getSeqID: no sync_sequence_id found" };
|
|
982
|
+
}
|
|
983
|
+
} catch (err) {
|
|
984
|
+
const detail = (err && err.detail && err.detail.message) ? ` | detail=${err.detail.message}` : "";
|
|
985
|
+
const msg = ((err && err.error) || (err && err.message) || String(err || "")) + detail;
|
|
986
|
+
|
|
987
|
+
if (/blocked the login|checkpoint|security check|authentication.*fail|auth.*fail|login.*block|account.*lock|verification.*requir|banned|disabled/i.test(msg)) {
|
|
988
|
+
utils.error("MQTT", "Auth error in getSeqID: Session/Login blocked (permanent)");
|
|
989
|
+
ctx._seqIdFailCount = 0;
|
|
990
|
+
return emitAuthError("login_blocked", msg);
|
|
991
|
+
}
|
|
992
|
+
|
|
993
|
+
throw err; // Re-throw for retry mechanism
|
|
994
|
+
}
|
|
995
|
+
}, 3, 1500);
|
|
996
|
+
|
|
997
|
+
function getSeqIDWrapper() {
|
|
998
|
+
utils.log("MQTT", "getSeqID call");
|
|
999
|
+
return getSeqID()
|
|
1000
|
+
.then(() => {
|
|
1001
|
+
utils.log("MQTT", "getSeqID done");
|
|
1002
|
+
ctx._cycling = false;
|
|
1003
|
+
})
|
|
1004
|
+
.catch(e => {
|
|
1005
|
+
utils.error("MQTT", `getSeqID error: ${e && e.message ? e.message : e}`);
|
|
1006
|
+
if (ctx.globalOptions.autoReconnect) {
|
|
1007
|
+
ctx._reconnectAttempts = (ctx._reconnectAttempts || 0) + 1;
|
|
1008
|
+
const baseDelay = (ctx._mqttOpt && ctx._mqttOpt.reconnectDelayMs) || 2000;
|
|
1009
|
+
scheduleReconnect(computeBackoffDelay(ctx, baseDelay, MQTT_MAX_BACKOFF, MQTT_JITTER_MAX));
|
|
1010
|
+
}
|
|
1011
|
+
});
|
|
1012
|
+
}
|
|
1013
|
+
|
|
1014
|
+
function isConnected() {
|
|
1015
|
+
return !!(ctx.mqttClient && ctx.mqttClient.connected);
|
|
1016
|
+
}
|
|
1017
|
+
|
|
1018
|
+
function unsubAll(cb) {
|
|
1019
|
+
if (!isConnected()) return cb && cb();
|
|
1020
|
+
let pending = topics.length;
|
|
1021
|
+
if (!pending) return cb && cb();
|
|
1022
|
+
let fired = false;
|
|
1023
|
+
// Safety timeout: if any unsubscribe callback never fires (e.g. network
|
|
1024
|
+
// dropped mid-unsub), we still proceed so reconnect is never blocked.
|
|
1025
|
+
const safetyTimer = setTimeout(() => {
|
|
1026
|
+
if (!fired) {
|
|
1027
|
+
fired = true;
|
|
1028
|
+
cb && cb();
|
|
1029
|
+
}
|
|
1030
|
+
}, 3000);
|
|
1031
|
+
topics.forEach(t => {
|
|
1032
|
+
ctx.mqttClient.unsubscribe(t, () => {
|
|
1033
|
+
if (--pending === 0 && !fired) {
|
|
1034
|
+
fired = true;
|
|
1035
|
+
clearTimeout(safetyTimer);
|
|
1036
|
+
cb && cb();
|
|
1037
|
+
}
|
|
1038
|
+
});
|
|
1039
|
+
});
|
|
1040
|
+
}
|
|
1041
|
+
|
|
1042
|
+
function endQuietly(next) {
|
|
1043
|
+
const finish = () => {
|
|
1044
|
+
try {
|
|
1045
|
+
ctx.mqttClient && ctx.mqttClient.removeAllListeners();
|
|
1046
|
+
} catch (_) { }
|
|
1047
|
+
if (ctx._tmsTimeout) {
|
|
1048
|
+
clearTimeout(ctx._tmsTimeout);
|
|
1049
|
+
ctx._tmsTimeout = null;
|
|
1050
|
+
}
|
|
1051
|
+
if (ctx._reconnectTimer) {
|
|
1052
|
+
clearTimeout(ctx._reconnectTimer);
|
|
1053
|
+
ctx._reconnectTimer = null;
|
|
1054
|
+
}
|
|
1055
|
+
if (ctx._recoveryTimer) {
|
|
1056
|
+
clearTimeout(ctx._recoveryTimer);
|
|
1057
|
+
ctx._recoveryTimer = null;
|
|
1058
|
+
}
|
|
1059
|
+
if (ctx._mqttWatchdog) {
|
|
1060
|
+
clearInterval(ctx._mqttWatchdog);
|
|
1061
|
+
ctx._mqttWatchdog = null;
|
|
1062
|
+
}
|
|
1063
|
+
ctx.mqttClient = undefined;
|
|
1064
|
+
ctx.lastSeqId = null;
|
|
1065
|
+
ctx.syncToken = undefined;
|
|
1066
|
+
ctx.t_mqttCalled = false;
|
|
1067
|
+
ctx._ending = false;
|
|
1068
|
+
ctx._mqttConnected = false;
|
|
1069
|
+
ctx._seqIdFailCount = 0;
|
|
1070
|
+
next && next();
|
|
1071
|
+
};
|
|
1072
|
+
try {
|
|
1073
|
+
if (ctx.mqttClient) {
|
|
1074
|
+
if (isConnected()) {
|
|
1075
|
+
try {
|
|
1076
|
+
ctx.mqttClient.publish("/browser_close", "{}");
|
|
1077
|
+
} catch (_) { }
|
|
1078
|
+
}
|
|
1079
|
+
ctx.mqttClient.end(true, finish);
|
|
1080
|
+
} else finish();
|
|
1081
|
+
} catch (_) {
|
|
1082
|
+
finish();
|
|
1083
|
+
}
|
|
1084
|
+
}
|
|
1085
|
+
|
|
1086
|
+
function delayedReconnect() {
|
|
1087
|
+
const d = conf.reconnectDelayMs;
|
|
1088
|
+
utils.log("MQTT", `Reconnect in ${d}ms`);
|
|
1089
|
+
setTimeout(() => getSeqIDWrapper(), d);
|
|
1090
|
+
}
|
|
1091
|
+
|
|
1092
|
+
function forceCycle() {
|
|
1093
|
+
if (ctx._cycling) return;
|
|
1094
|
+
ctx._cycling = true;
|
|
1095
|
+
ctx._ending = true;
|
|
1096
|
+
utils.warn("MQTT", "Force cycle begin");
|
|
1097
|
+
unsubAll(() => endQuietly(() => delayedReconnect()));
|
|
1098
|
+
}
|
|
1099
|
+
|
|
1100
|
+
return (callback) => {
|
|
1101
|
+
class MessageEmitter extends EventEmitter {
|
|
1102
|
+
stopListening(callback2) {
|
|
1103
|
+
const cb = callback2 || function() {};
|
|
1104
|
+
utils.log("MQTT", "Stop requested");
|
|
1105
|
+
globalCallback = identity;
|
|
1106
|
+
ctx._listeningActive = false;
|
|
1107
|
+
|
|
1108
|
+
if (ctx._autoCycleTimer) {
|
|
1109
|
+
clearInterval(ctx._autoCycleTimer);
|
|
1110
|
+
ctx._autoCycleTimer = null;
|
|
1111
|
+
utils.log("MQTT", "Auto-cycle cleared");
|
|
1112
|
+
}
|
|
1113
|
+
|
|
1114
|
+
if (ctx._reconnectTimer) {
|
|
1115
|
+
clearTimeout(ctx._reconnectTimer);
|
|
1116
|
+
ctx._reconnectTimer = null;
|
|
1117
|
+
utils.log("MQTT", "Reconnect timer cleared");
|
|
1118
|
+
}
|
|
1119
|
+
|
|
1120
|
+
if (ctx._recoveryTimer) {
|
|
1121
|
+
clearTimeout(ctx._recoveryTimer);
|
|
1122
|
+
ctx._recoveryTimer = null;
|
|
1123
|
+
utils.log("MQTT", "Recovery timer cleared");
|
|
1124
|
+
}
|
|
1125
|
+
|
|
1126
|
+
if (ctx._tmsTimeout) {
|
|
1127
|
+
clearTimeout(ctx._tmsTimeout);
|
|
1128
|
+
ctx._tmsTimeout = null;
|
|
1129
|
+
utils.log("MQTT", "TMS timeout cleared");
|
|
1130
|
+
}
|
|
1131
|
+
if (ctx._mqttWatchdog) {
|
|
1132
|
+
clearInterval(ctx._mqttWatchdog);
|
|
1133
|
+
ctx._mqttWatchdog = null;
|
|
1134
|
+
utils.log("MQTT", "Watchdog cleared");
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
ctx._ending = true;
|
|
1138
|
+
ctx._permanentFailure = false;
|
|
1139
|
+
ctx._seqIdFailCount = 0;
|
|
1140
|
+
ctx._reconnectAttempts = 0;
|
|
1141
|
+
|
|
1142
|
+
// Stop background timers that would keep making requests
|
|
1143
|
+
// to Facebook after the bot is supposed to be idle.
|
|
1144
|
+
try {
|
|
1145
|
+
if (api._cycleManager && typeof api._cycleManager.stopAutoRefresh === 'function') {
|
|
1146
|
+
api._cycleManager.stopAutoRefresh();
|
|
1147
|
+
utils.log("MQTT", "Token refresh stopped");
|
|
1148
|
+
}
|
|
1149
|
+
} catch (_) {}
|
|
1150
|
+
try {
|
|
1151
|
+
if (globalReviveManager && typeof globalReviveManager.stopSessionMonitoring === 'function') {
|
|
1152
|
+
globalReviveManager.stopSessionMonitoring();
|
|
1153
|
+
utils.log("MQTT", "Session monitoring stopped");
|
|
1154
|
+
}
|
|
1155
|
+
} catch (_) {}
|
|
1156
|
+
|
|
1157
|
+
unsubAll(() => endQuietly(() => {
|
|
1158
|
+
utils.log("MQTT", "Stopped successfully");
|
|
1159
|
+
cb();
|
|
1160
|
+
conf = mqttConf(ctx, conf);
|
|
1161
|
+
if (conf.reconnectAfterStop) delayedReconnect();
|
|
1162
|
+
}));
|
|
1163
|
+
}
|
|
1164
|
+
|
|
1165
|
+
async stopListeningAsync() {
|
|
1166
|
+
return new Promise(resolve => {
|
|
1167
|
+
this.stopListening(resolve);
|
|
1168
|
+
});
|
|
1169
|
+
}
|
|
1170
|
+
}
|
|
1171
|
+
|
|
1172
|
+
const msgEmitter = new MessageEmitter();
|
|
1173
|
+
|
|
1174
|
+
globalCallback = callback || function(error, message) {
|
|
1175
|
+
if (error) {
|
|
1176
|
+
utils.error("MQTT", "Emit error");
|
|
1177
|
+
return msgEmitter.emit("error", error);
|
|
1178
|
+
}
|
|
1179
|
+
if (message && (message.type === "message" || message.type === "message_reply")) {
|
|
1180
|
+
markAsRead(ctx, api, message.threadID);
|
|
1181
|
+
}
|
|
1182
|
+
msgEmitter.emit("message", message);
|
|
1183
|
+
};
|
|
1184
|
+
|
|
1185
|
+
ctx._listeningActive = true;
|
|
1186
|
+
ctx._lastListenCallback = callback || null;
|
|
1187
|
+
|
|
1188
|
+
conf = mqttConf(ctx, conf);
|
|
1189
|
+
|
|
1190
|
+
if (!ctx.firstListen) ctx.lastSeqId = null;
|
|
1191
|
+
ctx.syncToken = undefined;
|
|
1192
|
+
ctx.t_mqttCalled = false;
|
|
1193
|
+
|
|
1194
|
+
if (ctx._autoCycleTimer) {
|
|
1195
|
+
clearTimeout(ctx._autoCycleTimer);
|
|
1196
|
+
ctx._autoCycleTimer = null;
|
|
1197
|
+
}
|
|
1198
|
+
|
|
1199
|
+
function scheduleAutoCycle() {
|
|
1200
|
+
const base = conf.cycleMs;
|
|
1201
|
+
if (!base || base <= 0) return;
|
|
1202
|
+
const jitter = Math.floor(base * (0.2 + Math.random() * 0.4));
|
|
1203
|
+
const next = base + (Math.random() > 0.5 ? jitter : -jitter);
|
|
1204
|
+
ctx._autoCycleTimer = setTimeout(() => {
|
|
1205
|
+
ctx._autoCycleTimer = null;
|
|
1206
|
+
forceCycle();
|
|
1207
|
+
scheduleAutoCycle();
|
|
1208
|
+
}, next);
|
|
1209
|
+
utils.log("MQTT", `Auto-cycle scheduled: ${next}ms`);
|
|
1210
|
+
}
|
|
1211
|
+
if (conf.cycleMs && conf.cycleMs > 0) {
|
|
1212
|
+
scheduleAutoCycle();
|
|
1213
|
+
} else {
|
|
1214
|
+
utils.log("MQTT", "Auto-cycle disabled");
|
|
1215
|
+
}
|
|
1216
|
+
|
|
1217
|
+
if (!ctx.firstListen || !ctx.lastSeqId) {
|
|
1218
|
+
getSeqIDWrapper();
|
|
1219
|
+
} else {
|
|
1220
|
+
utils.log("MQTT", "Starting listenMqtt");
|
|
1221
|
+
listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconnect, emitAuthError);
|
|
1222
|
+
}
|
|
1223
|
+
|
|
1224
|
+
if (ctx.firstListen) {
|
|
1225
|
+
api.markAsReadAll().catch(err => {
|
|
1226
|
+
utils.error("Failed to mark all messages as read on startup:", err);
|
|
1227
|
+
});
|
|
1228
|
+
}
|
|
1229
|
+
|
|
1230
|
+
ctx.firstListen = false;
|
|
1231
|
+
|
|
1232
|
+
api.stopListening = msgEmitter.stopListening;
|
|
1233
|
+
api.stopListeningAsync = msgEmitter.stopListeningAsync;
|
|
1234
|
+
return msgEmitter;
|
|
1235
|
+
};
|
|
1236
|
+
};
|