@lazyneoaz/metachat 1.1.2 → 1.1.3

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@lazyneoaz/metachat",
3
- "version": "1.1.2",
3
+ "version": "1.1.3",
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,6 +37,7 @@
37
37
  "metachat"
38
38
  ],
39
39
  "dependencies": {
40
+ "@dongdev/fca-unofficial": "^4.0.3",
40
41
  "axios": "^1.13.5",
41
42
  "axios-cookiejar-support": "^4.0.7",
42
43
  "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: String(ctx.appID || ctx.mqttAppID || "2220391788200892"),
70
+ app_id: "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: String(ctx.appID || ctx.mqttAppID || "772021112871879")
117
+ app_id: "2220391788200892"
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: String(ctx.appID || ctx.mqttAppID || "2220391788200892"),
113
+ app_id: "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: "772021112871879",
61
+ app_id: "2220391788200892",
62
62
  payload: JSON.stringify(payload),
63
63
  request_id: ++ctx.wsReqNumber,
64
64
  type: 3
@@ -1,9 +1,20 @@
1
1
  "use strict";
2
2
 
3
3
  const utils = require('../utils');
4
+ const { publishLsRequestWithAck } = require('../utils/lsRequest');
4
5
 
5
6
  module.exports = (defaultFuncs, api, ctx) => {
6
- return async function deleteMessage(messageID, callback) {
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) {
7
18
  let resolveFunc = () => {};
8
19
  let rejectFunc = () => {};
9
20
  const returnPromise = new Promise((resolve, reject) => {
@@ -26,17 +37,66 @@ module.exports = (defaultFuncs, api, ctx) => {
26
37
  }
27
38
 
28
39
  try {
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));
40
+ const messages = Array.isArray(messageOrMessages)
41
+ ? messageOrMessages
42
+ : [messageOrMessages];
34
43
 
35
- if (res && res.error) {
36
- throw new Error(String(res.error_msg || res.error || "deleteMessage failed"));
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
+ });
94
+
95
+ } else {
96
+ // HTTP fallback
97
+ await _deleteViaHttp(messages, callback, defaultFuncs, ctx);
37
98
  }
38
99
 
39
- callback(null, { success: true });
40
100
  } catch (err) {
41
101
  utils.error("deleteMessage", err);
42
102
  callback(err instanceof Error ? err : new Error(String(err && err.message ? err.message : err)));
@@ -45,3 +105,24 @@ module.exports = (defaultFuncs, api, ctx) => {
45
105
  return returnPromise;
46
106
  };
47
107
  };
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: ctx.appID || '2220391788200892',
95
+ app_id: '2220391788200892',
96
96
  payload: {
97
97
  epoch_id: parseInt(utils.generateOfflineThreadingID()),
98
98
  tasks: [query],
99
- version_id: '24631415369801570',
99
+ version_id: '8798795233522156',
100
100
  },
101
101
  request_id: ctx.wsReqNumber,
102
102
  type: 3,
@@ -1,128 +1,172 @@
1
1
  "use strict";
2
2
 
3
3
  const utils = require('../utils');
4
+ const { publishLsRequestWithAck } = require('../utils/lsRequest');
4
5
 
5
6
  module.exports = function (defaultFuncs, api, ctx) {
6
7
  /**
7
- * Made by ChoruOfficial
8
- * Mqtt
9
- * Adds or removes members from a group chat with pre-checking.
8
+ * Adds or removes members from a group chat.
10
9
  *
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.
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]
16
19
  */
17
20
  return async function gcmember(action, userIDs, threadID, callback) {
18
21
  let _callback;
19
22
  if (typeof threadID === 'function') {
20
- _callback = threadID;
21
- threadID = null;
23
+ _callback = threadID;
24
+ threadID = null;
22
25
  } else if (typeof callback === 'function') {
23
- _callback = callback;
26
+ _callback = callback;
24
27
  }
25
-
28
+
26
29
  let resolvePromise, rejectPromise;
27
30
  const returnPromise = new Promise((resolve, reject) => {
28
- resolvePromise = resolve;
29
- rejectPromise = reject;
31
+ resolvePromise = resolve;
32
+ rejectPromise = reject;
30
33
  });
31
34
 
32
- if (typeof _callback != "function") {
35
+ if (typeof _callback !== "function") {
33
36
  _callback = (err, data) => {
34
- // Note: We will now rarely use the 'err' parameter for validation errors
35
37
  if (err) return rejectPromise(err);
36
38
  resolvePromise(data);
37
- }
39
+ };
38
40
  }
39
41
 
40
42
  try {
41
- const validActions = ["add", "remove"];
42
- action = action ? action.toLowerCase() : "";
43
+ const validActions = ["add", "remove"];
44
+ action = action ? action.toLowerCase() : "";
43
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
+ }
44
62
 
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;
60
- }
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
+ }
61
72
 
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;
73
+ const currentMembers = threadInfo.participantIDs || [];
74
+ const usersToModify = Array.isArray(userIDs) ? userIDs : [userIDs];
75
+
76
+ if (typeof ctx.wsReqNumber !== "number") ctx.wsReqNumber = 0;
77
+ if (typeof ctx.wsTaskNumber !== "number") ctx.wsTaskNumber = 0;
78
+
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;
70
85
  }
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 };
86
+
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;
99
132
  }
