fca-neokex-fix 1.0.1

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.
Files changed (114) hide show
  1. package/CHANGELOG.md +220 -0
  2. package/LICENSE +26 -0
  3. package/README.md +346 -0
  4. package/THEME_FEATURES.md +137 -0
  5. package/examples/README.md +131 -0
  6. package/examples/apply-ai-theme.js +127 -0
  7. package/examples/check-current-theme.js +74 -0
  8. package/examples/simple-bot.js +114 -0
  9. package/examples/test-bot.js +752 -0
  10. package/examples/test-logging.js +85 -0
  11. package/examples/theme-usage-example.js +53 -0
  12. package/index.js +2 -0
  13. package/package.json +105 -0
  14. package/src/apis/addExternalModule.js +24 -0
  15. package/src/apis/addUserToGroup.js +108 -0
  16. package/src/apis/changeAdminStatus.js +148 -0
  17. package/src/apis/changeArchivedStatus.js +61 -0
  18. package/src/apis/changeAvatar.js +103 -0
  19. package/src/apis/changeBio.js +69 -0
  20. package/src/apis/changeBlockedStatus.js +54 -0
  21. package/src/apis/changeGroupImage.js +136 -0
  22. package/src/apis/changeThreadColor.js +116 -0
  23. package/src/apis/comment.js +207 -0
  24. package/src/apis/createAITheme.js +129 -0
  25. package/src/apis/createNewGroup.js +79 -0
  26. package/src/apis/createPoll.js +73 -0
  27. package/src/apis/deleteMessage.js +44 -0
  28. package/src/apis/deleteThread.js +52 -0
  29. package/src/apis/editMessage.js +70 -0
  30. package/src/apis/emoji.js +124 -0
  31. package/src/apis/fetchThemeData.js +65 -0
  32. package/src/apis/follow.js +81 -0
  33. package/src/apis/forwardMessage.js +52 -0
  34. package/src/apis/friend.js +243 -0
  35. package/src/apis/gcmember.js +122 -0
  36. package/src/apis/gcname.js +123 -0
  37. package/src/apis/gcrule.js +119 -0
  38. package/src/apis/getAccess.js +111 -0
  39. package/src/apis/getBotInfo.js +88 -0
  40. package/src/apis/getBotInitialData.js +43 -0
  41. package/src/apis/getFriendsList.js +79 -0
  42. package/src/apis/getMessage.js +423 -0
  43. package/src/apis/getTheme.js +104 -0
  44. package/src/apis/getThemeInfo.js +96 -0
  45. package/src/apis/getThreadHistory.js +239 -0
  46. package/src/apis/getThreadInfo.js +257 -0
  47. package/src/apis/getThreadList.js +222 -0
  48. package/src/apis/getThreadPictures.js +58 -0
  49. package/src/apis/getUserID.js +83 -0
  50. package/src/apis/getUserInfo.js +495 -0
  51. package/src/apis/getUserInfoV2.js +146 -0
  52. package/src/apis/handleMessageRequest.js +50 -0
  53. package/src/apis/httpGet.js +63 -0
  54. package/src/apis/httpPost.js +89 -0
  55. package/src/apis/httpPostFormData.js +69 -0
  56. package/src/apis/listenMqtt.js +796 -0
  57. package/src/apis/listenSpeed.js +170 -0
  58. package/src/apis/logout.js +63 -0
  59. package/src/apis/markAsDelivered.js +47 -0
  60. package/src/apis/markAsRead.js +95 -0
  61. package/src/apis/markAsReadAll.js +41 -0
  62. package/src/apis/markAsSeen.js +70 -0
  63. package/src/apis/mqttDeltaValue.js +330 -0
  64. package/src/apis/muteThread.js +45 -0
  65. package/src/apis/nickname.js +132 -0
  66. package/src/apis/notes.js +163 -0
  67. package/src/apis/pinMessage.js +141 -0
  68. package/src/apis/produceMetaTheme.js +180 -0
  69. package/src/apis/realtime.js +161 -0
  70. package/src/apis/removeUserFromGroup.js +117 -0
  71. package/src/apis/resolvePhotoUrl.js +58 -0
  72. package/src/apis/searchForThread.js +154 -0
  73. package/src/apis/sendMessage.js +281 -0
  74. package/src/apis/sendMessageMqtt.js +188 -0
  75. package/src/apis/sendTypingIndicator.js +41 -0
  76. package/src/apis/setMessageReaction.js +27 -0
  77. package/src/apis/setMessageReactionMqtt.js +61 -0
  78. package/src/apis/setThreadTheme.js +260 -0
  79. package/src/apis/setThreadThemeMqtt.js +94 -0
  80. package/src/apis/share.js +107 -0
  81. package/src/apis/shareContact.js +66 -0
  82. package/src/apis/stickers.js +257 -0
  83. package/src/apis/story.js +181 -0
  84. package/src/apis/theme.js +233 -0
  85. package/src/apis/unfriend.js +47 -0
  86. package/src/apis/unsendMessage.js +17 -0
  87. package/src/database/appStateBackup.js +189 -0
  88. package/src/database/models/index.js +56 -0
  89. package/src/database/models/thread.js +31 -0
  90. package/src/database/models/user.js +32 -0
  91. package/src/database/threadData.js +101 -0
  92. package/src/database/userData.js +90 -0
  93. package/src/engine/client.js +91 -0
  94. package/src/engine/models/buildAPI.js +109 -0
  95. package/src/engine/models/loginHelper.js +326 -0
  96. package/src/engine/models/setOptions.js +53 -0
  97. package/src/utils/auth-helpers.js +149 -0
  98. package/src/utils/autoReLogin.js +169 -0
  99. package/src/utils/axios.js +290 -0
  100. package/src/utils/clients.js +270 -0
  101. package/src/utils/constants.js +396 -0
  102. package/src/utils/formatters/data/formatAttachment.js +370 -0
  103. package/src/utils/formatters/data/formatDelta.js +153 -0
  104. package/src/utils/formatters/index.js +159 -0
  105. package/src/utils/formatters/value/formatCookie.js +91 -0
  106. package/src/utils/formatters/value/formatDate.js +36 -0
  107. package/src/utils/formatters/value/formatID.js +16 -0
  108. package/src/utils/formatters.js +1067 -0
  109. package/src/utils/headers.js +199 -0
  110. package/src/utils/index.js +151 -0
  111. package/src/utils/monitoring.js +358 -0
  112. package/src/utils/rateLimiter.js +380 -0
  113. package/src/utils/tokenRefresh.js +311 -0
  114. package/src/utils/user-agents.js +238 -0
