@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 CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyneoaz/metachat",
3
- "version": "1.1.3",
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: "2220391788200892"
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
@@ -58,7 +58,7 @@ module.exports = (defaultFuncs, api, ctx) => {
58
58
  };
59
59
 
60
60
  const form = JSON.stringify({
61
- app_id: "2220391788200892",
61
+ app_id: "772021112871879",
62
62
  payload: JSON.stringify(payload),
63
63
  request_id: ++ctx.wsReqNumber,
64
64
  type: 3
@@ -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 messages = Array.isArray(messageOrMessages)
41
- ? messageOrMessages
42
- : [messageOrMessages];
43
-
44
- if (messages.length === 0 || messages.some(v => v === null || v === undefined || v === "")) {
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
- } else {
96
- // HTTP fallback
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: '8798795233522156',
99
+ version_id: '24631415369801570',
100
100
  },
101
101
  request_id: ctx.wsReqNumber,
102
102
  type: 3,
@@ -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
- * Adds or removes members from a group chat.
7
+ * Made by ChoruOfficial
8
+ * Mqtt
9
+ * Adds or removes members from a group chat with pre-checking.
9
10
  *
10
- * Add uses publishLsRequestWithAck (label "23", app_id "772021112871879",
11
- * version "24502707779384158") matches dongdev addUserToGroup.
12
- * Remove uses fire-and-forget publish (label "140", app_id "2220391788200892",
13
- * version "25002366262773827") — matches dongdev removeUserFromGroup.
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
- _callback = threadID;
24
- threadID = null;
20
+ _callback = threadID;
21
+ threadID = null;
25
22
  } else if (typeof callback === 'function') {
26
- _callback = callback;
23
+ _callback = callback;
27
24
  }
28
-
25
+
29
26
  let resolvePromise, rejectPromise;
30
27
  const returnPromise = new Promise((resolve, reject) => {
31
- resolvePromise = resolve;
32
- rejectPromise = reject;
28
+ resolvePromise = resolve;
29
+ rejectPromise = reject;
33
30
  });
34
31
 
35
- if (typeof _callback !== "function") {
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
- const validActions = ["add", "remove"];
44
- action = action ? action.toLowerCase() : "";
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
- if (action === 'add') {
80
- // Filter out users already in the group
81
- const usersToAdd = usersToModify.filter(id => !currentMembers.includes(String(id)));
82
- if (usersToAdd.length === 0) {
83
- _callback(null, { type: "error_gc", error: "All specified users are already in the group." });
84
- return returnPromise;
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 requestId = ++ctx.wsReqNumber;
88
- const taskId = ++ctx.wsTaskNumber;
89
-
90
- // addUserToGroup: app_id "772021112871879", version "24502707779384158" (dongdev confirmed)
91
- publishLsRequestWithAck({
92
- client: ctx.mqttClient,
93
- requestId,
94
- content: {
95
- app_id: "772021112871879",
96
- payload: JSON.stringify({
97
- epoch_id: parseInt(utils.generateOfflineThreadingID()),
98
- tasks: [{
99
- failure_count: null,
100
- label: "23",
101
- payload: JSON.stringify({
102
- thread_key: threadID,
103
- contact_ids: usersToAdd.map(id => String(id)),
104
- sync_group: 1
105
- }),
106
- queue_name: String(threadID),
107
- task_id: taskId
108
- }],
109
- version_id: "24502707779384158"
110
- }),
111
- request_id: requestId,
112
- type: 3
113
- },
114
- extract: (message) => ({ success: true, response: message.payload })
115
- }).then((result) => {
116
- _callback(null, {
117
- type: "gc_member_update",
118
- threadID, userIDs: usersToAdd, action,
119
- senderID: ctx.userID, BotID: ctx.userID, timestamp: Date.now()
120
- });
121
- }).catch((err) => {
122
- utils.error("gcmember/add", err);
123
- _callback(err instanceof Error ? err : new Error(err.message || "gcmember add failed"));
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: "2220391788200892",
138
- payload: JSON.stringify({
139
- epoch_id: parseInt(utils.generateOfflineThreadingID()),
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
- return _callback(null, {
111
+
112
+ const gcmemberInfo = {
160
113
  type: "gc_member_update",
161
- threadID, userIDs: [userToRemove], action,
162
- senderID: ctx.userID, BotID: ctx.userID, timestamp: Date.now()
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
- utils.error("gcmember", err);
169
- _callback(err instanceof Error ? err : new Error(err.message || "An unknown error occurred."));
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;
@@ -91,7 +91,7 @@ module.exports = function (defaultFuncs, api, ctx) {
91
91
  };
92
92
 
93
93
  const context = {
94
- app_id: '2220391788200892',
94
+ app_id: ctx.appID || '2220391788200892',
95
95
  payload: {
96
96
  epoch_id: parseInt(utils.generateOfflineThreadingID()),
97
97
  tasks: [query],
@@ -74,33 +74,36 @@ module.exports = function (defaultFuncs, api, ctx) {
74
74
  }
75
75
 
76
76
  const isAdminStatus = action === 'admin' ? 1 : 0;
77
- if (typeof ctx.wsReqNumber !== "number") ctx.wsReqNumber = 0;
77
+ ctx.wsReqNumber = (ctx.wsReqNumber || 0) + 1;
78
+ ctx.wsTaskNumber = (ctx.wsTaskNumber || 0) + 1;
78
79
 
79
- // Dongdev pattern: task_id starts at 1 (index + 1), fire-and-forget publish
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: JSON.stringify({
93
+ app_id: ctx.appID || '2220391788200892',
94
+ payload: {
83
95
  epoch_id: parseInt(utils.generateOfflineThreadingID()),
84
- tasks: [{
85
- failure_count: null,
86
- label: "25",
87
- payload: JSON.stringify({
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
- return _callback(null, {
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) {
@@ -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: mqttAid,
117
+ aid: 219994525426954,
124
118
  aids: null,
125
119
  mqtt_sid: '',
126
120
  cp: 3,
@@ -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],
@@ -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,
@@ -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 = (msg.body || "").indexOf(tag, mention.fromIndex || 0);
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
- // Apply anti-suspension delay and volume checks for ALL send paths
290
- // (both MQTT and HTTP). This must run before transport selection so the
291
- // circuit-breaker can block sends on either path.
292
- try {
293
- await globalAntiSuspension.prepareBeforeMessage(
294
- Array.isArray(threadID) ? threadID[0] : threadID,
295
- typeof msg === "string" ? msg : (msg.body || "")
296
- );
297
- } catch (suspensionErr) {
298
- // Surface circuit-breaker and volume-limit errors directly to the caller
299
- // instead of silently swallowing them. Bots MUST handle these to avoid
300
- // triggering Facebook's automated-behavior detection.
301
- return callback(suspensionErr);
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
- if (preferMqtt) {
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
- primaryOk = true;
321
- } catch (err) {
322
- primaryErr = err;
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
- // Primary transport failed — fall back to the OTHER transport.
330
- let fallbackOk = false;
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
- result = await api.sendMessageMqtt(msg, threadID, replyToMessage);
347
- fallbackOk = true;
348
- } catch (e) {
349
- fallbackErr = e;
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
- fallbackErr = primaryErr;
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
- if (fallbackOk) {
357
- return callback(null, result);
358
- } else {
359
- // Both transports failed — report the primary error (most informative)
360
- // with the fallback error attached for diagnostics.
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
- // Use monotonic ctx.wsReqNumber so concurrent sends never share the same
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: taskId1,
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: taskId2,
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)], app_id: '772021112871879' },
51
- { label: 1037, queue: ['msgr_custom_thread_theme', String(threadID)], app_id: '772021112871879' },
52
- { label: 1028, queue: ['thread_theme_writer', String(threadID)], app_id: '772021112871879' },
53
- // label "43" (thread_theme) is the standard FB operation app_id "2220391788200892" (dongdev confirmed)
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, app_id }) => {
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: '2220391788200892',
153
+ app_id: ctx.appID,
154
154
  payload: {
155
155
  epoch_id: currentEpochId,
156
156
  tasks: [query],
157
- version_id: '8798795233522156',
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()),
@@ -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/136.0.0.0 Safari/537.36",
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,
@@ -205,10 +205,7 @@ class AutoReLoginManager {
205
205
 
206
206
  async pauseAPIRequests() {
207
207
  utils.log("AutoReLogin", "Pausing API requests during re-login...");
208
- // Wait long enough for in-flight HTTP requests to drain before we start
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) {
@@ -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 - Type of request: 'xhr' for GraphQL/AJAX or 'navigate' for page navigation
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': isXhr
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
- if (isWindows || isMac || isLinux) {
194
- headers['Sec-Ch-Ua-Arch'] = '"x86"';
195
- headers['Sec-Ch-Ua-Bitness'] = '"64"';
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
+ };
@@ -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,
@@ -14,12 +14,8 @@ class RateLimiter {
14
14
 
15
15
  this.ERROR_CACHE_TTL = 300000;
16
16
  this.COOLDOWN_DURATION = 60000;
17
- // Raised from 50 → 120 req/min. The old limit was too aggressive — it
18
- // throttled legitimate API calls (thread info lookups, getSeqID, markAsRead
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
- // Raised from 20 → 40 req/min per endpoint. The old value throttled
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 = {}) {
@@ -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
- "137.0.0.0", "136.0.0.0", "135.0.0.0", "134.0.0.0", "133.0.0.0",
10
- "132.0.6834.160", "131.0.6778.205", "130.0.6723.119", "129.0.6668.100"
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
- "137.0.0.0", "136.0.0.0", "135.0.0.0", "134.0.0.0",
14
- "133.0.3065.92", "132.0.2957.140", "131.0.2903.112"
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
- "137.0.0.0", "136.0.0.0", "135.0.0.0", "134.0.0.0", "133.0.0.0",
22
- "132.0.6834.160", "131.0.6778.205", "130.0.6723.119", "129.0.6668.100"
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
- "137.0.0.0", "136.0.0.0", "135.0.0.0", "134.0.0.0",
26
- "133.0.3065.92", "132.0.2957.140"
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.1"'
34
+ platformVersion: '"14.7.0"'
29
35
  },
30
36
  linux: {
31
37
  platform: "X11; Linux x86_64",
32
38
  chromeVersions: [
33
- "137.0.0.0", "136.0.0.0", "135.0.0.0", "134.0.0.0",
34
- "133.0.0.0", "132.0.6834.160", "131.0.6778.205"
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
- "137.0.0.0", "136.0.0.0", "135.0.0.0",
38
- "134.0.0.0", "133.0.3065.92"
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/137.0.0.0 Safari/537.36";
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.7 && data.edgeVersions;
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
- const greeseValue = Math.random() > 0.5 ? '99' : '8';
67
- const brands = useEdge ? [
68
- `"Chromium";v="${majorVersion}"`,
69
- `"Not(A:Brand";v="${greeseValue}"`,
70
- `"${browserName}";v="${majorVersion}"`
71
- ] : [
72
- `"${browserName}";v="${majorVersion}"`,
73
- `"Not;A=Brand";v="${greeseValue}"`,
74
- `"Chromium";v="${majorVersion}"`
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
- const match = b.match(/v="(\d+)"/);
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", "15"];
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
+ };