100
133
 
134
+ // removeUserFromGroup: app_id "2220391788200892", version "25002366262773827" (dongdev confirmed)
135
+ // Fire-and-forget (no ACK needed) — matches dongdev's publishRealtimeMessage pattern
101
136
  const context = {
102
- app_id: ctx.appID || '2220391788200892',
103
- payload: { epoch_id: parseInt(utils.generateOfflineThreadingID()), tasks: [query], version_id: "24631415369801570" },
104
- request_id: ctx.wsReqNumber,
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,
105
154
  type: 3
106
155
  };
107
- context.payload = JSON.stringify(context.payload);
108
156
 
109
157
  ctx.mqttClient.publish('/ls_req', JSON.stringify(context), { qos: 1, retain: false }, (err) => {
110
158
  if (err) return _callback(err);
111
-
112
- const gcmemberInfo = {
159
+ return _callback(null, {
113
160
  type: "gc_member_update",
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);
161
+ threadID, userIDs: [userToRemove], action,
162
+ senderID: ctx.userID, BotID: ctx.userID, timestamp: Date.now()
163
+ });
122
164
  });
165
+ }
166
+
123
167
  } catch (err) {
124
- utils.error("gcmember", err);
125
- _callback(err instanceof Error ? err : new Error(err.message || "An unknown error occurred."));
168
+ utils.error("gcmember", err);
169
+ _callback(err instanceof Error ? err : new Error(err.message || "An unknown error occurred."));
126
170
  }
127
171
 
128
172
  return returnPromise;
@@ -91,7 +91,7 @@ module.exports = function (defaultFuncs, api, ctx) {
91
91
  };
92
92
 
