@lazyneoaz/metachat 1.1.3 → 1.1.4
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/package.json +1 -2
- package/src/apis/changeAdminStatus.js +2 -2
- package/src/apis/changeGroupImage.js +1 -1
- package/src/apis/createPoll.js +1 -1
- package/src/apis/deleteMessage.js +9 -90
- package/src/apis/emoji.js +2 -2
- package/src/apis/gcmember.js +87 -131
- package/src/apis/gcname.js +1 -1
- package/src/apis/gcrule.js +24 -20
- package/src/apis/listenMqtt.js +1 -7
- package/src/apis/nickname.js +1 -1
- package/src/apis/pinMessage.js +1 -1
- package/src/apis/sendMessage.js +58 -65
- package/src/apis/sendMessageMqtt.js +3 -21
- package/src/apis/setThreadThemeMqtt.js +6 -7
- package/src/apis/theme.js +2 -2
- package/src/apis/unsendMessage.js +1 -1
- package/src/engine/client.js +1 -1
- package/src/utils/antiSuspension.js +0 -29
- package/src/utils/autoReLogin.js +1 -4
- package/src/utils/headers.js +38 -20
- package/src/utils/lsRequest.js +1 -1
- package/src/utils/rateLimiter.js +3 -9
- package/src/utils/user-agents.js +76 -32
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@lazyneoaz/metachat",
|
|
3
|
-
"version": "1.1.
|
|
3
|
+
"version": "1.1.4",
|
|
4
4
|
"type": "commonjs",
|
|
5
5
|
"types": "src/types/index.d.ts",
|
|
6
6
|
"description": "Advanced Facebook Chat API client for building Messenger bots — real-time messaging, thread management, MQTT, and session stability.",
|
|
@@ -37,7 +37,6 @@
|
|
|
37
37
|
"metachat"
|
|
38
38
|
],
|
|
39
39
|
"dependencies": {
|
|
40
|
-
"@dongdev/fca-unofficial": "^4.0.3",
|
|
41
40
|
"axios": "^1.13.5",
|
|
42
41
|
"axios-cookiejar-support": "^4.0.7",
|
|
43
42
|
"bluebird": "^3.7.2",
|
|
@@ -67,7 +67,7 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
67
67
|
});
|
|
68
68
|
|
|
69
69
|
const form = JSON.stringify({
|
|
70
|
-
app_id: "2220391788200892",
|
|
70
|
+
app_id: String(ctx.appID || ctx.mqttAppID || "2220391788200892"),
|
|
71
71
|
payload: JSON.stringify({
|
|
72
72
|
epoch_id: epochID,
|
|
73
73
|
tasks: tasks,
|
|
@@ -114,7 +114,7 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
114
114
|
epoch_id: epochID,
|
|
115
115
|
data_trace_id: null
|
|
116
116
|
}),
|
|
117
|
-
app_id: "
|
|
117
|
+
app_id: String(ctx.appID || ctx.mqttAppID || "772021112871879")
|
|
118
118
|
};
|
|
119
119
|
|
|
120
120
|
defaultFuncs
|
|
@@ -110,7 +110,7 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
110
110
|
};
|
|
111
111
|
|
|
112
112
|
const request = {
|
|
113
|
-
app_id: "2220391788200892",
|
|
113
|
+
app_id: String(ctx.appID || ctx.mqttAppID || "2220391788200892"),
|
|
114
114
|
payload: JSON.stringify(mqttPayload),
|
|
115
115
|
request_id: reqID,
|
|
116
116
|
type: 3
|
package/src/apis/createPoll.js
CHANGED
|
@@ -1,20 +1,9 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const utils = require('../utils');
|
|
4
|
-
const { publishLsRequestWithAck } = require('../utils/lsRequest');
|
|
5
4
|
|
|
6
5
|
module.exports = (defaultFuncs, api, ctx) => {
|
|
7
|
-
|
|
8
|
-
* Deletes one or more messages.
|
|
9
|
-
*
|
|
10
|
-
* Uses MQTT label "146" (version "25909428212080747") when the MQTT client is
|
|
11
|
-
* connected — this matches dongdev's deleteMessage implementation.
|
|
12
|
-
* Falls back to the legacy HTTP delete_messages.php endpoint otherwise.
|
|
13
|
-
*
|
|
14
|
-
* @param {string|string[]} messageOrMessages
|
|
15
|
-
* @param {Function} [callback]
|
|
16
|
-
*/
|
|
17
|
-
return async function deleteMessage(messageOrMessages, callback) {
|
|
6
|
+
return async function deleteMessage(messageID, callback) {
|
|
18
7
|
let resolveFunc = () => {};
|
|
19
8
|
let rejectFunc = () => {};
|
|
20
9
|
const returnPromise = new Promise((resolve, reject) => {
|
|
@@ -37,66 +26,17 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
37
26
|
}
|
|
38
27
|
|
|
39
28
|
try {
|
|
40
|
-
const
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
throw new Error("messageOrMessages must contain at least one non-empty message ID");
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
const mqttReady = !!(ctx.mqttClient && ctx.mqttClient.connected);
|
|
49
|
-
|
|
50
|
-
if (mqttReady) {
|
|
51
|
-
// MQTT path: label "146", version "25909428212080747" (dongdev confirmed)
|
|
52
|
-
if (typeof ctx.wsReqNumber !== "number") ctx.wsReqNumber = 0;
|
|
53
|
-
if (typeof ctx.wsTaskNumber !== "number") ctx.wsTaskNumber = 0;
|
|
54
|
-
|
|
55
|
-
const requestId = ++ctx.wsReqNumber;
|
|
56
|
-
const tasks = messages.map((messageID) => {
|
|
57
|
-
const queueName = String(messageID);
|
|
58
|
-
const taskId = ++ctx.wsTaskNumber;
|
|
59
|
-
return {
|
|
60
|
-
failure_count: null,
|
|
61
|
-
label: "146",
|
|
62
|
-
payload: JSON.stringify({
|
|
63
|
-
thread_key: queueName,
|
|
64
|
-
remove_type: 0,
|
|
65
|
-
sync_group: 1
|
|
66
|
-
}),
|
|
67
|
-
queue_name: queueName,
|
|
68
|
-
task_id: taskId
|
|
69
|
-
};
|
|
70
|
-
});
|
|
71
|
-
|
|
72
|
-
publishLsRequestWithAck({
|
|
73
|
-
client: ctx.mqttClient,
|
|
74
|
-
requestId,
|
|
75
|
-
timeoutMs: 20000,
|
|
76
|
-
content: {
|
|
77
|
-
app_id: "2220391788200892",
|
|
78
|
-
payload: JSON.stringify({
|
|
79
|
-
epoch_id: parseInt(utils.generateOfflineThreadingID()),
|
|
80
|
-
tasks,
|
|
81
|
-
version_id: "25909428212080747"
|
|
82
|
-
}),
|
|
83
|
-
request_id: requestId,
|
|
84
|
-
type: 3
|
|
85
|
-
},
|
|
86
|
-
extract: (message) => ({ success: true, response: message.payload })
|
|
87
|
-
}).then((result) => {
|
|
88
|
-
callback(null, result);
|
|
89
|
-
}).catch((mqttErr) => {
|
|
90
|
-
utils.error("deleteMessage/mqtt", mqttErr);
|
|
91
|
-
// MQTT failed → fall back to HTTP
|
|
92
|
-
_deleteViaHttp(messages, callback, defaultFuncs, ctx);
|
|
93
|
-
});
|
|
29
|
+
const res = await defaultFuncs.post(
|
|
30
|
+
"https://www.facebook.com/ajax/mercury/delete_messages.php",
|
|
31
|
+
ctx.jar,
|
|
32
|
+
{ message_id: messageID }
|
|
33
|
+
).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
94
34
|
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
await _deleteViaHttp(messages, callback, defaultFuncs, ctx);
|
|
35
|
+
if (res && res.error) {
|
|
36
|
+
throw new Error(String(res.error_msg || res.error || "deleteMessage failed"));
|
|
98
37
|
}
|
|
99
38
|
|
|
39
|
+
callback(null, { success: true });
|
|
100
40
|
} catch (err) {
|
|
101
41
|
utils.error("deleteMessage", err);
|
|
102
42
|
callback(err instanceof Error ? err : new Error(String(err && err.message ? err.message : err)));
|
|
@@ -105,24 +45,3 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
105
45
|
return returnPromise;
|
|
106
46
|
};
|
|
107
47
|
};
|
|
108
|
-
|
|
109
|
-
async function _deleteViaHttp(messages, callback, defaultFuncs, ctx) {
|
|
110
|
-
try {
|
|
111
|
-
// The legacy endpoint only accepts one messageID at a time
|
|
112
|
-
for (const messageID of messages) {
|
|
113
|
-
const res = await defaultFuncs.post(
|
|
114
|
-
"https://www.facebook.com/ajax/mercury/delete_messages.php",
|
|
115
|
-
ctx.jar,
|
|
116
|
-
{ message_id: messageID }
|
|
117
|
-
).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
118
|
-
|
|
119
|
-
if (res && res.error) {
|
|
120
|
-
throw new Error(String(res.error_msg || res.error || "deleteMessage HTTP failed"));
|
|
121
|
-
}
|
|
122
|
-
}
|
|
123
|
-
callback(null, { success: true });
|
|
124
|
-
} catch (err) {
|
|
125
|
-
utils.error("deleteMessage/http", err);
|
|
126
|
-
callback(err instanceof Error ? err : new Error(String(err && err.message ? err.message : err)));
|
|
127
|
-
}
|
|
128
|
-
}
|
package/src/apis/emoji.js
CHANGED
|
@@ -92,11 +92,11 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
92
92
|
};
|
|
93
93
|
|
|
94
94
|
const context = {
|
|
95
|
-
app_id: '2220391788200892',
|
|
95
|
+
app_id: ctx.appID || '2220391788200892',
|
|
96
96
|
payload: {
|
|
97
97
|
epoch_id: parseInt(utils.generateOfflineThreadingID()),
|
|
98
98
|
tasks: [query],
|
|
99
|
-
version_id: '
|
|
99
|
+
version_id: '24631415369801570',
|
|
100
100
|
},
|
|
101
101
|
request_id: ctx.wsReqNumber,
|
|
102
102
|
type: 3,
|
package/src/apis/gcmember.js
CHANGED
|
@@ -1,172 +1,128 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
|
|
3
3
|
const utils = require('../utils');
|
|
4
|
-
const { publishLsRequestWithAck } = require('../utils/lsRequest');
|
|
5
4
|
|
|
6
5
|
module.exports = function (defaultFuncs, api, ctx) {
|
|
7
6
|
/**
|
|
8
|
-
*
|
|
7
|
+
* Made by ChoruOfficial
|
|
8
|
+
* Mqtt
|
|
9
|
+
* Adds or removes members from a group chat with pre-checking.
|
|
9
10
|
*
|
|
10
|
-
*
|
|
11
|
-
*
|
|
12
|
-
*
|
|
13
|
-
*
|
|
14
|
-
*
|
|
15
|
-
* @param {"add"|"remove"} action
|
|
16
|
-
* @param {string|string[]} userIDs
|
|
17
|
-
* @param {string} threadID
|
|
18
|
-
* @param {Function} [callback]
|
|
11
|
+
* @param {"add" | "remove"} action The action to perform.
|
|
12
|
+
* @param {string|string[]} userIDs The user ID or array of user IDs.
|
|
13
|
+
* @param {string} threadID The ID of the group chat.
|
|
14
|
+
* @param {Function} [callback] Optional callback function.
|
|
15
|
+
* @returns {Promise<object>} A promise that resolves with information about the action.
|
|
19
16
|
*/
|
|
20
17
|
return async function gcmember(action, userIDs, threadID, callback) {
|
|
21
18
|
let _callback;
|
|
22
19
|
if (typeof threadID === 'function') {
|
|
23
|
-
|
|
24
|
-
|
|
20
|
+
_callback = threadID;
|
|
21
|
+
threadID = null;
|
|
25
22
|
} else if (typeof callback === 'function') {
|
|
26
|
-
|
|
23
|
+
_callback = callback;
|
|
27
24
|
}
|
|
28
|
-
|
|
25
|
+
|
|
29
26
|
let resolvePromise, rejectPromise;
|
|
30
27
|
const returnPromise = new Promise((resolve, reject) => {
|
|
31
|
-
|
|
32
|
-
|
|
28
|
+
resolvePromise = resolve;
|
|
29
|
+
rejectPromise = reject;
|
|
33
30
|
});
|
|
34
31
|
|
|
35
|
-
if (typeof _callback
|
|
32
|
+
if (typeof _callback != "function") {
|
|
36
33
|
_callback = (err, data) => {
|
|
34
|
+
// Note: We will now rarely use the 'err' parameter for validation errors
|
|
37
35
|
if (err) return rejectPromise(err);
|
|
38
36
|
resolvePromise(data);
|
|
39
|
-
}
|
|
37
|
+
}
|
|
40
38
|
}
|
|
41
39
|
|
|
42
40
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
46
|
-
if (!validActions.includes(action)) {
|
|
47
|
-
_callback(null, { type: "error_gc", error: `Invalid action. Must be one of: ${validActions.join(", ")}` });
|
|
48
|
-
return returnPromise;
|
|
49
|
-
}
|
|
50
|
-
if (!userIDs || (Array.isArray(userIDs) ? userIDs.length === 0 : false)) {
|
|
51
|
-
_callback(null, { type: "error_gc", error: "userIDs is required." });
|
|
52
|
-
return returnPromise;
|
|
53
|
-
}
|
|
54
|
-
if (!threadID) {
|
|
55
|
-
_callback(null, { type: "error_gc", error: "threadID is required." });
|
|
56
|
-
return returnPromise;
|
|
57
|
-
}
|
|
58
|
-
if (!ctx.mqttClient || !ctx.mqttClient.connected) {
|
|
59
|
-
_callback(null, { type: "error_gc", error: "Not connected to MQTT" });
|
|
60
|
-
return returnPromise;
|
|
61
|
-
}
|
|
62
|
-
|
|
63
|
-
const threadInfo = await api.getThreadInfo(threadID);
|
|
64
|
-
if (!threadInfo) {
|
|
65
|
-
_callback(null, { type: "error_gc", error: "Could not retrieve thread information." });
|
|
66
|
-
return returnPromise;
|
|
67
|
-
}
|
|
68
|
-
if (threadInfo.isGroup === false) {
|
|
69
|
-
_callback(null, { type: "error_gc", error: "This feature is only for group chats, not private messages." });
|
|
70
|
-
return returnPromise;
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
const currentMembers = threadInfo.participantIDs || [];
|
|
74
|
-
const usersToModify = Array.isArray(userIDs) ? userIDs : [userIDs];
|
|
41
|
+
const validActions = ["add", "remove"];
|
|
42
|
+
action = action ? action.toLowerCase() : "";
|
|
75
43
|
|
|
76
|
-
if (typeof ctx.wsReqNumber !== "number") ctx.wsReqNumber = 0;
|
|
77
|
-
if (typeof ctx.wsTaskNumber !== "number") ctx.wsTaskNumber = 0;
|
|
78
44
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
82
|
-
|
|
83
|
-
|
|
84
|
-
|
|
45
|
+
if (!validActions.includes(action)) {
|
|
46
|
+
_callback(null, { type: "error_gc", error: `Invalid action. Must be one of: ${validActions.join(", ")}` });
|
|
47
|
+
return returnPromise;
|
|
48
|
+
}
|
|
49
|
+
if (!userIDs || userIDs.length === 0) {
|
|
50
|
+
_callback(null, { type: "error_gc", error: "userIDs is required." });
|
|
51
|
+
return returnPromise;
|
|
52
|
+
}
|
|
53
|
+
if (!threadID) {
|
|
54
|
+
_callback(null, { type: "error_gc", error: "threadID is required." });
|
|
55
|
+
return returnPromise;
|
|
56
|
+
}
|
|
57
|
+
if (!ctx.mqttClient || !ctx.mqttClient.connected) {
|
|
58
|
+
_callback(null, { type: "error_gc", error: "Not connected to MQTT" });
|
|
59
|
+
return returnPromise;
|
|
85
60
|
}
|
|
86
61
|
|
|
87
|
-
const
|
|
88
|
-
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
99
|
-
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
}
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
|
|
115
|
-
}
|
|
116
|
-
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
});
|
|
125
|
-
|
|
126
|
-
} else {
|
|
127
|
-
// action === 'remove'
|
|
128
|
-
const userToRemove = String(usersToModify[0]);
|
|
129
|
-
if (!currentMembers.includes(userToRemove)) {
|
|
130
|
-
_callback(null, { type: "error_gc", error: `User ${userToRemove} is not in this group.` });
|
|
131
|
-
return returnPromise;
|
|
62
|
+
const threadInfo = await api.getThreadInfo(threadID);
|
|
63
|
+
if (!threadInfo) {
|
|
64
|
+
_callback(null, { type: "error_gc", error: "Could not retrieve thread information." });
|
|
65
|
+
return returnPromise;
|
|
66
|
+
}
|
|
67
|
+
if (threadInfo.isGroup === false) {
|
|
68
|
+
_callback(null, { type: "error_gc", error: "This feature is only for group chats, not private messages." });
|
|
69
|
+
return returnPromise;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const currentMembers = threadInfo.participantIDs;
|
|
73
|
+
const usersToModify = Array.isArray(userIDs) ? userIDs : [userIDs];
|
|
74
|
+
let queryPayload, query;
|
|
75
|
+
let finalUsers = usersToModify;
|
|
76
|
+
|
|
77
|
+
ctx.wsReqNumber = (ctx.wsReqNumber || 0) + 1;
|
|
78
|
+
ctx.wsTaskNumber = (ctx.wsTaskNumber || 0) + 1;
|
|
79
|
+
|
|
80
|
+
if (action === 'add') {
|
|
81
|
+
const usersToAdd = usersToModify.filter(id => !currentMembers.includes(id));
|
|
82
|
+
if (usersToAdd.length === 0) {
|
|
83
|
+
_callback(null, { type: "error_gc", error: "All specified users are already in the group." });
|
|
84
|
+
return returnPromise;
|
|
85
|
+
}
|
|
86
|
+
finalUsers = usersToAdd;
|
|
87
|
+
queryPayload = { thread_key: parseInt(threadID), contact_ids: finalUsers.map(id => parseInt(id)), sync_group: 1 };
|
|
88
|
+
query = { label: "23", payload: JSON.stringify(queryPayload), queue_name: threadID, task_id: ctx.wsTaskNumber };
|
|
89
|
+
|
|
90
|
+
} else { // action is 'remove'
|
|
91
|
+
const userToRemove = usersToModify[0];
|
|
92
|
+
if (!currentMembers.includes(userToRemove)) {
|
|
93
|
+
_callback(null, { type: "error_gc", error: `User with ID ${userToRemove} is not in this group chat.` });
|
|
94
|
+
return returnPromise;
|
|
95
|
+
}
|
|
96
|
+
finalUsers = [userToRemove];
|
|
97
|
+
queryPayload = { thread_id: threadID, contact_id: userToRemove, sync_group: 1 };
|
|
98
|
+
query = { label: "140", payload: JSON.stringify(queryPayload), queue_name: "remove_participant_v2", task_id: ctx.wsTaskNumber };
|
|
132
99
|
}
|
|
133
100
|
|
|
134
|
-
// removeUserFromGroup: app_id "2220391788200892", version "25002366262773827" (dongdev confirmed)
|
|
135
|
-
// Fire-and-forget (no ACK needed) — matches dongdev's publishRealtimeMessage pattern
|
|
136
101
|
const context = {
|
|
137
|
-
app_id:
|
|
138
|
-
payload:
|
|
139
|
-
|
|
140
|
-
tasks: [{
|
|
141
|
-
failure_count: null,
|
|
142
|
-
label: "140",
|
|
143
|
-
payload: JSON.stringify({
|
|
144
|
-
thread_id: threadID,
|
|
145
|
-
contact_id: userToRemove,
|
|
146
|
-
sync_group: 1
|
|
147
|
-
}),
|
|
148
|
-
queue_name: "remove_participant_v2",
|
|
149
|
-
task_id: Math.floor(Math.random() * 1001)
|
|
150
|
-
}],
|
|
151
|
-
version_id: "25002366262773827"
|
|
152
|
-
}),
|
|
153
|
-
request_id: ++ctx.wsReqNumber,
|
|
102
|
+
app_id: ctx.appID || '2220391788200892',
|
|
103
|
+
payload: { epoch_id: parseInt(utils.generateOfflineThreadingID()), tasks: [query], version_id: "24631415369801570" },
|
|
104
|
+
request_id: ctx.wsReqNumber,
|
|
154
105
|
type: 3
|
|
155
106
|
};
|
|
107
|
+
context.payload = JSON.stringify(context.payload);
|
|
156
108
|
|
|
157
109
|
ctx.mqttClient.publish('/ls_req', JSON.stringify(context), { qos: 1, retain: false }, (err) => {
|
|
158
110
|
if (err) return _callback(err);
|
|
159
|
-
|
|
111
|
+
|
|
112
|
+
const gcmemberInfo = {
|
|
160
113
|
type: "gc_member_update",
|
|
161
|
-
threadID
|
|
162
|
-
|
|
163
|
-
|
|
114
|
+
threadID: threadID,
|
|
115
|
+
userIDs: finalUsers,
|
|
116
|
+
action: action,
|
|
117
|
+
senderID: ctx.userID,
|
|
118
|
+
BotID: ctx.userID,
|
|
119
|
+
timestamp: Date.now(),
|
|
120
|
+
};
|
|
121
|
+
return _callback(null, gcmemberInfo);
|
|
164
122
|
});
|
|
165
|
-
}
|
|
166
|
-
|
|
167
123
|
} catch (err) {
|
|
168
|
-
|
|
169
|
-
|
|
124
|
+
utils.error("gcmember", err);
|
|
125
|
+
_callback(err instanceof Error ? err : new Error(err.message || "An unknown error occurred."));
|
|
170
126
|
}
|
|
171
127
|
|
|
172
128
|
return returnPromise;
|
package/src/apis/gcname.js
CHANGED
package/src/apis/gcrule.js
CHANGED
|
@@ -74,33 +74,36 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
74
74
|
}
|
|
75
75
|
|
|
76
76
|
const isAdminStatus = action === 'admin' ? 1 : 0;
|
|
77
|
-
|
|
77
|
+
ctx.wsReqNumber = (ctx.wsReqNumber || 0) + 1;
|
|
78
|
+
ctx.wsTaskNumber = (ctx.wsTaskNumber || 0) + 1;
|
|
78
79
|
|
|
79
|
-
|
|
80
|
+
const queryPayload = {
|
|
81
|
+
thread_key: parseInt(threadID),
|
|
82
|
+
contact_id: parseInt(userID),
|
|
83
|
+
is_admin: isAdminStatus
|
|
84
|
+
};
|
|
85
|
+
const query = {
|
|
86
|
+
failure_count: null,
|
|
87
|
+
label: "25",
|
|
88
|
+
payload: JSON.stringify(queryPayload),
|
|
89
|
+
queue_name: "admin_status",
|
|
90
|
+
task_id: ctx.wsTaskNumber
|
|
91
|
+
};
|
|
80
92
|
const context = {
|
|
81
|
-
app_id: '2220391788200892',
|
|
82
|
-
payload:
|
|
93
|
+
app_id: ctx.appID || '2220391788200892',
|
|
94
|
+
payload: {
|
|
83
95
|
epoch_id: parseInt(utils.generateOfflineThreadingID()),
|
|
84
|
-
tasks: [
|
|
85
|
-
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
thread_key: threadID,
|
|
89
|
-
contact_id: userID,
|
|
90
|
-
is_admin: isAdminStatus
|
|
91
|
-
}),
|
|
92
|
-
queue_name: "admin_status",
|
|
93
|
-
task_id: 1
|
|
94
|
-
}],
|
|
95
|
-
version_id: "8798795233522156"
|
|
96
|
-
}),
|
|
97
|
-
request_id: ++ctx.wsReqNumber,
|
|
96
|
+
tasks: [query],
|
|
97
|
+
version_id: "24631415369801570"
|
|
98
|
+
},
|
|
99
|
+
request_id: ctx.wsReqNumber,
|
|
98
100
|
type: 3
|
|
99
101
|
};
|
|
102
|
+
context.payload = JSON.stringify(context.payload);
|
|
100
103
|
|
|
101
104
|
ctx.mqttClient.publish('/ls_req', JSON.stringify(context), { qos: 1, retain: false }, (err) => {
|
|
102
105
|
if (err) return _callback(err);
|
|
103
|
-
|
|
106
|
+
const gcruleInfo = {
|
|
104
107
|
type: "gc_rule_update",
|
|
105
108
|
threadID: threadID,
|
|
106
109
|
userID: userID,
|
|
@@ -108,7 +111,8 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
108
111
|
senderID: ctx.userID,
|
|
109
112
|
BotID: ctx.userID,
|
|
110
113
|
timestamp: Date.now(),
|
|
111
|
-
}
|
|
114
|
+
};
|
|
115
|
+
return _callback(null, gcruleInfo);
|
|
112
116
|
});
|
|
113
117
|
|
|
114
118
|
} catch (err) {
|
package/src/apis/listenMqtt.js
CHANGED
|
@@ -107,12 +107,6 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
|
|
|
107
107
|
const sessionID = Math.floor(Math.random() * Number.MAX_SAFE_INTEGER) + 1;
|
|
108
108
|
const cid = ctx.clientID;
|
|
109
109
|
const cachedUA = ctx.globalOptions.cachedUserAgent || ctx.globalOptions.userAgent;
|
|
110
|
-
// Use app_id from Facebook's own page data (populated by buildAPI) when
|
|
111
|
-
// available. Hard-coded fallback is the Messenger web app ID.
|
|
112
|
-
const rawAppId = ctx.appID || ctx.mqttAppID || ctx.userAppID;
|
|
113
|
-
// Coerce to number safely — Number() of a non-numeric string yields NaN which
|
|
114
|
-
// serialises as null in JSON and breaks the MQTT CONNECT packet.
|
|
115
|
-
const mqttAid = rawAppId && !isNaN(Number(rawAppId)) ? Number(rawAppId) : 219994525426954;
|
|
116
110
|
const username = {
|
|
117
111
|
u: ctx.userID,
|
|
118
112
|
s: sessionID,
|
|
@@ -120,7 +114,7 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
|
|
|
120
114
|
fg: false,
|
|
121
115
|
d: cid,
|
|
122
116
|
ct: 'websocket',
|
|
123
|
-
aid:
|
|
117
|
+
aid: 219994525426954,
|
|
124
118
|
aids: null,
|
|
125
119
|
mqtt_sid: '',
|
|
126
120
|
cp: 3,
|
package/src/apis/nickname.js
CHANGED
|
@@ -94,7 +94,7 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
94
94
|
});
|
|
95
95
|
|
|
96
96
|
const envelope = {
|
|
97
|
-
app_id: "2220391788200892",
|
|
97
|
+
app_id: String(ctx.appID || ctx.mqttAppID || "2220391788200892"),
|
|
98
98
|
payload: JSON.stringify({
|
|
99
99
|
epoch_id: generateEpochId(),
|
|
100
100
|
tasks: [task],
|
package/src/apis/pinMessage.js
CHANGED
|
@@ -54,7 +54,7 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
54
54
|
|
|
55
55
|
const epoch_id = parseInt(utils.generateOfflineThreadingID());
|
|
56
56
|
const version_id = "9523201934447612";
|
|
57
|
-
const app_id = "2220391788200892";
|
|
57
|
+
const app_id = String(ctx.appID || ctx.mqttAppID || "2220391788200892");
|
|
58
58
|
|
|
59
59
|
const createMqttRequest = (tasks, increment = 0) => ({
|
|
60
60
|
app_id,
|
package/src/apis/sendMessage.js
CHANGED
|
@@ -166,7 +166,7 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
166
166
|
const mention = msg.mentions[i];
|
|
167
167
|
const tag = mention.tag;
|
|
168
168
|
if (typeof tag !== "string") throw new Error("Mention tags must be strings.");
|
|
169
|
-
const offset =
|
|
169
|
+
const offset = msg.body.indexOf(tag, mention.fromIndex || 0);
|
|
170
170
|
if (offset < 0) utils.warn("handleMention", 'Mention for "' + tag + '" not found in message string.');
|
|
171
171
|
const id = mention.id || 0;
|
|
172
172
|
const emptyChar = '\u200E';
|
|
@@ -285,84 +285,77 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
285
285
|
return callback(new Error("Dissallowed props: `" + disallowedProperties.join(", ") + "`"));
|
|
286
286
|
}
|
|
287
287
|
|
|
288
|
+
// Declare typing state here so the finally block never hits a ReferenceError
|
|
289
|
+
// in strict mode. These are set below if simulateTyping is enabled.
|
|
290
|
+
let typingTimeout = null;
|
|
291
|
+
let typingStarted = false;
|
|
292
|
+
|
|
288
293
|
try {
|
|
289
|
-
//
|
|
290
|
-
//
|
|
291
|
-
//
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
294
|
+
// Simulate human typing delay when the option is enabled and there is
|
|
295
|
+
// text to type. The indicator is sent first, we wait the computed delay,
|
|
296
|
+
// then the actual send follows. The finally block stops the indicator.
|
|
297
|
+
if (ctx.globalOptions.simulateTyping && msg.body && typeof api.sendTypingIndicator === 'function') {
|
|
298
|
+
try {
|
|
299
|
+
const typingDelay = await globalAntiSuspension.simulateTyping(threadID, msg.body.length);
|
|
300
|
+
await api.sendTypingIndicator(true, threadID);
|
|
301
|
+
typingStarted = true;
|
|
302
|
+
await new Promise(resolve => {
|
|
303
|
+
typingTimeout = setTimeout(resolve, typingDelay);
|
|
304
|
+
});
|
|
305
|
+
} catch (_) {
|
|
306
|
+
// Typing simulation is best-effort — a failure here must never
|
|
307
|
+
// block the actual message send.
|
|
308
|
+
}
|
|
302
309
|
}
|
|
303
310
|
|
|
304
|
-
const mqttReady = !!(ctx.mqttClient && ctx.mqttClient.connected);
|
|
305
|
-
const isMultiRecipient = Array.isArray(threadID);
|
|
306
|
-
// Primary transport: MQTT when connected (faster, lower overhead).
|
|
307
|
-
// Multi-recipient sends always use HTTP (MQTT only handles single thread).
|
|
308
|
-
const preferMqtt = mqttReady && !isMultiRecipient && !!api.sendMessageMqtt;
|
|
309
|
-
|
|
310
|
-
let result;
|
|
311
|
-
let primaryErr;
|
|
312
|
-
let primaryOk = false;
|
|
313
|
-
|
|
314
311
|
try {
|
|
315
|
-
|
|
312
|
+
const mqttReady = ctx.mqttClient && ctx.mqttClient.connected;
|
|
313
|
+
const isMultiRecipient = Array.isArray(threadID);
|
|
314
|
+
// Track which transport was used so the catch block can fall back to
|
|
315
|
+
// the OTHER transport — retrying the same one that just failed is useless.
|
|
316
|
+
const usedMqtt = mqttReady && !isMultiRecipient && api.sendMessageMqtt;
|
|
317
|
+
|
|
318
|
+
let result;
|
|
319
|
+
if (usedMqtt) {
|
|
316
320
|
result = await api.sendMessageMqtt(msg, threadID, replyToMessage);
|
|
317
321
|
} else {
|
|
318
322
|
result = await sendViaHttp(msg, threadID, replyToMessage, isGroup);
|
|
319
323
|
}
|
|
320
|
-
|
|
321
|
-
} catch (
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
if (primaryOk) {
|
|
326
|
-
return callback(null, result);
|
|
327
|
-
}
|
|
324
|
+
callback(null, result);
|
|
325
|
+
} catch (sendErr) {
|
|
326
|
+
// Fall back to the OTHER transport, not the one that just failed.
|
|
327
|
+
const mqttNowReady = ctx.mqttClient && ctx.mqttClient.connected;
|
|
328
|
+
const primaryWasMqtt = mqttNowReady && !Array.isArray(threadID) && api.sendMessageMqtt;
|
|
328
329
|
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
let fallbackErr;
|
|
332
|
-
|
|
333
|
-
if (preferMqtt) {
|
|
334
|
-
// MQTT was primary → fall back to HTTP
|
|
335
|
-
try {
|
|
336
|
-
result = await sendViaHttp(msg, threadID, replyToMessage, isGroup);
|
|
337
|
-
fallbackOk = true;
|
|
338
|
-
} catch (e) {
|
|
339
|
-
fallbackErr = e;
|
|
340
|
-
}
|
|
341
|
-
} else {
|
|
342
|
-
// HTTP was primary → fall back to MQTT if available
|
|
343
|
-
const mqttNow = !!(ctx.mqttClient && ctx.mqttClient.connected);
|
|
344
|
-
if (mqttNow && !isMultiRecipient && api.sendMessageMqtt) {
|
|
330
|
+
if (primaryWasMqtt) {
|
|
331
|
+
// MQTT was used (or is available) — fall back to HTTP
|
|
345
332
|
try {
|
|
346
|
-
|
|
347
|
-
|
|
348
|
-
} catch (
|
|
349
|
-
|
|
333
|
+
const httpRes = await sendViaHttp(msg, threadID, replyToMessage, isGroup);
|
|
334
|
+
callback(null, httpRes);
|
|
335
|
+
} catch (_httpErr) {
|
|
336
|
+
callback(sendErr);
|
|
350
337
|
}
|
|
351
338
|
} else {
|
|
352
|
-
|
|
339
|
+
// HTTP was used — try MQTT if it has since become available
|
|
340
|
+
if (mqttNowReady && !Array.isArray(threadID) && api.sendMessageMqtt) {
|
|
341
|
+
try {
|
|
342
|
+
const mqttRes = await api.sendMessageMqtt(msg, threadID, replyToMessage);
|
|
343
|
+
callback(null, mqttRes);
|
|
344
|
+
} catch (_mqttErr) {
|
|
345
|
+
callback(sendErr);
|
|
346
|
+
}
|
|
347
|
+
} else {
|
|
348
|
+
callback(sendErr);
|
|
349
|
+
}
|
|
353
350
|
}
|
|
354
|
-
}
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
const err = primaryErr;
|
|
362
|
-
if (err && fallbackErr && fallbackErr !== primaryErr) {
|
|
363
|
-
err.fallbackError = fallbackErr;
|
|
351
|
+
} finally {
|
|
352
|
+
// Stop typing indicator regardless of success or failure.
|
|
353
|
+
// typingTimeout and typingStarted are always declared above so this
|
|
354
|
+
// block can never throw a ReferenceError in strict mode.
|
|
355
|
+
if (typingTimeout) clearTimeout(typingTimeout);
|
|
356
|
+
if (typingStarted) {
|
|
357
|
+
try { await api.sendTypingIndicator(false, threadID); } catch (_) {}
|
|
364
358
|
}
|
|
365
|
-
return callback(err || fallbackErr);
|
|
366
359
|
}
|
|
367
360
|
} catch (err) {
|
|
368
361
|
callback(err);
|
|
@@ -142,22 +142,10 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
142
142
|
replyToMessage = null;
|
|
143
143
|
}
|
|
144
144
|
|
|
145
|
-
// Anti-suspension preparation (circuit-breaker check, smart delay, thread
|
|
146
|
-
// throttling, and daily-stat tracking) is the responsibility of the CALLER.
|
|
147
|
-
// When invoked through api.sendMessage(), preparation is already done there
|
|
148
|
-
// for ALL transport paths (both MQTT and HTTP).
|
|
149
|
-
//
|
|
150
|
-
// If you call api.sendMessageMqtt() directly (bypassing api.sendMessage()),
|
|
151
|
-
// call globalAntiSuspension.prepareBeforeMessage() yourself first and handle
|
|
152
|
-
// the circuit-breaker error it may throw — otherwise your bot has no protection.
|
|
153
|
-
|
|
154
145
|
const normalized = typeof msg === "string" ? { body: msg } : msg;
|
|
155
146
|
const baseBody = normalized.body != null ? String(normalized.body) : "";
|
|
156
147
|
const epoch = (BigInt(Date.now()) << 22n).toString();
|
|
157
|
-
|
|
158
|
-
// request_id, which would cause the ACK listener to match the wrong response.
|
|
159
|
-
if (typeof ctx.wsReqNumber !== "number") ctx.wsReqNumber = 0;
|
|
160
|
-
const requestId = ++ctx.wsReqNumber;
|
|
148
|
+
const requestId = Math.floor(100 + Math.random() * 900);
|
|
161
149
|
const otid = utils.generateOfflineThreadingID();
|
|
162
150
|
|
|
163
151
|
const payload0 = {
|
|
@@ -229,12 +217,6 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
229
217
|
payload0.attachment_fbids = files.map(f => String(Object.values(f)[0]));
|
|
230
218
|
}
|
|
231
219
|
|
|
232
|
-
// Use monotonic task IDs so concurrent sends never reuse the same task_id
|
|
233
|
-
// within a session, which can confuse Facebook's MQTT deduplication.
|
|
234
|
-
if (typeof ctx.wsTaskNumber !== "number") ctx.wsTaskNumber = 0;
|
|
235
|
-
const taskId1 = ++ctx.wsTaskNumber;
|
|
236
|
-
const taskId2 = ++ctx.wsTaskNumber;
|
|
237
|
-
|
|
238
220
|
const content = {
|
|
239
221
|
app_id: "2220391788200892",
|
|
240
222
|
payload: {
|
|
@@ -243,7 +225,7 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
243
225
|
label: "46",
|
|
244
226
|
payload: payload0,
|
|
245
227
|
queue_name: String(threadID),
|
|
246
|
-
task_id:
|
|
228
|
+
task_id: 400,
|
|
247
229
|
failure_count: null,
|
|
248
230
|
},
|
|
249
231
|
{
|
|
@@ -254,7 +236,7 @@ module.exports = (defaultFuncs, api, ctx) => {
|
|
|
254
236
|
sync_group: 1,
|
|
255
237
|
},
|
|
256
238
|
queue_name: String(threadID),
|
|
257
|
-
task_id:
|
|
239
|
+
task_id: 401,
|
|
258
240
|
failure_count: null,
|
|
259
241
|
},
|
|
260
242
|
],
|
|
@@ -47,17 +47,16 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
47
47
|
});
|
|
48
48
|
|
|
49
49
|
const messageDefs = [
|
|
50
|
-
{ label: 1013, queue: ['ai_generated_theme', String(threadID)]
|
|
51
|
-
{ label: 1037, queue: ['msgr_custom_thread_theme', String(threadID)]
|
|
52
|
-
{ label: 1028, queue: ['thread_theme_writer', String(threadID)]
|
|
53
|
-
|
|
54
|
-
{ label: 43, queue: 'thread_theme', extra: { source: null, payload: null }, app_id: '2220391788200892' },
|
|
50
|
+
{ label: 1013, queue: ['ai_generated_theme', String(threadID)] },
|
|
51
|
+
{ label: 1037, queue: ['msgr_custom_thread_theme', String(threadID)] },
|
|
52
|
+
{ label: 1028, queue: ['thread_theme_writer', String(threadID)] },
|
|
53
|
+
{ label: 43, queue: 'thread_theme', extra: { source: null, payload: null } },
|
|
55
54
|
];
|
|
56
55
|
|
|
57
|
-
const messages = messageDefs.map(({ label, queue, extra
|
|
56
|
+
const messages = messageDefs.map(({ label, queue, extra }) => {
|
|
58
57
|
ctx.wsReqNumber += 1;
|
|
59
58
|
return {
|
|
60
|
-
app_id,
|
|
59
|
+
app_id: '772021112871879',
|
|
61
60
|
payload: JSON.stringify({
|
|
62
61
|
epoch_id: parseInt(utils.generateOfflineThreadingID()),
|
|
63
62
|
tasks: [makeTask(label, queue, extra)],
|
package/src/apis/theme.js
CHANGED
|
@@ -150,11 +150,11 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
150
150
|
};
|
|
151
151
|
|
|
152
152
|
const context = {
|
|
153
|
-
app_id:
|
|
153
|
+
app_id: ctx.appID,
|
|
154
154
|
payload: {
|
|
155
155
|
epoch_id: currentEpochId,
|
|
156
156
|
tasks: [query],
|
|
157
|
-
version_id: '
|
|
157
|
+
version_id: '24631415369801570',
|
|
158
158
|
},
|
|
159
159
|
request_id: request_id,
|
|
160
160
|
type: 3,
|
|
@@ -33,7 +33,7 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
33
33
|
const taskId = ++ctx.wsTaskNumber;
|
|
34
34
|
|
|
35
35
|
const content = {
|
|
36
|
-
app_id: "2220391788200892",
|
|
36
|
+
app_id: String(ctx.appID || ctx.mqttAppID || "2220391788200892"),
|
|
37
37
|
payload: JSON.stringify({
|
|
38
38
|
data_trace_id: null,
|
|
39
39
|
epoch_id: parseInt(utils.generateOfflineThreadingID()),
|
package/src/engine/client.js
CHANGED
|
@@ -26,7 +26,7 @@ const DEFAULT_OPTIONS = {
|
|
|
26
26
|
autoReconnect: true,
|
|
27
27
|
online: true,
|
|
28
28
|
emitReady: false,
|
|
29
|
-
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
29
|
+
userAgent: "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.7499.182 Safari/537.36",
|
|
30
30
|
};
|
|
31
31
|
|
|
32
32
|
/**
|
|
@@ -459,35 +459,6 @@ class AntiSuspension {
|
|
|
459
459
|
return results;
|
|
460
460
|
}
|
|
461
461
|
|
|
462
|
-
/**
|
|
463
|
-
* Prepare before sending — single delay model.
|
|
464
|
-
* Enforces smart delay, thread throttle, and volume limits, respects circuit breaker.
|
|
465
|
-
* If the circuit breaker is tripped (checkpoint/suspension detected) or
|
|
466
|
-
* volume limits are reached, throws to protect the account.
|
|
467
|
-
*/
|
|
468
|
-
async prepareBeforeMessage(threadID, message) {
|
|
469
|
-
if (this.isCircuitBreakerTripped()) {
|
|
470
|
-
const remaining = this.getCircuitBreakerRemainingMs();
|
|
471
|
-
const { utils } = this._getUtils();
|
|
472
|
-
utils && utils.warn && utils.warn("AntiSuspension",
|
|
473
|
-
`Circuit breaker is tripped. Blocking message send to protect account. ` +
|
|
474
|
-
`Cooldown remaining: ${Math.ceil(remaining / 1000)}s`);
|
|
475
|
-
const err = new Error(`Circuit breaker is tripped. Pausing sends to protect account. Retry in ${Math.ceil(remaining / 1000)}s.`);
|
|
476
|
-
err.error = 'circuit_breaker_tripped';
|
|
477
|
-
err.remainingMs = remaining;
|
|
478
|
-
throw err;
|
|
479
|
-
}
|
|
480
|
-
|
|
481
|
-
const volumeWarning = this.checkVolumeLimit(threadID);
|
|
482
|
-
if (volumeWarning) {
|
|
483
|
-
const { utils } = this._getUtils();
|
|
484
|
-
utils && utils.warn && utils.warn("AntiSuspension", volumeWarning);
|
|
485
|
-
}
|
|
486
|
-
|
|
487
|
-
await this.enforceThreadThrottling(threadID);
|
|
488
|
-
this._incrementDailyStats(threadID);
|
|
489
|
-
}
|
|
490
|
-
|
|
491
462
|
getConfig() {
|
|
492
463
|
return {
|
|
493
464
|
messageDelayMs: this.messageDelayMs,
|
package/src/utils/autoReLogin.js
CHANGED
|
@@ -205,10 +205,7 @@ class AutoReLoginManager {
|
|
|
205
205
|
|
|
206
206
|
async pauseAPIRequests() {
|
|
207
207
|
utils.log("AutoReLogin", "Pausing API requests during re-login...");
|
|
208
|
-
|
|
209
|
-
// re-logging in. A 1s pause was too short — the session cookies could
|
|
210
|
-
// change mid-request, causing follow-up requests to arrive with stale auth.
|
|
211
|
-
await new Promise(resolve => setTimeout(resolve, 3000));
|
|
208
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
212
209
|
}
|
|
213
210
|
|
|
214
211
|
resolvePendingRequests(success) {
|
package/src/utils/headers.js
CHANGED
|
@@ -28,9 +28,6 @@ function sanitizeHeaders(headers) {
|
|
|
28
28
|
const out = {};
|
|
29
29
|
for (const [key, value] of Object.entries(headers)) {
|
|
30
30
|
if (!key || typeof key !== 'string') continue;
|
|
31
|
-
// Skip undefined/null values — these are intentional omissions (e.g.
|
|
32
|
-
// 'Upgrade-Insecure-Requests' on XHR requests that browsers don't send).
|
|
33
|
-
if (value === undefined || value === null) continue;
|
|
34
31
|
const cleanKey = key.replace(/[^\x21-\x7E]/g, '').trim();
|
|
35
32
|
if (!cleanKey) continue;
|
|
36
33
|
const cleanVal = sanitizeHeaderValue(value);
|
|
@@ -62,16 +59,28 @@ function getRandomTimezone() {
|
|
|
62
59
|
|
|
63
60
|
/**
|
|
64
61
|
* Generates a comprehensive and realistic set of headers for requests to Facebook.
|
|
62
|
+
*
|
|
63
|
+
* Key fingerprint rules enforced here:
|
|
64
|
+
* - Navigate (page load): Accept = full HTML accept, Upgrade-Insecure-Requests = 1,
|
|
65
|
+
* Sec-Fetch-Dest/Mode/Site = document/navigate/none. NO X-Requested-With, NO Origin.
|
|
66
|
+
* - XHR (AJAX/API call): Accept = *\/*, Sec-Fetch-Dest/Mode/Site = empty/cors/same-origin,
|
|
67
|
+
* Origin = host. NO Upgrade-Insecure-Requests.
|
|
68
|
+
*
|
|
69
|
+
* Real Chrome never sends X-Requested-With automatically — it is a jQuery/XHR
|
|
70
|
+
* legacy header that Facebook's own modern client does NOT include and that
|
|
71
|
+
* Facebook's bot-detection system flags when seen on navigation requests.
|
|
72
|
+
*
|
|
65
73
|
* @param {string} url - The target URL.
|
|
66
74
|
* @param {object} options - Global options from context.
|
|
67
75
|
* @param {object} ctx - The application context (containing fb_dtsg, lsd, etc.).
|
|
68
76
|
* @param {object} customHeader - Any extra headers to merge.
|
|
69
|
-
* @param {string} requestType -
|
|
77
|
+
* @param {string} requestType - 'xhr' for GraphQL/AJAX or 'navigate' for page navigation.
|
|
70
78
|
* @returns {object} A complete headers object.
|
|
71
79
|
*/
|
|
72
80
|
function getHeaders(url, options, ctx, customHeader, requestType = 'navigate') {
|
|
73
81
|
const persona = options?.persona || 'desktop';
|
|
74
82
|
const isAndroid = persona === 'android' || persona === 'mobile';
|
|
83
|
+
const isXhr = requestType === 'xhr';
|
|
75
84
|
|
|
76
85
|
let userAgent, secChUa, secChUaFullVersionList, secChUaPlatform, secChUaPlatformVersion;
|
|
77
86
|
let androidData = null;
|
|
@@ -113,8 +122,6 @@ function getHeaders(url, options, ctx, customHeader, requestType = 'navigate') {
|
|
|
113
122
|
const host = new URL(url).hostname;
|
|
114
123
|
const referer = `https://${host}/`;
|
|
115
124
|
|
|
116
|
-
const isXhr = requestType === 'xhr';
|
|
117
|
-
|
|
118
125
|
const locales = options?.cachedLocale || (androidData?.locale ? androidData.locale.replace('_', '-') : getRandomLocale());
|
|
119
126
|
|
|
120
127
|
if (isAndroid) {
|
|
@@ -162,43 +169,55 @@ function getHeaders(url, options, ctx, customHeader, requestType = 'navigate') {
|
|
|
162
169
|
const isMac = secChUaPlatform === '"macOS"';
|
|
163
170
|
const isLinux = secChUaPlatform === '"Linux"';
|
|
164
171
|
|
|
172
|
+
// Navigate (page load) Accept mirrors real Chrome — includes avif/webp/apng
|
|
173
|
+
// XHR Accept is simply */* as sent by Chrome's fetch/XHR APIs
|
|
174
|
+
const acceptHeader = isXhr
|
|
175
|
+
? '*/*'
|
|
176
|
+
: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7';
|
|
177
|
+
|
|
165
178
|
const headers = {
|
|
166
|
-
'Accept':
|
|
167
|
-
? 'application/json, text/plain, */*'
|
|
168
|
-
: 'text/html,application/xhtml+xml,application/xml;q=0.9,image/avif,image/webp,image/apng,*/*;q=0.8,application/signed-exchange;v=b3;q=0.7',
|
|
179
|
+
'Accept': acceptHeader,
|
|
169
180
|
'Accept-Language': locales,
|
|
170
181
|
'Accept-Encoding': 'gzip, deflate, br',
|
|
171
182
|
'Cache-Control': 'no-cache',
|
|
172
183
|
'Connection': 'keep-alive',
|
|
173
|
-
'DNT': '1',
|
|
174
184
|
'Host': host,
|
|
175
185
|
'Pragma': 'no-cache',
|
|
176
186
|
'Referer': referer,
|
|
177
187
|
'Sec-Ch-Ua': secChUa,
|
|
178
188
|
'Sec-Ch-Ua-Full-Version-List': secChUaFullVersionList,
|
|
179
189
|
'Sec-Ch-Ua-Mobile': '?0',
|
|
190
|
+
'Sec-Ch-Ua-Model': '""',
|
|
180
191
|
'Sec-Ch-Ua-Platform': secChUaPlatform,
|
|
181
192
|
'Sec-Ch-Ua-Platform-Version': secChUaPlatformVersion,
|
|
182
193
|
'Sec-Fetch-Dest': isXhr ? 'empty' : 'document',
|
|
183
194
|
'Sec-Fetch-Mode': isXhr ? 'cors' : 'navigate',
|
|
184
195
|
'Sec-Fetch-Site': isXhr ? 'same-origin' : 'none',
|
|
185
196
|
'User-Agent': userAgent,
|
|
186
|
-
'Upgrade-Insecure-Requests': isXhr ? undefined : '1',
|
|
187
|
-
// X-Requested-With is only sent by XHR calls, NOT by page navigations.
|
|
188
|
-
// Sending it on navigate requests is a clear bot-detection fingerprint
|
|
189
|
-
// because real browsers never include it for document loads.
|
|
190
|
-
...(isXhr ? { 'X-Requested-With': 'XMLHttpRequest' } : {})
|
|
191
197
|
};
|
|
192
198
|
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
199
|
+
// Upgrade-Insecure-Requests is a navigation-only hint — browsers never
|
|
200
|
+
// include it on XHR/fetch calls. Sending it on XHR is a bot fingerprint.
|
|
201
|
+
if (!isXhr) {
|
|
202
|
+
headers['Upgrade-Insecure-Requests'] = '1';
|
|
196
203
|
}
|
|
197
204
|
|
|
205
|
+
// Origin is only present on cross-origin or same-origin XHR/fetch requests.
|
|
206
|
+
// Real browsers do NOT send Origin on top-level page navigations (Sec-Fetch-Mode: navigate).
|
|
198
207
|
if (isXhr) {
|
|
199
208
|
headers['Origin'] = `https://${host}`;
|
|
200
209
|
}
|
|
201
210
|
|
|
211
|
+
// X-Requested-With is NOT a browser-native header — it is a jQuery/XHR
|
|
212
|
+
// convention that Facebook's own modern web client does not set.
|
|
213
|
+
// Sending it (especially on navigate requests) is a well-known bot fingerprint
|
|
214
|
+
// that Facebook's detection system keys on. We never send it.
|
|
215
|
+
|
|
216
|
+
if (isWindows || isMac || isLinux) {
|
|
217
|
+
headers['Sec-Ch-Ua-Arch'] = '"x86"';
|
|
218
|
+
headers['Sec-Ch-Ua-Bitness'] = '"64"';
|
|
219
|
+
}
|
|
220
|
+
|
|
202
221
|
if (ctx) {
|
|
203
222
|
if (ctx.lsd || ctx.fb_dtsg) {
|
|
204
223
|
headers['X-Fb-Lsd'] = ctx.lsd || ctx.fb_dtsg;
|
|
@@ -214,7 +233,6 @@ function getHeaders(url, options, ctx, customHeader, requestType = 'navigate') {
|
|
|
214
233
|
}
|
|
215
234
|
}
|
|
216
235
|
|
|
217
|
-
|
|
218
236
|
if (customHeader) {
|
|
219
237
|
Object.assign(headers, customHeader);
|
|
220
238
|
if (customHeader.noRef) {
|
|
@@ -232,4 +250,4 @@ module.exports = {
|
|
|
232
250
|
meta,
|
|
233
251
|
getRandomLocale,
|
|
234
252
|
getRandomTimezone
|
|
235
|
-
};
|
|
253
|
+
};
|
package/src/utils/lsRequest.js
CHANGED
|
@@ -122,7 +122,7 @@ function buildLsEnvelope(ctx, tasks, versionId = "25459622483894963") {
|
|
|
122
122
|
const requestId = ++ctx.wsReqNumber;
|
|
123
123
|
|
|
124
124
|
const envelope = {
|
|
125
|
-
app_id: "2220391788200892",
|
|
125
|
+
app_id: String(ctx.appID || ctx.mqttAppID || "2220391788200892"),
|
|
126
126
|
payload: JSON.stringify({
|
|
127
127
|
epoch_id: generateEpochId(),
|
|
128
128
|
tasks,
|
package/src/utils/rateLimiter.js
CHANGED
|
@@ -14,12 +14,8 @@ class RateLimiter {
|
|
|
14
14
|
|
|
15
15
|
this.ERROR_CACHE_TTL = 300000;
|
|
16
16
|
this.COOLDOWN_DURATION = 60000;
|
|
17
|
-
|
|
18
|
-
|
|
19
|
-
// etc.) and caused unnecessary delays that looked bot-like when many
|
|
20
|
-
// requests were queued. Facebook's actual rate-limiting kicks in far higher.
|
|
21
|
-
this.MAX_REQUESTS_PER_MINUTE = 120;
|
|
22
|
-
this.MAX_CONCURRENT_REQUESTS = 8;
|
|
17
|
+
this.MAX_REQUESTS_PER_MINUTE = 50;
|
|
18
|
+
this.MAX_CONCURRENT_REQUESTS = 5;
|
|
23
19
|
|
|
24
20
|
this.activeRequests = 0;
|
|
25
21
|
|
|
@@ -29,9 +25,7 @@ class RateLimiter {
|
|
|
29
25
|
|
|
30
26
|
// Per-endpoint sliding windows
|
|
31
27
|
this._endpointWindows = new Map();
|
|
32
|
-
|
|
33
|
-
// burst-write scenarios (e.g. thread hydration after reconnect) unnecessarily.
|
|
34
|
-
this._MAX_PER_ENDPOINT_PER_MINUTE = 40;
|
|
28
|
+
this._MAX_PER_ENDPOINT_PER_MINUTE = 20;
|
|
35
29
|
}
|
|
36
30
|
|
|
37
31
|
configure(opts = {}) {
|
package/src/utils/user-agents.js
CHANGED
|
@@ -1,47 +1,81 @@
|
|
|
1
1
|
"use strict";
|
|
2
2
|
const { getRandom } = require("./constants");
|
|
3
3
|
|
|
4
|
+
// Chrome versions ordered newest-first. Keep this list current — Facebook's
|
|
5
|
+
// bot-detection compares claimed UA versions against known release timelines.
|
|
6
|
+
// Versions that are too old (pre-120) or too new (unreleased) are flagged.
|
|
4
7
|
const BROWSER_DATA = {
|
|
5
8
|
windows: {
|
|
6
9
|
platform: "Windows NT 10.0; Win64; x64",
|
|
7
|
-
// Ordered from newest to oldest so random selection skews toward current builds
|
|
8
10
|
chromeVersions: [
|
|
9
|
-
"
|
|
10
|
-
"
|
|
11
|
+
"143.0.7499.182", "142.0.7410.114", "141.0.7358.100",
|
|
12
|
+
"140.0.7294.114", "139.0.7258.100", "138.0.7204.92",
|
|
13
|
+
"137.0.7151.68", "136.0.7103.113", "135.0.7049.96",
|
|
14
|
+
"134.0.6998.165"
|
|
11
15
|
],
|
|
12
16
|
edgeVersions: [
|
|
13
|
-
"
|
|
14
|
-
"
|
|
17
|
+
"143.0.7499.182", "142.0.7410.114", "141.0.7358.100",
|
|
18
|
+
"140.0.7294.114", "139.0.7258.100"
|
|
15
19
|
],
|
|
16
20
|
platformVersion: '"15.0.0"'
|
|
17
21
|
},
|
|
18
22
|
mac: {
|
|
19
23
|
platform: "Macintosh; Intel Mac OS X 10_15_7",
|
|
20
24
|
chromeVersions: [
|
|
21
|
-
"
|
|
22
|
-
"
|
|
25
|
+
"143.0.7499.182", "142.0.7410.114", "141.0.7358.100",
|
|
26
|
+
"140.0.7294.114", "139.0.7258.100", "138.0.7204.92",
|
|
27
|
+
"137.0.7151.68", "136.0.7103.113", "135.0.7049.96",
|
|
28
|
+
"134.0.6998.165"
|
|
23
29
|
],
|
|
24
30
|
edgeVersions: [
|
|
25
|
-
"
|
|
26
|
-
"
|
|
31
|
+
"143.0.7499.182", "142.0.7410.114", "141.0.7358.100",
|
|
32
|
+
"140.0.7294.114", "139.0.7258.100"
|
|
27
33
|
],
|
|
28
|
-
platformVersion: '"14.7.
|
|
34
|
+
platformVersion: '"14.7.0"'
|
|
29
35
|
},
|
|
30
36
|
linux: {
|
|
31
37
|
platform: "X11; Linux x86_64",
|
|
32
38
|
chromeVersions: [
|
|
33
|
-
"
|
|
34
|
-
"
|
|
39
|
+
"143.0.7499.182", "142.0.7410.114", "141.0.7358.100",
|
|
40
|
+
"140.0.7294.114", "139.0.7258.100", "138.0.7204.92",
|
|
41
|
+
"137.0.7151.68", "136.0.7103.113"
|
|
35
42
|
],
|
|
36
43
|
edgeVersions: [
|
|
37
|
-
"
|
|
38
|
-
"
|
|
44
|
+
"143.0.7499.182", "142.0.7410.114", "141.0.7358.100",
|
|
45
|
+
"140.0.7294.114"
|
|
39
46
|
],
|
|
40
47
|
platformVersion: '""'
|
|
41
48
|
}
|
|
42
49
|
};
|
|
43
50
|
|
|
44
|
-
const defaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/
|
|
51
|
+
const defaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/143.0.7499.182 Safari/537.36";
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Builds the correct GREASE token for a given major Chrome version.
|
|
55
|
+
*
|
|
56
|
+
* Chrome picks one of several "not a brand" placeholder strings via a
|
|
57
|
+
* deterministic algorithm keyed to the major version. Using the wrong
|
|
58
|
+
* GREASE value is a detectable fingerprint mismatch.
|
|
59
|
+
*
|
|
60
|
+
* The set used by Chrome 100+ rotates through these four forms based on
|
|
61
|
+
* (major % 4):
|
|
62
|
+
* 0 → "Not)A;Brand"
|
|
63
|
+
* 1 → "Not A(Brand"
|
|
64
|
+
* 2 → "Not;A=Brand"
|
|
65
|
+
* 3 → "Not A Brand" (older alias)
|
|
66
|
+
*
|
|
67
|
+
* @param {number} major - Chrome major version number.
|
|
68
|
+
* @returns {string} - GREASE brand string without the surrounding quotes.
|
|
69
|
+
*/
|
|
70
|
+
function greaseForVersion(major) {
|
|
71
|
+
const forms = [
|
|
72
|
+
"Not)A;Brand",
|
|
73
|
+
"Not A(Brand",
|
|
74
|
+
"Not;A=Brand",
|
|
75
|
+
"Not A Brand"
|
|
76
|
+
];
|
|
77
|
+
return forms[major % 4];
|
|
78
|
+
}
|
|
45
79
|
|
|
46
80
|
/**
|
|
47
81
|
* Generates a realistic, randomized User-Agent string and related Sec-CH headers.
|
|
@@ -52,32 +86,42 @@ function randomUserAgent() {
|
|
|
52
86
|
const os = getRandom(Object.keys(BROWSER_DATA));
|
|
53
87
|
const data = BROWSER_DATA[os];
|
|
54
88
|
|
|
55
|
-
const useEdge = Math.random() > 0.
|
|
89
|
+
const useEdge = Math.random() > 0.75 && data.edgeVersions && data.edgeVersions.length > 0;
|
|
56
90
|
const versions = useEdge ? data.edgeVersions : data.chromeVersions;
|
|
57
91
|
const version = getRandom(versions);
|
|
58
92
|
const majorVersion = version.split('.')[0];
|
|
93
|
+
const major = parseInt(majorVersion, 10);
|
|
59
94
|
const browserName = useEdge ? 'Microsoft Edge' : 'Google Chrome';
|
|
60
|
-
const browserIdentifier = useEdge ? 'Edg' : 'Chrome';
|
|
61
95
|
|
|
62
|
-
const userAgent = useEdge
|
|
96
|
+
const userAgent = useEdge
|
|
63
97
|
? `Mozilla/5.0 (${data.platform}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${version} Safari/537.36 Edg/${version}`
|
|
64
98
|
: `Mozilla/5.0 (${data.platform}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${version} Safari/537.36`;
|
|
65
99
|
|
|
66
|
-
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
71
|
-
|
|
72
|
-
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
100
|
+
// Use the version-correct GREASE placeholder — mismatched GREASE is a
|
|
101
|
+
// detectable fingerprint that Facebook's bot detection system checks.
|
|
102
|
+
const greaseStr = greaseForVersion(major);
|
|
103
|
+
const greaseVersion = "99";
|
|
104
|
+
|
|
105
|
+
let brands;
|
|
106
|
+
if (useEdge) {
|
|
107
|
+
brands = [
|
|
108
|
+
`"Chromium";v="${majorVersion}"`,
|
|
109
|
+
`"${browserName}";v="${majorVersion}"`,
|
|
110
|
+
`"${greaseStr}";v="${greaseVersion}"`
|
|
111
|
+
];
|
|
112
|
+
} else {
|
|
113
|
+
brands = [
|
|
114
|
+
`"${browserName}";v="${majorVersion}"`,
|
|
115
|
+
`"Chromium";v="${majorVersion}"`,
|
|
116
|
+
`"${greaseStr}";v="${greaseVersion}"`
|
|
117
|
+
];
|
|
118
|
+
}
|
|
76
119
|
|
|
77
120
|
const secChUa = brands.join(', ');
|
|
121
|
+
|
|
122
|
+
// Full version list replaces the major-only version with the full string
|
|
78
123
|
const secChUaFullVersionList = brands.map(b => {
|
|
79
|
-
|
|
80
|
-
if (match && match[1] === majorVersion) {
|
|
124
|
+
if (b.includes(`v="${majorVersion}"`)) {
|
|
81
125
|
return b.replace(`v="${majorVersion}"`, `v="${version}"`);
|
|
82
126
|
}
|
|
83
127
|
return b;
|
|
@@ -124,7 +168,7 @@ function randomFbav() {
|
|
|
124
168
|
}
|
|
125
169
|
|
|
126
170
|
function randomOrcaUA() {
|
|
127
|
-
const androidVersions = ["10", "11", "12", "13", "14"
|
|
171
|
+
const androidVersions = ["8.1.0", "9", "10", "11", "12", "13", "14"];
|
|
128
172
|
const devices = [
|
|
129
173
|
{ brand: "samsung", model: "SM-G996B" },
|
|
130
174
|
{ brand: "samsung", model: "SM-S908E" },
|
|
@@ -254,4 +298,4 @@ module.exports = {
|
|
|
254
298
|
randomOrcaUA,
|
|
255
299
|
generateUserAgentByPersona,
|
|
256
300
|
cachePersonaData,
|
|
257
|
-
};
|
|
301
|
+
};
|