@@ -0,0 +1,154 @@
1
+ "use strict";
2
+
3
+ const utils = require('../utils');
4
+
5
+ module.exports = (defaultFuncs, api, ctx) => {
6
+ return async function searchForThread(searchQuery, callback) {
7
+ let resolveFunc = () => {};
8
+ let rejectFunc = () => {};
9
+ const returnPromise = new Promise((resolve, reject) => {
10
+ resolveFunc = resolve;
11
+ rejectFunc = reject;
12
+ });
13
+
14
+ // Store original callback if provided
15
+ const originalCallback = callback;
16
+
17
+ // Always use a wrapped callback that settles the promise
18
+ callback = (err, result) => {
19
+ if (originalCallback) {
20
+ originalCallback(err, result);
21
+ }
22
+ if (err) return rejectFunc(err);
23
+ resolveFunc(result);
24
+ };
25
+
26
+ if (!searchQuery || typeof searchQuery !== 'string') {
27
+ const error = { error: "searchForThread: searchQuery parameter must be a non-empty string" };
28
+ utils.error("searchForThread", error);
29
+ callback(error);
30
+ return returnPromise;
31
+ }
32
+
33
+ try {
34
+ // Strategy 1: Use GraphQL-based getThreadList and filter locally
35
+ // This bypasses checkpoint issues entirely
36
+ utils.log("searchForThread", "Using GraphQL-based search (bypasses checkpoints)");
37
+
38
+ try {
39
+ // Use getThreadList to fetch threads from INBOX
40
+ // This ensures consistent behavior and avoids tag parameter issues
41
+ const threads = await api.getThreadList(100, null, ["INBOX"]);
42
+
43
+ if (!threads || threads.length === 0) {
44
+ utils.warn("searchForThread", "No threads available in INBOX, trying legacy method");
45
+ throw new Error("No threads available from GraphQL");
46
+ }
47
+
48
+ utils.log("searchForThread", `Retrieved ${threads.length} threads from GraphQL`);
49
+
50
+ // Filter threads by search query (case-insensitive, partial match)
51
+ const searchLower = searchQuery.toLowerCase().trim();
52
+ const matchedThreads = threads.filter(thread => {
53
+ // Search in thread name
54
+ if (thread.threadName && thread.threadName.toLowerCase().includes(searchLower)) {
55
+ return true;
56
+ }
57
+
58
+ // Search in thread ID (exact or partial match)
59
+ if (thread.threadID && thread.threadID.toString().includes(searchQuery)) {
60
+ return true;
61
+ }
62
+
63
+ // Search in participant names
64
+ if (thread.userInfo && Array.isArray(thread.userInfo)) {
65
+ return thread.userInfo.some(user =>
66
+ user.name && user.name.toLowerCase().includes(searchLower)
67
+ );
68
+ }
69
+
70
+ return false;
71
+ });
72
+
73
+ if (matchedThreads.length === 0) {
74
+ callback({
75
+ error: `Could not find thread matching "${searchQuery}".`,
76
+ details: "No threads match your search query. Try a different search term."
77
+ });
78
+ return returnPromise;
79
+ }
80
+
81
+ utils.log("searchForThread", `Found ${matchedThreads.length} matching thread(s) using GraphQL method`);
82
+ callback(null, matchedThreads);
83
+ return returnPromise;
84
+
85
+ } catch (graphqlError) {
86
+ utils.warn("searchForThread", "GraphQL method failed, falling back to legacy AJAX endpoint");
87
+ utils.error("searchForThread GraphQL error", graphqlError);
88
+
89
+ // Strategy 2: Fallback to legacy AJAX endpoint (may trigger checkpoints)
90
+ const form = {
91
+ client: "web_messenger",
92
+ query: searchQuery,
93
+ offset: 0,
94
+ limit: 21,
95
+ index: "fbid"
96
+ };
97
+
98
+ const res = await defaultFuncs.post(
99
+ "https://www.facebook.com/ajax/mercury/search_threads.php",
100
+ ctx.jar,
101
+ form
102
+ ).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
103
+
104
+ if (!res) {
105
+ const error = {
106
+ error: "Account checkpoint required - searchForThread is restricted until verification",
107
+ details: "Please verify your account on facebook.com. This function requires additional permissions.",
108
+ errorCode: 1357004,
109
+ errorType: 'CHECKPOINT'
110
+ };
111
+ callback(error);
112
+ return returnPromise;
113
+ }
114
+
115
+ if (res.error) {
116
+ throw res;
117
+ }
118
+
119
+ // Support both legacy payload.threads (object map) and newer payload.mercury_payload.threads (array)
120
+ let threadsData = res.payload?.mercury_payload?.threads || res.payload?.threads;
121
+
122
+ if (!threadsData) {
123
+ callback({
124
+ error: `Could not find thread "${searchQuery}".`,
125
+ details: "The thread may not exist or access may be restricted."
126
+ });
127
+ return returnPromise;
128
+ }
129
+
130
+ // Convert legacy object format to array if needed
131
+ if (!Array.isArray(threadsData)) {
132
+ threadsData = Object.values(threadsData);
133
+ }
134
+
135
+ const threads = threadsData.map(utils.formatThread);
136
+ utils.log("searchForThread", `Found ${threads.length} thread(s) using legacy AJAX method`);
137
+ callback(null, threads);
138
+ }
139
+ } catch (err) {
140
+ // Enhanced error handling for checkpoint errors
141
+ if (err.errorCode === 1357004 || err.errorType === 'CHECKPOINT') {
142
+ err.error = "Account checkpoint required - searchForThread is restricted until verification";
143
+ err.friendlyMessage = "Your account requires verification on facebook.com before using search features";
144
+ } else if (err.error && typeof err.error === 'string' && err.error.includes('checkpoint')) {
145
+ err.friendlyMessage = "Account checkpoint required - searchForThread is restricted until verification";
146
+ }
147
+
148
+ utils.error("searchForThread", err);
149
+ callback(err);
150
+ }
151
+
152
+ return returnPromise;
153
+ };
154
+ };
@@ -0,0 +1,281 @@
1
+ "use strict";
2
+
3
+ const utils = require('../utils');
4
+
5
+ const allowedProperties = {
6
+ attachment: true,
7
+ url: true,
8
+ sticker: true,
9
+ emoji: true,
10
+ emojiSize: true,
11
+ body: true,
12
+ mentions: true,
13
+ location: true,
14
+ };
15
+
16
+ module.exports = (defaultFuncs, api, ctx) => {
17
+ function detectAttachmentType(attachment) {
18
+ const path = attachment.path || '';
19
+ const ext = path.toLowerCase().split('.').pop();
20
+
21
+ const audioTypes = ['mp3', 'wav', 'aac', 'm4a', 'ogg', 'opus', 'flac'];
22
+ const videoTypes = ['mp4', 'mov', 'avi', 'mkv', 'webm', 'wmv', 'flv'];
23
+ const imageTypes = ['jpg', 'jpeg', 'png', 'gif', 'bmp', 'webp', 'svg'];
24
+
25
+ if (audioTypes.includes(ext)) {
26
+ return { voice_clip: "true" };
27
+ } else if (videoTypes.includes(ext)) {
28
+ return { video: "true" };
29
+ } else if (imageTypes.includes(ext)) {
30
+ return { image: "true" };
31
+ }
32
+
33
+ return { file: "true" };
34
+ }
35
+
36
+ async function uploadSingleAttachment(attachment) {
37
+ if (!utils.isReadableStream(attachment)) {
38
+ throw new Error("Attachment should be a readable stream and not " + utils.getType(attachment) + ".");
39
+ }
40
+
41
+ const uploadType = detectAttachmentType(attachment);
42
+ const oksir = await defaultFuncs.postFormData("https://upload.facebook.com/ajax/mercury/upload.php", ctx.jar, {
43
+ upload_1024: attachment,
44
+ ...uploadType
45
+ }, {}).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
46
+
47
+ if (oksir.error) {
48
+ throw new Error(JSON.stringify(oksir));
49
+ }
50
+ return oksir.payload.metadata[0];
51
+ }
52
+
53
+ async function uploadAttachment(attachments) {
54
+ const CONCURRENT_UPLOADS = 3;
55
+
56
+ const uploadPromises = [];
57
+ const uploads = [];
58
+
59
+ for (let i = 0; i < attachments.length; i += CONCURRENT_UPLOADS) {
60
+ const batch = attachments.slice(i, i + CONCURRENT_UPLOADS);
61
+ const batchPromises = batch.map(attachment => uploadSingleAttachment(attachment));
62
+ const batchResults = await Promise.all(batchPromises);
63
+ uploads.push(...batchResults);
64
+ }
65
+
66
+ return uploads;
67
+ }
68
+
69
+ async function getUrl(url) {
70
+ const resData = await defaultFuncs.post("https://www.facebook.com/message_share_attachment/fromURI/", ctx.jar, {
71
+ image_height: 960,
72
+ image_width: 960,
73
+ uri: url
74
+ }).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
75
+ if (!resData || resData.error || !resData.payload){
76
+ throw new Error("Invalid url");
77
+ }
78
+ return resData.payload.share_data.share_params;
79
+ }
80
+
81
+ async function sendContent(form, threadID, isSingleUser, messageAndOTID, callback) {
82
+ if (utils.getType(threadID) === "Array") {
83
+ for (var i = 0; i < threadID.length; i++) {
84
+ form["specific_to_list[" + i + "]"] = "fbid:" + threadID[i];
85
+ }
86
+ form["specific_to_list[" + threadID.length + "]"] = "fbid:" + ctx.userID;
87
+ form["client_thread_id"] = "root:" + messageAndOTID;
88
+ utils.log("sendMessage", "Sending message to multiple users: " + threadID);
89
+ } else {
90
+ if (isSingleUser) {
91
+ form["specific_to_list[0]"] = "fbid:" + threadID;
92
+ form["specific_to_list[1]"] = "fbid:" + ctx.userID;
93
+ form["other_user_fbid"] = threadID;
94
+ } else {
95
+ form["thread_fbid"] = threadID;
96
+ }
97
+ }
98
+
99
+ if (ctx.globalOptions.pageID) {
100
+ form["author"] = "fbid:" + ctx.globalOptions.pageID;
101
+ form["specific_to_list[1]"] = "fbid:" + ctx.globalOptions.pageID;
102
+ form["creator_info[creatorID]"] = ctx.userID;
103
+ form["creator_info[creatorType]"] = "direct_admin";
104
+ form["creator_info[labelType]"] = "sent_message";
105
+ form["creator_info[pageID]"] = ctx.globalOptions.pageID;
106
+ form["request_user_id"] = ctx.globalOptions.pageID;
107
+ form["creator_info[profileURI]"] =
108
+ "https://www.facebook.com/profile.php?id=" + ctx.userID;
109
+ }
110
+
111
+ const resData = await defaultFuncs.post("https://www.facebook.com/messaging/send/", ctx.jar, form).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
112
+ if (!resData) {
113
+ throw new Error("Send message failed.");
114
+ }
115
+ if (resData.error) {
116
+ if (resData.error === 1545012) {
117
+ utils.warn("sendMessage", "Got error 1545012. This might mean that you're not part of the conversation " + threadID);
118
+ }
119
+ throw new Error(JSON.stringify(resData));
120
+ }
121
+ const messageInfo = resData.payload.actions.reduce((p, v) => {
122
+ return { threadID: v.thread_fbid, messageID: v.message_id, timestamp: v.timestamp } || p;
123
+ }, null);
124
+ return messageInfo;
125
+ }
126
+
127
+ return async (msg, threadID, callback, replyToMessage, isGroup) => {
128
+ if (!callback && (utils.getType(threadID) === "Function" || utils.getType(threadID) === "AsyncFunction")) {
129
+ throw new Error("Pass a threadID as a second argument.");
130
+ }
131
+ if (!replyToMessage && utils.getType(callback) === "String") {
132
+ replyToMessage = callback;
133
+ callback = undefined;
134
+ }
135
+
136
+ let resolveFunc = () => {};
137
+ let rejectFunc = () => {};
138
+ let returnPromise = new Promise((resolve, reject) => {
139
+ resolveFunc = resolve;
140
+ rejectFunc = reject;
141
+ });
142
+
143
+ if (!callback) {
144
+ callback = (err, data) => {
145
+ if (err) return rejectFunc(err);
146
+ resolveFunc(data);
147
+ };
148
+ }
149
+
150
+ let msgType = utils.getType(msg);
151
+ let threadIDType = utils.getType(threadID);
152
+ let messageIDType = utils.getType(replyToMessage);
153
+ if (msgType !== "String" && msgType !== "Object") {
154
+ return callback(new Error("Message should be of type string or object and not " + msgType + "."));
155
+ }
156
+ if (threadIDType !== "Array" && threadIDType !== "Number" && threadIDType !== "String") {
157
+ return callback(new Error("ThreadID should be of type number, string, or array and not " + threadIDType + "."));
158
+ }
159
+ if (replyToMessage && messageIDType !== 'String') {
160
+ return callback(new Error("MessageID should be of type string and not " + threadIDType + "."));
161
+ }
162
+ if (msgType === "String") {
163
+ msg = { body: msg };
164
+ }
165
+ let disallowedProperties = Object.keys(msg).filter(prop => !allowedProperties[prop]);
166
+ if (disallowedProperties.length > 0) {
167
+ return callback(new Error("Dissallowed props: `" + disallowedProperties.join(", ") + "`"));
168
+ }
169
+
170
+ try {
171
+ let messageAndOTID = utils.generateOfflineThreadingID();
172
+ let form = {
173
+ client: "mercury",
174
+ action_type: "ma-type:user-generated-message",
175
+ author: "fbid:" + ctx.userID,
176
+ timestamp: Date.now(),
177
+ timestamp_absolute: "Today",
178
+ timestamp_relative: utils.generateTimestampRelative(),
179
+ timestamp_time_passed: "0",
180
+ is_unread: false,
181
+ is_cleared: false,
182
+ is_forward: false,
183
+ is_filtered_content: false,
184
+ is_filtered_content_bh: false,
185
+ is_filtered_content_account: false,
186
+ is_filtered_content_quasar: false,
187
+ is_filtered_content_invalid_app: false,
188
+ is_spoof_warning: false,
189
+ source: "source:chat:web",
190
+ "source_tags[0]": "source:chat",
191
+ ...(msg.body && {
192
+ body: msg.body
193
+ }),
194
+ html_body: false,
195
+ ui_push_phase: "V3",
196
+ status: "0",
197
+ offline_threading_id: messageAndOTID,
198
+ message_id: messageAndOTID,
199
+ threading_id: utils.generateThreadingID(ctx.clientID),
200
+ "ephemeral_ttl_mode:": "0",
201
+ manual_retry_cnt: "0",
202
+ has_attachment: !!(msg.attachment || msg.url || msg.sticker),
203
+ signatureID: utils.getSignatureID(),
204
+ ...(replyToMessage && {
205
+ replied_to_message_id: replyToMessage
206
+ })
207
+ };
208
+
209
+ if (msg.location) {
210
+ if (!msg.location.latitude || !msg.location.longitude) {
211
+ return callback(new Error("location property needs both latitude and longitude"));
212
+ }
213
+ form["location_attachment[coordinates][latitude]"] = msg.location.latitude;
214
+ form["location_attachment[coordinates][longitude]"] = msg.location.longitude;
215
+ form["location_attachment[is_current_location]"] = !!msg.location.current;
216
+ }
217
+ if (msg.sticker) {
218
+ form["sticker_id"] = msg.sticker;
219
+ }
220
+ if (msg.attachment) {
221
+ form.image_ids = [];
222
+ form.gif_ids = [];
223
+ form.file_ids = [];
224
+ form.video_ids = [];
225
+ form.audio_ids = [];
226
+ if (utils.getType(msg.attachment) !== "Array") {
227
+ msg.attachment = [msg.attachment];
228
+ }
229
+ const files = await uploadAttachment(msg.attachment);
230
+ files.forEach(file => {
231
+ const type = Object.keys(file)[0];
232
+ form["" + type + "s"].push(file[type]);
233
+ });
234
+ }
235
+ if (msg.url) {
236
+ form["shareable_attachment[share_type]"] = "100";
237
+ const params = await getUrl(msg.url);
238
+ form["shareable_attachment[share_params]"] = params;
239
+ }
240
+ if (msg.emoji) {
241
+ if (!msg.emojiSize) {
242
+ msg.emojiSize = "medium";
243
+ }
244
+ if (msg.emojiSize !== "small" && msg.emojiSize !== "medium" && msg.emojiSize !== "large") {
245
+ return callback(new Error("emojiSize property is invalid"));
246
+ }
247
+ if (form.body && form.body !== "") {
248
+ return callback(new Error("body is not empty"));
249
+ }
250
+ form.body = msg.emoji;
251
+ form["tags[0]"] = "hot_emoji_size:" + msg.emojiSize;
252
+ }
253
+ if (msg.mentions) {
254
+ for (let i = 0; i < msg.mentions.length; i++) {
255
+ const mention = msg.mentions[i];
256
+ const tag = mention.tag;
257
+ if (typeof tag !== "string") {
258
+ return callback(new Error("Mention tags must be strings."));
259
+ }
260
+ const offset = msg.body.indexOf(tag, mention.fromIndex || 0);
261
+ if (offset < 0) utils.warn("handleMention", 'Mention for "' + tag + '" not found in message string.');
262
+ if (!mention.id) utils.warn("handleMention", "Mention id should be non-null.");
263
+ const id = mention.id || 0;
264
+ const emptyChar = '\u200E';
265
+ form["body"] = emptyChar + msg.body;
266
+ form["profile_xmd[" + i + "][offset]"] = offset + 1;
267
+ form["profile_xmd[" + i + "][length]"] = tag.length;
268
+ form["profile_xmd[" + i + "][id]"] = id;
269
+ form["profile_xmd[" + i + "][type]"] = "p";
270
+ }
271
+ }
272
+
273
+ const isSingleUser = (utils.getType(isGroup) !== "Boolean") ? (threadID.toString().length <= 15) : !isGroup;
274
+ const result = await sendContent(form, threadID, isSingleUser, messageAndOTID);
275
+ callback(null, result);
276
+ } catch (err) {
277
+ callback(err);
278
+ }
279
+ return returnPromise;
280
+ };
281
+ };
@@ -0,0 +1,188 @@
1
+ "use strict";
2
+
3
+ const utils = require('../utils');
4
+ const delay = async ms => await new Promise(res => setTimeout(res, ms));
5
+
6
+ module.exports = (defaultFuncs, api, ctx) => {
7
+ /**
8
+ * Uploads an attachment to Facebook's servers.
9
+ * @param {Array<Stream>} attachments An array of readable streams.
10
+ * @param {Function} callback The callback function.
11
+ */
12
+ function uploadAttachment(attachments, callback) {
13
+ callback = callback || function () {};
14
+ var uploads = [];
15
+ for (var i = 0; i < attachments.length; i++) {
16
+ if (!utils.isReadableStream(attachments[i])) {
17
+ throw { error: "Attachment should be a readable stream and not " + utils.getType(attachments[i]) + "." };
18
+ }
19
+ var form = {
20
+ upload_1024: attachments[i],
21
+ voice_clip: "true",
22
+ };
23
+ uploads.push(
24
+ defaultFuncs
25
+ .postFormData("https://upload.facebook.com/ajax/mercury/upload.php", ctx.jar, form, {})
26
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
27
+ .then(resData => {
28
+ if (resData.error) throw resData;
29
+ return resData.payload.metadata[0];
30
+ }),
31
+ );
32
+ }
33
+ Promise.all(uploads)
34
+ .then(resData => callback(null, resData))
35
+ .catch(err => {
36
+ utils.error("uploadAttachment", err);
37
+ return callback(err);
38
+ });
39
+ }
40
+
41
+ function getSendPayload(threadID, msg, otid) {
42
+ const isString = typeof msg === 'string';
43
+ const body = isString ? msg : msg.body || "";
44
+ otid = otid.toString() || utils.generateOfflineThreadingID().toString();
45
+ let payload = {
46
+ thread_id: threadID.toString(),
47
+ otid,
48
+ source: 0,
49
+ send_type: 1,
50
+ sync_group: 1,
51
+ text: body,
52
+ initiating_source: 1,
53
+ skip_url_preview_gen: 0,
54
+ };
55
+ if (typeof msg === 'object') {
56
+ if (msg.sticker) {
57
+ payload.send_type = 2;
58
+ payload.sticker_id = msg.sticker;
59
+ payload.text = null;
60
+ }
61
+ if (msg.attachment) {
62
+ payload.send_type = 3;
63
+ payload.attachment_fbids = Array.isArray(msg.attachment) ? msg.attachment : [msg.attachment];
64
+ }
65
+ }
66
+ return payload;
67
+ }
68
+
69
+ /**
70
+ * Sends a message to a thread via MQTT with optional sequential editing.
71
+ * @param {object|string} msg The message to send. Can be a string or an object.
72
+ * @param {string} msg.body The main text of the message.
73
+ * @param {*} [msg.attachment] An attachment to send.
74
+ * @param {*} [msg.sticker] A sticker to send.
75
+ * @param {*} [msg.emoji] An emoji to send.
76
+ * @param {string} threadID The ID of the thread.
77
+ * @param {string} [replyToMessage] The ID of the message to reply to.
78
+ */
79
+
80
+ return async (msg, threadID, replyToMessage, callback) => {
81
+
82
+ if (typeof msg !== 'string' && typeof msg !== 'object') {
83
+ throw new Error("Message should be of type string or object, not " + utils.getType(msg) + ".");
84
+ }
85
+
86
+ if (typeof threadID !== 'string' && typeof threadID !== 'number') {
87
+ throw new Error("threadID must be a string or number.");
88
+ }
89
+
90
+ if (!callback && typeof threadID === "function") {
91
+ throw new Error("Pass a threadID as a second argument.");
92
+ }
93
+
94
+ if (!callback && typeof replyToMessage === "function") {
95
+ callback = replyToMessage;
96
+ replyToMessage = null;
97
+ }
98
+
99
+ let resolveFunc = () => {};
100
+ let rejectFunc = () => {};
101
+ let returnPromise = new Promise((resolve, reject) => {
102
+ resolveFunc = resolve;
103
+ rejectFunc = reject;
104
+ });
105
+
106
+ if (!callback) {
107
+ callback = (err, data) => {
108
+ if (err) return rejectFunc(err);
109
+ resolveFunc(data);
110
+ };
111
+ }
112
+
113
+ const timestamp = Date.now();
114
+ const otid = utils.generateOfflineThreadingID();
115
+ const epoch_id = utils.generateOfflineThreadingID();
116
+ const payload = getSendPayload(threadID, msg, otid);
117
+
118
+ const tasks = [{
119
+ label: "46",
120
+ payload,
121
+ queue_name: threadID.toString(),
122
+ task_id: 0,
123
+ failure_count: null,
124
+ }, {
125
+ label: "21",
126
+ payload: {
127
+ thread_id: threadID.toString(),
128
+ last_read_watermark_ts: timestamp,
129
+ sync_group: 1,
130
+ },
131
+ queue_name: threadID.toString(),
132
+ task_id: 1,
133
+ failure_count: null,
134
+ }];
135
+
136
+ if (replyToMessage) {
137
+ tasks[0].payload.reply_metadata = {
138
+ reply_source_id: replyToMessage,
139
+ reply_source_type: 1,
140
+ reply_type: 0,
141
+ };
142
+ }
143
+
144
+ const form = {
145
+ app_id: "2220391788200892",
146
+ payload: {
147
+ tasks,
148
+ epoch_id,
149
+ version_id: "6120284488008082",
150
+ data_trace_id: null,
151
+ },
152
+ request_id: 1,
153
+ type: 3,
154
+ };
155
+
156
+ if (msg.attachment) {
157
+ try {
158
+ const files = await new Promise((resolve, reject) => {
159
+ uploadAttachment(
160
+ Array.isArray(msg.attachment) ? msg.attachment : [msg.attachment],
161
+ (err, files) => {
162
+ if (err) return reject(err);
163
+ return resolve(files);
164
+ }
165
+ );
166
+ });
167
+ form.payload.tasks[0].payload.attachment_fbids = files.map(file => Object.values(file)[0]);
168
+ } catch (err) {
169
+ utils.error("Attachment upload failed:", err);
170
+ throw err;
171
+ }
172
+ }
173
+
174
+ form.payload.tasks.forEach(task => {
175
+ task.payload = JSON.stringify(task.payload);
176
+ });
177
+ form.payload = JSON.stringify(form.payload);
178
+ await ctx.mqttClient.publish("/ls_req", JSON.stringify(form), {
179
+ qos: 1,
180
+ retain: false
181
+ });
182
+ callback(null, {
183
+ threadID,
184
+ type: replyToMessage ? "message_reply" : "message"
185
+ });
186
+ return returnPromise;
187
+ };
188
+ };
@@ -0,0 +1,41 @@
1
+
2
+ "use strict";
3
+
4
+ const utils = require('../utils');
5
+
6
+ /**
7
+ * @param {Object} defaultFuncs
8
+ * @param {Object} api
9
+ * @param {Object} ctx
10
+ */
11
+ module.exports = function (defaultFuncs, api, ctx) {
12
+ /**
13
+ * Sends a typing indicator to a specific thread.
14
+ * @param {boolean} sendTyping - True to show typing indicator, false to hide.
15
+ * @param {string} threadID - The ID of the thread to send the typing indicator to.
16
+ * @param {Function} [callback] - An optional callback function.
17
+ * @returns {Promise<void>}
18
+ */
19
+ return async function sendTypingIndicatorV2(sendTyping, threadID, callback) {
20
+ let count_req = 0;
21
+ const wsContent = {
22
+ app_id: 2220391788200892,
23
+ payload: JSON.stringify({
24
+ label: 3,
25
+ payload: JSON.stringify({
26
+ thread_key: threadID.toString(),
27
+ is_group_thread: +(threadID.toString().length >= 16),
28
+ is_typing: +sendTyping,
29
+ attribution: 0
30
+ }),
31
+ version: 5849951561777440
32
+ }),
33
+ request_id: ++count_req,
34
+ type: 4
35
+ };
36
+ await new Promise((resolve, reject) => ctx.mqttClient.publish('/ls_req', JSON.stringify(wsContent), {}, (err, _packet) => err ? reject(err) : resolve()));
37
+ if (callback) {
38
+ callback();
39
+ }
40
+ };
41
+ };
@@ -0,0 +1,27 @@
1
+ "use strict";
2
+
3
+ const utils = require('../utils');
4
+
5
+
6
+ module.exports = function (defaultFuncs, api, ctx) {
7
+ return async (reaction, messageID) => {
8
+ if (!reaction) throw new Error("Please enter a valid emoji.");
9
+ const defData = await defaultFuncs.postFormData("https://www.facebook.com/webgraphql/mutation/", ctx.jar, {}, {
10
+ doc_id: "1491398900900362",
11
+ variables: JSON.stringify({
12
+ data: {
13
+ client_mutation_id: ctx.clientMutationId++,
14
+ actor_id: ctx.userID,
15
+ action: reaction == "" ? "REMOVE_REACTION" : "ADD_REACTION",
16
+ message_id: messageID,
17
+ reaction
18
+ }
19
+ }),
20
+ dpr: 1
21
+ });
22
+ const resData = await utils.parseAndCheckLogin(ctx, defaultFuncs)(defData);
23
+ if (!resData) {
24
+ throw new Error("setMessageReactionLegacy returned empty object.");
25
+ }
26
+ };
27
+ };