93
93
  const context = {
94
- app_id: ctx.appID || '2220391788200892',
94
+ app_id: '2220391788200892',
95
95
  payload: {
96
96
  epoch_id: parseInt(utils.generateOfflineThreadingID()),
97
97
  tasks: [query],
@@ -74,36 +74,33 @@ module.exports = function (defaultFuncs, api, ctx) {
74
74
  }
75
75
 
76
76
  const isAdminStatus = action === 'admin' ? 1 : 0;
77
- ctx.wsReqNumber = (ctx.wsReqNumber || 0) + 1;
78
- ctx.wsTaskNumber = (ctx.wsTaskNumber || 0) + 1;
77
+ if (typeof ctx.wsReqNumber !== "number") ctx.wsReqNumber = 0;
79
78
 
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
- };
79
+ // Dongdev pattern: task_id starts at 1 (index + 1), fire-and-forget publish
92
80
  const context = {
93
- app_id: ctx.appID || '2220391788200892',
94
- payload: {
81
+ app_id: '2220391788200892',
82
+ payload: JSON.stringify({
95
83
  epoch_id: parseInt(utils.generateOfflineThreadingID()),
96
- tasks: [query],
97
- version_id: "24631415369801570"
98
- },
99
- request_id: ctx.wsReqNumber,
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,
100
98
  type: 3
101
99
  };
102
- context.payload = JSON.stringify(context.payload);
103
100
 
104
101
  ctx.mqttClient.publish('/ls_req', JSON.stringify(context), { qos: 1, retain: false }, (err) => {
105
102
  if (err) return _callback(err);
106
- const gcruleInfo = {
103
+ return _callback(null, {
107
104
  type: "gc_rule_update",
108
105
  threadID: threadID,
109
106
  userID: userID,
@@ -111,8 +108,7 @@ module.exports = function (defaultFuncs, api, ctx) {
111
108
  senderID: ctx.userID,
112
109
  BotID: ctx.userID,
113
110
  timestamp: Date.now(),
114
- };
115
- return _callback(null, gcruleInfo);
111
+ });
116
112
  });
117
113
 
118
114
  } catch (err) {
@@ -107,6 +107,12 @@ 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;
110
116
  const username = {
111
117
  u: ctx.userID,
112
118
  s: sessionID,
@@ -114,7 +120,7 @@ async function listenMqtt(defaultFuncs, api, ctx, globalCallback, scheduleReconn
114
120
  fg: false,
115
121
  d: cid,
116
122
  ct: 'websocket',
117
- aid: 219994525426954,
123
+ aid: mqttAid,
118
124
  aids: null,
119
125
  mqtt_sid: '',
120
126
  cp: 3,
@@ -94,7 +94,7 @@ module.exports = function (defaultFuncs, api, ctx) {
94
94
  });
95
95
 
96
96
  const envelope = {
97
- app_id: String(ctx.appID || ctx.mqttAppID || "2220391788200892"),
97
+ app_id: "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 = String(ctx.appID || ctx.mqttAppID || "2220391788200892");
57
+ const app_id = "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';
@@ -286,51 +286,83 @@ module.exports = (defaultFuncs, api, ctx) => {
286
286
  }
287
287
 
288
288
  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.
289
292
  try {
290
- const mqttReady = ctx.mqttClient && ctx.mqttClient.connected;
291
- const isMultiRecipient = Array.isArray(threadID);
292
- // Track which transport was used so the catch block can fall back to
293
- // the OTHER transport — retrying the same one that just failed is useless.
294
- const usedMqtt = mqttReady && !isMultiRecipient && api.sendMessageMqtt;
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);
302
+ }
303
+
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;
295
309
 
296
- let result;
297
- if (usedMqtt) {
310
+ let result;
311
+ let primaryErr;
312
+ let primaryOk = false;
313
+
314
+ try {
315
+ if (preferMqtt) {
298
316
  result = await api.sendMessageMqtt(msg, threadID, replyToMessage);
299
317
  } else {
300
318
  result = await sendViaHttp(msg, threadID, replyToMessage, isGroup);
301
319
  }
302
- callback(null, result);
303
- } catch (sendErr) {
304
- // Fall back to the OTHER transport, not the one that just failed.
305
- const mqttNowReady = ctx.mqttClient && ctx.mqttClient.connected;
306
- const primaryWasMqtt = mqttNowReady && !Array.isArray(threadID) && api.sendMessageMqtt;
320
+ primaryOk = true;
321
+ } catch (err) {
322
+ primaryErr = err;
323
+ }
324
+
325
+ if (primaryOk) {
326
+ return callback(null, result);
327
+ }
328
+
329
+ // Primary transport failed — fall back to the OTHER transport.
330
+ let fallbackOk = false;
331
+ let fallbackErr;
307
332
 
308
- if (primaryWasMqtt) {
309
- // MQTT was used (or is available) — fall back to HTTP
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) {
310
345
  try {
311
- const httpRes = await sendViaHttp(msg, threadID, replyToMessage, isGroup);
312
- callback(null, httpRes);
313
- } catch (_httpErr) {
314
- callback(sendErr);
346
+ result = await api.sendMessageMqtt(msg, threadID, replyToMessage);
347
+ fallbackOk = true;
348
+ } catch (e) {
349
+ fallbackErr = e;
315
350
  }
316
351
  } else {
317
- // HTTP was used — try MQTT if it has since become available
318
- if (mqttNowReady && !Array.isArray(threadID) && api.sendMessageMqtt) {
319
- try {
320
- const mqttRes = await api.sendMessageMqtt(msg, threadID, replyToMessage);
321
- callback(null, mqttRes);
322
- } catch (_mqttErr) {
323
- callback(sendErr);
324
- }
325
- } else {
326
- callback(sendErr);
327
- }
352
+ fallbackErr = primaryErr;
328
353
  }
329
- } finally {
330
- if (typingTimeout) clearTimeout(typingTimeout);
331
- if (typingStarted) {
332
- try { await api.sendTypingIndicator(false, threadID); } catch (_) {}
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;
333
364
  }
365
+ return callback(err || fallbackErr);
334
366
  }
335
367
  } catch (err) {
336
368
  callback(err);
@@ -1,7 +1,6 @@
1
1
  "use strict";
2
2
 
3
3
  const utils = require('../utils');
4
- const { globalAntiSuspension } = require('../utils/antiSuspension');
5
4
 
6
5
  module.exports = (defaultFuncs, api, ctx) => {
7
6
  function detectAttachmentType(attachment) {
@@ -143,14 +142,22 @@ module.exports = (defaultFuncs, api, ctx) => {
143
142
  replyToMessage = null;
144
143
  }
145
144
 
146
- try {
147
- await globalAntiSuspension.prepareBeforeMessage(String(threadID), typeof msg === "string" ? msg : (msg.body || ""));
148
- } catch (_) {}
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.
149
153
 
150
154
  const normalized = typeof msg === "string" ? { body: msg } : msg;
151
155
  const baseBody = normalized.body != null ? String(normalized.body) : "";
152
156
  const epoch = (BigInt(Date.now()) << 22n).toString();
153
- const requestId = Math.floor(100 + Math.random() * 900);
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;
154
161
  const otid = utils.generateOfflineThreadingID();
155
162
 
156
163
  const payload0 = {
@@ -222,6 +229,12 @@ module.exports = (defaultFuncs, api, ctx) => {
222
229
  payload0.attachment_fbids = files.map(f => String(Object.values(f)[0]));
223
230
  }
224
231
 
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
+
225
238
  const content = {
226
239
  app_id: "2220391788200892",
227
240
  payload: {
@@ -230,7 +243,7 @@ module.exports = (defaultFuncs, api, ctx) => {
230
243
  label: "46",
231
244
  payload: payload0,
232
245
  queue_name: String(threadID),
233
- task_id: 400,
246
+ task_id: taskId1,
234
247
  failure_count: null,
235
248
  },
236
249
  {
@@ -241,7 +254,7 @@ module.exports = (defaultFuncs, api, ctx) => {
241
254
  sync_group: 1,
242
255
  },
243
256
  queue_name: String(threadID),
244
- task_id: 401,
257
+ task_id: taskId2,
245
258
  failure_count: null,
246
259
  },
247
260
  ],
@@ -47,16 +47,17 @@ 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
- { label: 43, queue: 'thread_theme', extra: { source: null, payload: null } },
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' },
54
55
  ];
55
56
 
56
- const messages = messageDefs.map(({ label, queue, extra }) => {
57
+ const messages = messageDefs.map(({ label, queue, extra, app_id }) => {
57
58
  ctx.wsReqNumber += 1;
58
59
  return {
59
- app_id: '772021112871879',
60
+ app_id,
60
61
  payload: JSON.stringify({
61
62
  epoch_id: parseInt(utils.generateOfflineThreadingID()),
62
63
  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: ctx.appID,
153
+ app_id: '2220391788200892',
154
154
  payload: {
155
155
  epoch_id: currentEpochId,
156
156
  tasks: [query],
157
- version_id: '24631415369801570',
157
+ version_id: '8798795233522156',
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: String(ctx.appID || ctx.mqttAppID || "2220391788200892"),
36
+ app_id: "2220391788200892",
37
37
  payload: JSON.stringify({
38
38
  data_trace_id: null,
39
39
  epoch_id: parseInt(utils.generateOfflineThreadingID()),
@@ -461,7 +461,7 @@ class AntiSuspension {
461
461
 
462
462
  /**
463
463
  * Prepare before sending — single delay model.
464
- * Enforces thread throttle and volume limits, respects circuit breaker.
464
+ * Enforces smart delay, thread throttle, and volume limits, respects circuit breaker.
465
465
  * If the circuit breaker is tripped (checkpoint/suspension detected) or
466
466
  * volume limits are reached, throws to protect the account.
467
467
  */
@@ -205,7 +205,10 @@ class AutoReLoginManager {
205
205
 
206
206
  async pauseAPIRequests() {
207
207
  utils.log("AutoReLogin", "Pausing API requests during re-login...");
208
- await new Promise(resolve => setTimeout(resolve, 1000));
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));
209
212
  }
210
213
 
211
214
  resolvePendingRequests(success) {
@@ -28,6 +28,9 @@ 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;
31
34
  const cleanKey = key.replace(/[^\x21-\x7E]/g, '').trim();
32
35
  if (!cleanKey) continue;
33
36
  const cleanVal = sanitizeHeaderValue(value);
@@ -160,7 +163,9 @@ function getHeaders(url, options, ctx, customHeader, requestType = 'navigate') {
160
163
  const isLinux = secChUaPlatform === '"Linux"';
161
164
 
162
165
  const headers = {
163
- 'Accept': 'text/html,application/xhtml+xml,application/xml;q=0.9,application/json;q=0.8,*/*;q=0.7',
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',
164
169
  'Accept-Language': locales,
165
170
  'Accept-Encoding': 'gzip, deflate, br',
166
171
  'Cache-Control': 'no-cache',
@@ -178,8 +183,11 @@ function getHeaders(url, options, ctx, customHeader, requestType = 'navigate') {
178
183
  'Sec-Fetch-Mode': isXhr ? 'cors' : 'navigate',
179
184
  'Sec-Fetch-Site': isXhr ? 'same-origin' : 'none',
180
185
  'User-Agent': userAgent,
181
- 'Upgrade-Insecure-Requests': '1',
182
- 'X-Requested-With': 'XMLHttpRequest'
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' } : {})
183
191
  };
184
192
 
185
193
  if (isWindows || isMac || isLinux) {
@@ -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: String(ctx.appID || ctx.mqttAppID || "2220391788200892"),
125
+ app_id: "2220391788200892",
126
126
  payload: JSON.stringify({
127
127
  epoch_id: generateEpochId(),
128
128
  tasks,
@@ -14,8 +14,12 @@ class RateLimiter {
14
14
 
15
15
  this.ERROR_CACHE_TTL = 300000;
16
16
  this.COOLDOWN_DURATION = 60000;
17
- this.MAX_REQUESTS_PER_MINUTE = 50;
18
- this.MAX_CONCURRENT_REQUESTS = 5;
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;
19
23
 
20
24
  this.activeRequests = 0;
21
25
 
@@ -25,7 +29,9 @@ class RateLimiter {
25
29
 
26
30
  // Per-endpoint sliding windows
27
31
  this._endpointWindows = new Map();
28
- this._MAX_PER_ENDPOINT_PER_MINUTE = 20;
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;
29
35
  }
30
36
 
31
37
  configure(opts = {}) {
@@ -4,25 +4,44 @@ const { getRandom } = require("./constants");
4
4
  const BROWSER_DATA = {
5
5
  windows: {
6
6
  platform: "Windows NT 10.0; Win64; x64",
7
- chromeVersions: ["139.0.0.0", "131.0.6778.86", "130.0.6723.92", "129.0.6668.101", "128.0.6613.120", "127.0.6533.120"],
8
- edgeVersions: ["139.0.0.0", "131.0.2903.51", "130.0.2849.68", "129.0.2792.89"],
7
+ // Ordered from newest to oldest so random selection skews toward current builds
8
+ 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
+ ],
12
+ 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"
15
+ ],
9
16
  platformVersion: '"15.0.0"'
10
17
  },
11
18
  mac: {
12
19
  platform: "Macintosh; Intel Mac OS X 10_15_7",
13
- chromeVersions: ["139.0.0.0", "131.0.6778.86", "130.0.6723.92", "129.0.6668.101", "128.0.6613.120", "127.0.6533.120"],
14
- edgeVersions: ["139.0.0.0", "131.0.2903.51", "130.0.2849.68", "129.0.2792.89"],
15
- platformVersion: '"14.7.0"'
20
+ 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"
23
+ ],
24
+ 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"
27
+ ],
28
+ platformVersion: '"14.7.1"'
16
29
  },
17
30
  linux: {
18
31
  platform: "X11; Linux x86_64",
19
- chromeVersions: ["139.0.0.0", "131.0.6778.86", "130.0.6723.92", "129.0.6668.101", "128.0.6613.120"],
20
- edgeVersions: ["139.0.0.0", "131.0.2903.51", "130.0.2849.68"],
32
+ 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"
35
+ ],
36
+ edgeVersions: [
37
+ "137.0.0.0", "136.0.0.0", "135.0.0.0",
38
+ "134.0.0.0", "133.0.3065.92"
39
+ ],
21
40
  platformVersion: '""'
22
41
  }
23
42
  };
24
43
 
25
- const defaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36";
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";
26
45
 
27
46
  /**
28
47
  * Generates a realistic, randomized User-Agent string and related Sec-CH headers.
@@ -105,7 +124,7 @@ function randomFbav() {
105
124
  }
106
125
 
107
126
  function randomOrcaUA() {
108
- const androidVersions = ["8.1.0", "9", "10", "11", "12", "13", "14"];
127
+ const androidVersions = ["10", "11", "12", "13", "14", "15"];
109
128
  const devices = [
110
129
  { brand: "samsung", model: "SM-G996B" },
111
130
  { brand: "samsung", model: "SM-S908E" },