@lazyneoaz/metachat 1.0.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (122) hide show
  1. package/LICENSE +3 -0
  2. package/README.md +199 -0
  3. package/index.js +2 -0
  4. package/package.json +86 -0
  5. package/src/apis/addExternalModule.js +24 -0
  6. package/src/apis/addUserToGroup.js +108 -0
  7. package/src/apis/changeAdminStatus.js +148 -0
  8. package/src/apis/changeArchivedStatus.js +61 -0
  9. package/src/apis/changeAvatar.js +103 -0
  10. package/src/apis/changeBio.js +69 -0
  11. package/src/apis/changeBlockedStatus.js +54 -0
  12. package/src/apis/changeGroupImage.js +136 -0
  13. package/src/apis/changeThreadColor.js +116 -0
  14. package/src/apis/changeThreadEmoji.js +53 -0
  15. package/src/apis/comment.js +207 -0
  16. package/src/apis/createAITheme.js +129 -0
  17. package/src/apis/createNewGroup.js +79 -0
  18. package/src/apis/createPoll.js +73 -0
  19. package/src/apis/deleteMessage.js +44 -0
  20. package/src/apis/deleteThread.js +52 -0
  21. package/src/apis/editMessage.js +70 -0
  22. package/src/apis/emoji.js +124 -0
  23. package/src/apis/enableAutoSaveAppState.js +69 -0
  24. package/src/apis/fetchThemeData.js +113 -0
  25. package/src/apis/follow.js +81 -0
  26. package/src/apis/forwardAttachment.js +195 -0
  27. package/src/apis/forwardMessage.js +52 -0
  28. package/src/apis/friend.js +243 -0
  29. package/src/apis/gcmember.js +122 -0
  30. package/src/apis/gcname.js +123 -0
  31. package/src/apis/gcrule.js +119 -0
  32. package/src/apis/getAccess.js +111 -0
  33. package/src/apis/getBotInfo.js +88 -0
  34. package/src/apis/getBotInitialData.js +43 -0
  35. package/src/apis/getEmojiUrl.js +40 -0
  36. package/src/apis/getFriendsList.js +79 -0
  37. package/src/apis/getMessage.js +423 -0
  38. package/src/apis/getTheme.js +123 -0
  39. package/src/apis/getThemeInfo.js +116 -0
  40. package/src/apis/getThemePictures.js +87 -0
  41. package/src/apis/getThreadColors.js +119 -0
  42. package/src/apis/getThreadHistory.js +239 -0
  43. package/src/apis/getThreadInfo.js +271 -0
  44. package/src/apis/getThreadList.js +236 -0
  45. package/src/apis/getThreadPictures.js +58 -0
  46. package/src/apis/getUserID.js +117 -0
  47. package/src/apis/getUserInfo.js +513 -0
  48. package/src/apis/getUserInfoV2.js +146 -0
  49. package/src/apis/handleFriendRequest.js +66 -0
  50. package/src/apis/handleMessageRequest.js +50 -0
  51. package/src/apis/httpGet.js +63 -0
  52. package/src/apis/httpPost.js +89 -0
  53. package/src/apis/httpPostFormData.js +69 -0
  54. package/src/apis/listenMqtt.js +1081 -0
  55. package/src/apis/listenSpeed.js +178 -0
  56. package/src/apis/logout.js +63 -0
  57. package/src/apis/markAsDelivered.js +47 -0
  58. package/src/apis/markAsRead.js +82 -0
  59. package/src/apis/markAsReadAll.js +40 -0
  60. package/src/apis/markAsSeen.js +70 -0
  61. package/src/apis/mqttDeltaValue.js +252 -0
  62. package/src/apis/muteThread.js +45 -0
  63. package/src/apis/nickname.js +132 -0
  64. package/src/apis/notes.js +163 -0
  65. package/src/apis/pinMessage.js +150 -0
  66. package/src/apis/produceMetaTheme.js +160 -0
  67. package/src/apis/realtime.js +182 -0
  68. package/src/apis/refreshFb_dtsg.js +94 -0
  69. package/src/apis/removeUserFromGroup.js +117 -0
  70. package/src/apis/resolvePhotoUrl.js +58 -0
  71. package/src/apis/scheduler.js +129 -0
  72. package/src/apis/searchForThread.js +154 -0
  73. package/src/apis/sendEffect.js +311 -0
  74. package/src/apis/sendMessage.js +341 -0
  75. package/src/apis/sendMessageMqtt.js +271 -0
  76. package/src/apis/sendTypingIndicator.js +74 -0
  77. package/src/apis/setMessageReaction.js +27 -0
  78. package/src/apis/setMessageReactionMqtt.js +61 -0
  79. package/src/apis/setPostReaction.js +118 -0
  80. package/src/apis/setThreadTheme.js +210 -0
  81. package/src/apis/setThreadThemeMqtt.js +94 -0
  82. package/src/apis/setTitle.js +26 -0
  83. package/src/apis/share.js +106 -0
  84. package/src/apis/shareContact.js +66 -0
  85. package/src/apis/stickers.js +257 -0
  86. package/src/apis/story.js +181 -0
  87. package/src/apis/theme.js +233 -0
  88. package/src/apis/unfriend.js +47 -0
  89. package/src/apis/unsendMessage.js +17 -0
  90. package/src/apis/uploadAttachment.js +87 -0
  91. package/src/database/appStateBackup.js +189 -0
  92. package/src/database/models/index.js +56 -0
  93. package/src/database/models/thread.js +31 -0
  94. package/src/database/models/user.js +32 -0
  95. package/src/database/threadData.js +101 -0
  96. package/src/database/userData.js +90 -0
  97. package/src/engine/client.js +92 -0
  98. package/src/engine/models/buildAPI.js +118 -0
  99. package/src/engine/models/loginHelper.js +492 -0
  100. package/src/engine/models/setOptions.js +88 -0
  101. package/src/types/index.d.ts +498 -0
  102. package/src/utils/antiSuspension.js +516 -0
  103. package/src/utils/auth-helpers.js +149 -0
  104. package/src/utils/autoReLogin.js +239 -0
  105. package/src/utils/axios.js +368 -0
  106. package/src/utils/cache.js +54 -0
  107. package/src/utils/clients.js +279 -0
  108. package/src/utils/constants.js +525 -0
  109. package/src/utils/formatters/data/formatAttachment.js +370 -0
  110. package/src/utils/formatters/data/formatDelta.js +109 -0
  111. package/src/utils/formatters/index.js +159 -0
  112. package/src/utils/formatters/value/formatCookie.js +91 -0
  113. package/src/utils/formatters/value/formatDate.js +36 -0
  114. package/src/utils/formatters/value/formatID.js +16 -0
  115. package/src/utils/formatters.js +1369 -0
  116. package/src/utils/headers.js +235 -0
  117. package/src/utils/index.js +153 -0
  118. package/src/utils/monitoring.js +333 -0
  119. package/src/utils/rateLimiter.js +251 -0
  120. package/src/utils/tokenRefresh.js +285 -0
  121. package/src/utils/user-agents.js +238 -0
  122. package/src/utils/validation.js +157 -0
@@ -0,0 +1,271 @@
1
+ "use strict";
2
+
3
+ const utils = require('../utils');
4
+ const { globalAntiSuspension } = require('../utils/antiSuspension');
5
+
6
+ module.exports = (defaultFuncs, api, ctx) => {
7
+ function detectAttachmentType(attachment) {
8
+ const p = attachment.path || "";
9
+ const ext = p.toLowerCase().split(".").pop();
10
+ const audio = ["mp3", "wav", "aac", "m4a", "ogg", "opus", "flac"];
11
+ const video = ["mp4", "mov", "avi", "mkv", "webm", "wmv", "flv"];
12
+ const image = ["jpg", "jpeg", "png", "gif", "bmp", "webp", "svg"];
13
+ if (audio.includes(ext)) return { voice_clip: "true" };
14
+ if (video.includes(ext)) return { video: "true" };
15
+ if (image.includes(ext)) return { image: "true" };
16
+ return { file: "true" };
17
+ }
18
+
19
+ async function uploadAttachments(attachments) {
20
+ const uploads = [];
21
+ for (let i = 0; i < attachments.length; i++) {
22
+ if (!utils.isReadableStream(attachments[i])) {
23
+ throw new Error("Attachment should be a readable stream and not " + utils.getType(attachments[i]) + ".");
24
+ }
25
+ if (i > 0) {
26
+ await globalAntiSuspension.addSmartDelay();
27
+ }
28
+ const form = {
29
+ upload_1024: attachments[i],
30
+ ...detectAttachmentType(attachments[i]),
31
+ };
32
+ const upload = await defaultFuncs
33
+ .postFormData("https://upload.facebook.com/ajax/mercury/upload.php", ctx.jar, form, {}, { ...ctx, requestThreadID: String(ctx._lastThreadHint || "") })
34
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
35
+ .then(resData => {
36
+ if (resData.error) throw resData;
37
+ return resData.payload.metadata[0];
38
+ });
39
+ uploads.push(upload);
40
+ }
41
+ return uploads;
42
+ }
43
+
44
+ function buildMentionData(msg, baseBody) {
45
+ if (!Array.isArray(msg.mentions) || msg.mentions.length === 0) return null;
46
+ const ids = [], offsets = [], lengths = [], types = [];
47
+ let cursor = 0;
48
+ for (const mention of msg.mentions) {
49
+ const rawTag = String(mention.tag || "");
50
+ const displayName = rawTag.replace(/^@+/, "");
51
+ const start = Number.isInteger(mention.fromIndex) ? mention.fromIndex : cursor;
52
+ let index = baseBody.indexOf(rawTag, start);
53
+ let adjustment = 0;
54
+ if (index === -1) {
55
+ index = baseBody.indexOf(displayName, start);
56
+ } else {
57
+ adjustment = rawTag.length - displayName.length;
58
+ }
59
+ if (index < 0) { index = 0; adjustment = 0; }
60
+ const offset = index + adjustment;
61
+ ids.push(String(mention.id || 0));
62
+ offsets.push(offset);
63
+ lengths.push(displayName.length);
64
+ types.push("p");
65
+ cursor = offset + displayName.length;
66
+ }
67
+ return {
68
+ mention_ids: ids.join(","),
69
+ mention_offsets: offsets.join(","),
70
+ mention_lengths: lengths.join(","),
71
+ mention_types: types.join(","),
72
+ };
73
+ }
74
+
75
+ function hasLinks(text) {
76
+ return /(https?:\/\/|www\.|t\.me\/|fb\.me\/|youtu\.be\/|facebook\.com\/|youtube\.com\/)/i.test(text);
77
+ }
78
+
79
+ function extractIdsFromPayload(payload) {
80
+ let messageID = null;
81
+ let threadID = null;
82
+ function walk(n) {
83
+ if (!Array.isArray(n)) return;
84
+ if (n[0] === 5 && (n[1] === "replaceOptimsiticMessage" || n[1] === "replaceOptimisticMessage")) {
85
+ messageID = String(n[3]);
86
+ }
87
+ if (n[0] === 5 && n[1] === "writeCTAIdToThreadsTable") {
88
+ const a = n[2];
89
+ if (Array.isArray(a) && a[0] === 19) threadID = String(a[1]);
90
+ }
91
+ for (const x of n) walk(x);
92
+ }
93
+ walk(payload?.step);
94
+ return { threadID, messageID };
95
+ }
96
+
97
+ function publishWithAck(content, reqID) {
98
+ return new Promise((resolve, reject) => {
99
+ if (!ctx.mqttClient || typeof ctx.mqttClient.on !== "function" || typeof ctx.mqttClient.publish !== "function") {
100
+ return reject(new Error("MQTT client is not initialized"));
101
+ }
102
+ if (typeof ctx.mqttClient.setMaxListeners === "function") {
103
+ ctx.mqttClient.setMaxListeners(0);
104
+ }
105
+ let settled = false;
106
+ let timer;
107
+ const cleanup = () => {
108
+ if (settled) return;
109
+ settled = true;
110
+ if (timer) clearTimeout(timer);
111
+ ctx.mqttClient.removeListener("message", onMessage);
112
+ };
113
+ const onMessage = (topic, message) => {
114
+ if (topic !== "/ls_resp") return;
115
+ let parsed;
116
+ try {
117
+ parsed = JSON.parse(message.toString());
118
+ if (typeof parsed.payload === "string") parsed.payload = JSON.parse(parsed.payload);
119
+ } catch { return; }
120
+ if (parsed.request_id !== reqID) return;
121
+ const { threadID, messageID } = extractIdsFromPayload(parsed.payload);
122
+ cleanup();
123
+ resolve({ messageID, threadID });
124
+ };
125
+ ctx.mqttClient.on("message", onMessage);
126
+ ctx.mqttClient.publish("/ls_req", JSON.stringify(content), { qos: 1, retain: false }, (err) => {
127
+ if (err) { cleanup(); reject(err); }
128
+ });
129
+ timer = setTimeout(() => {
130
+ if (settled) return;
131
+ cleanup();
132
+ reject({ error: "Timeout waiting for ACK" });
133
+ }, 15000);
134
+ });
135
+ }
136
+
137
+ return async (msg, threadID, replyToMessage, callback) => {
138
+ if (typeof msg !== "string" && typeof msg !== "object") {
139
+ throw new Error("Message should be of type string or object, not " + utils.getType(msg) + ".");
140
+ }
141
+ if (typeof threadID !== "string" && typeof threadID !== "number") {
142
+ throw new Error("threadID must be a string or number.");
143
+ }
144
+ if (!callback && typeof replyToMessage === "function") {
145
+ callback = replyToMessage;
146
+ replyToMessage = null;
147
+ }
148
+
149
+ try {
150
+ await globalAntiSuspension.prepareBeforeMessage(String(threadID), typeof msg === "string" ? msg : (msg.body || ""));
151
+ } catch (_) {}
152
+
153
+ const normalized = typeof msg === "string" ? { body: msg } : msg;
154
+ const baseBody = normalized.body != null ? String(normalized.body) : "";
155
+ const epoch = (BigInt(Date.now()) << 22n).toString();
156
+ const requestId = Math.floor(100 + Math.random() * 900);
157
+ const otid = utils.generateOfflineThreadingID();
158
+
159
+ const payload0 = {
160
+ thread_id: String(threadID),
161
+ otid: otid.toString(),
162
+ source: 2097153,
163
+ send_type: 1,
164
+ sync_group: 1,
165
+ mark_thread_read: 1,
166
+ text: baseBody === "" ? null : baseBody,
167
+ initiating_source: 0,
168
+ skip_url_preview_gen: 0,
169
+ text_has_links: hasLinks(baseBody) ? 1 : 0,
170
+ multitab_env: 0,
171
+ metadata_dataclass: JSON.stringify({ media_accessibility_metadata: { alt_text: null } }),
172
+ };
173
+
174
+ if (normalized.mentions && Array.isArray(normalized.mentions) && normalized.mentions.length > 0) {
175
+ const mentionData = buildMentionData(normalized, baseBody);
176
+ if (mentionData) payload0.mention_data = mentionData;
177
+ }
178
+
179
+ if (normalized.sticker) {
180
+ payload0.send_type = 2;
181
+ payload0.sticker_id = normalized.sticker;
182
+ payload0.text = null;
183
+ }
184
+
185
+ if (normalized.emoji) {
186
+ payload0.send_type = 1;
187
+ payload0.text = normalized.emoji;
188
+ const sizeMap = { small: 1, medium: 2, large: 3 };
189
+ const emojiSize = normalized.emojiSize;
190
+ payload0.hot_emoji_size = (typeof emojiSize === "number" ? Math.min(3, Math.max(1, emojiSize)) : sizeMap[emojiSize]) || 1;
191
+ }
192
+
193
+ if (normalized.location && normalized.location.latitude != null && normalized.location.longitude != null) {
194
+ payload0.send_type = 1;
195
+ payload0.location_data = {
196
+ coordinates: { latitude: normalized.location.latitude, longitude: normalized.location.longitude },
197
+ is_current_location: Boolean(normalized.location.current),
198
+ is_live_location: Boolean(normalized.location.live),
199
+ };
200
+ }
201
+
202
+ if (normalized.effect) {
203
+ const effectTag = String(normalized.effect).toUpperCase().replace(/[\s\-]+/g, "_");
204
+ payload0.lightweight_action_attached = {
205
+ message_id: otid.toString(),
206
+ messaging_tag: "fb.messaging.effects." + effectTag,
207
+ };
208
+ }
209
+
210
+ if (replyToMessage || normalized.replyToMessage) {
211
+ const replyId = replyToMessage || normalized.replyToMessage;
212
+ payload0.reply_metadata = {
213
+ reply_source_id: replyId,
214
+ reply_source_type: 1,
215
+ reply_type: 0,
216
+ };
217
+ }
218
+
219
+ if (normalized.attachment) {
220
+ payload0.send_type = 3;
221
+ if (payload0.text === "") payload0.text = null;
222
+ const list = Array.isArray(normalized.attachment) ? normalized.attachment : [normalized.attachment];
223
+ ctx._lastThreadHint = threadID;
224
+ const files = await uploadAttachments(list.filter(a => utils.isReadableStream(a)));
225
+ payload0.attachment_fbids = files.map(f => String(Object.values(f)[0]));
226
+ }
227
+
228
+ const content = {
229
+ app_id: "2220391788200892",
230
+ payload: {
231
+ tasks: [
232
+ {
233
+ label: "46",
234
+ payload: payload0,
235
+ queue_name: String(threadID),
236
+ task_id: 400,
237
+ failure_count: null,
238
+ },
239
+ {
240
+ label: "21",
241
+ payload: {
242
+ thread_id: String(threadID),
243
+ last_read_watermark_ts: Date.now(),
244
+ sync_group: 1,
245
+ },
246
+ queue_name: String(threadID),
247
+ task_id: 401,
248
+ failure_count: null,
249
+ },
250
+ ],
251
+ epoch_id: epoch,
252
+ version_id: "24804310205905615",
253
+ data_trace_id: `#${Buffer.from(String(Math.random())).toString("base64").replace(/=+$/g, "")}`,
254
+ },
255
+ request_id: requestId,
256
+ type: 3,
257
+ };
258
+
259
+ content.payload.tasks = content.payload.tasks.map(t => ({ ...t, payload: JSON.stringify(t.payload) }));
260
+ content.payload = JSON.stringify(content.payload);
261
+
262
+ try {
263
+ const result = await publishWithAck(content, requestId);
264
+ if (callback) callback(undefined, result);
265
+ return result;
266
+ } catch (err) {
267
+ if (callback) callback(err);
268
+ throw err;
269
+ }
270
+ };
271
+ };
@@ -0,0 +1,74 @@
1
+ "use strict";
2
+
3
+ const utils = require('../utils');
4
+
5
+ module.exports = function (defaultFuncs, api, ctx) {
6
+ return function sendTypingIndicator(sendTyping, threadID, callback) {
7
+ let resolveFunc, rejectFunc;
8
+ const returnPromise = new Promise((resolve, reject) => {
9
+ resolveFunc = resolve;
10
+ rejectFunc = reject;
11
+ });
12
+
13
+ if (!callback) {
14
+ callback = (err) => {
15
+ if (err) return rejectFunc(err);
16
+ resolveFunc(true);
17
+ };
18
+ }
19
+
20
+ if (!ctx.mqttClient || typeof ctx.mqttClient.publish !== "function") {
21
+ const err = new Error("You can only use sendTypingIndicator after you start listening.");
22
+ callback(err);
23
+ return returnPromise;
24
+ }
25
+
26
+ const threadIDs = Array.isArray(threadID) ? threadID : [threadID];
27
+ if (!threadIDs.length) {
28
+ const err = new Error("threadID is required");
29
+ callback(err);
30
+ return returnPromise;
31
+ }
32
+
33
+ if (typeof ctx.wsReqNumber !== "number") ctx.wsReqNumber = 0;
34
+
35
+ function buildPayload(tid) {
36
+ const isGroup = String(tid).length >= 16 ? 1 : 0;
37
+ return {
38
+ app_id: "772021112871879",
39
+ payload: JSON.stringify({
40
+ label: "3",
41
+ payload: JSON.stringify({
42
+ thread_key: Number.parseInt(String(tid), 10),
43
+ is_group_thread: isGroup,
44
+ is_typing: sendTyping ? 1 : 0,
45
+ attribution: 0,
46
+ sync_group: 1,
47
+ thread_type: isGroup ? 2 : 1,
48
+ }),
49
+ version: "8965252033599983",
50
+ }),
51
+ request_id: ++ctx.wsReqNumber,
52
+ type: 4,
53
+ };
54
+ }
55
+
56
+ const publishes = threadIDs.map(tid =>
57
+ new Promise((resolve, reject) => {
58
+ ctx.mqttClient.publish("/ls_req", JSON.stringify(buildPayload(tid)), { qos: 1 }, (err) => {
59
+ if (err) return reject(err);
60
+ resolve();
61
+ });
62
+ })
63
+ );
64
+
65
+ Promise.all(publishes)
66
+ .then(() => callback(null, true))
67
+ .catch(err => {
68
+ utils.error("sendTypingIndicator", err);
69
+ callback(err);
70
+ });
71
+
72
+ return returnPromise;
73
+ };
74
+ };
@@ -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
+ };
@@ -0,0 +1,61 @@
1
+
2
+ 'use strict';
3
+
4
+ const utils = require('../utils');
5
+
6
+ function isCallable(func) {
7
+   try {
8
+     Reflect.apply(func, null, []);
9
+     return true;
10
+   } catch (error) {
11
+     return false;
12
+   }
13
+ }
14
+
15
+ module.exports = function (defaultFuncs, api, ctx) {
16
+   return function setMessageReactionMqtt(reaction, messageID, threadID) {
17
+     if (!ctx.mqttClient) {
18
+       throw new Error('Not connected to MQTT');
19
+     }
20
+
21
+     ctx.wsReqNumber += 1;
22
+     ctx.wsTaskNumber += 1;
23
+
24
+     const taskPayload = {
25
+       thread_key: threadID,
26
+       timestamp_ms: Date.now(),
27
+       message_id: messageID,
28
+       reaction,
29
+       actor_id: ctx.userID,
30
+       reaction_style: null,
31
+       sync_group: 1,
32
+       send_attribution: Math.random() < 0.5 ? 65537 : 524289
33
+     };
34
+
35
+     const task = {
36
+       failure_count: null,
37
+       label: '29',
38
+       payload: JSON.stringify(taskPayload),
39
+       queue_name: JSON.stringify(['reaction', messageID]),
40
+       task_id: ctx.wsTaskNumber
41
+     };
42
+
43
+     const content = {
44
+       app_id: '2220391788200892',
45
+       payload: JSON.stringify({
46
+         data_trace_id: null,
47
+         epoch_id: parseInt(utils.generateOfflineThreadingID()),
48
+         tasks: [task],
49
+         version_id: '7158486590867448',
50
+       }),
51
+       request_id: ctx.wsReqNumber,
52
+       type: 3,
53
+     };
54
+
55
+     /*if (isCallable(callback)) {
56
+       ctx.reqCallbacks[ctx.wsReqNumber] = callback;
57
+     }*/
58
+
59
+     ctx.mqttClient.publish('/ls_req', JSON.stringify(content), { qos: 1, retain: false });
60
+   };
61
+ };
@@ -0,0 +1,118 @@
1
+ "use strict";
2
+
3
+ const utils = require("../utils");
4
+
5
+ /**
6
+ * Adds or removes a reaction on a Facebook post (not a Messenger message).
7
+ *
8
+ * For reacting to Messenger messages use api.setMessageReaction() instead.
9
+ *
10
+ * @param {string} postID The Facebook post/object ID.
11
+ * @param {string|number} type Reaction type:
12
+ * - Named: "like" | "heart" | "love" | "haha" | "wow" | "sad" | "angry" | "unlike"
13
+ * - Numeric: 0=unlike, 1=like, 2=heart, 4=haha, 3=wow, 7=sad, 8=angry, 16=love
14
+ * @param {Function} [callback]
15
+ * @returns {Promise}
16
+ */
17
+ module.exports = function (defaultFuncs, api, ctx) {
18
+ const REACTION_MAP = {
19
+ unlike: 0,
20
+ like: 1,
21
+ heart: 2,
22
+ love: 16,
23
+ haha: 4,
24
+ wow: 3,
25
+ sad: 7,
26
+ angry: 8,
27
+ };
28
+
29
+ return function setPostReaction(postID, type, callback) {
30
+ let reactionType = type;
31
+ let cb = callback;
32
+
33
+ // Allow omitting type to default to "unlike"
34
+ if (!cb && (utils.getType(type) === "Function" || utils.getType(type) === "AsyncFunction")) {
35
+ cb = type;
36
+ reactionType = 0;
37
+ }
38
+
39
+ let resolveFunc, rejectFunc;
40
+ const promise = new Promise((resolve, reject) => {
41
+ resolveFunc = resolve;
42
+ rejectFunc = reject;
43
+ });
44
+ if (!cb) {
45
+ cb = (err, data) => { if (err) return rejectFunc(err); resolveFunc(data); };
46
+ }
47
+
48
+ if (!postID) return cb(new Error("postID is required"));
49
+
50
+ // Resolve named type
51
+ if (utils.getType(reactionType) === "String") {
52
+ const key = String(reactionType).toLowerCase();
53
+ if (key in REACTION_MAP) {
54
+ reactionType = REACTION_MAP[key];
55
+ } else {
56
+ return cb(new Error(`Unknown reaction type "${type}". Valid types: ${Object.keys(REACTION_MAP).join(", ")}`));
57
+ }
58
+ }
59
+
60
+ if (typeof reactionType !== "number") {
61
+ return cb(new Error("setPostReaction: reaction type must be a string or number"));
62
+ }
63
+
64
+ const feedbackID = Buffer.from(`feedback:${postID}`).toString("base64");
65
+
66
+ const form = {
67
+ av: ctx.userID,
68
+ __user: ctx.userID,
69
+ __a: 1,
70
+ __req: utils.getSignatureID(),
71
+ fb_dtsg: ctx.fb_dtsg,
72
+ lsd: ctx.lsd || ctx.fb_dtsg,
73
+ jazoest: ctx.jazoest,
74
+ fb_api_caller_class: "RelayModern",
75
+ fb_api_req_friendly_name: "CometUFIFeedbackReactMutation",
76
+ doc_id: "4769042373179384",
77
+ variables: JSON.stringify({
78
+ input: {
79
+ actor_id: ctx.userID,
80
+ feedback_id: feedbackID,
81
+ feedback_reaction: reactionType,
82
+ feedback_source: "OBJECT",
83
+ is_tracking_encrypted: true,
84
+ tracking: [],
85
+ session_id: utils.getSignatureID() + "-" + Date.now(),
86
+ client_mutation_id: Math.floor(Math.random() * 20).toString(),
87
+ },
88
+ useDefaultActor: false,
89
+ scale: 3,
90
+ }),
91
+ };
92
+
93
+ defaultFuncs
94
+ .post("https://www.facebook.com/api/graphql/", ctx.jar, form)
95
+ .then(utils.parseAndCheckLogin(ctx, defaultFuncs))
96
+ .then((resData) => {
97
+ if (resData && resData.errors) throw resData;
98
+
99
+ const fbReact = resData && resData.data && resData.data.feedback_react;
100
+ const result = {
101
+ postID,
102
+ reactionType,
103
+ success: true,
104
+ reaction_count: fbReact && fbReact.feedback
105
+ ? fbReact.feedback.reaction_count
106
+ : null,
107
+ };
108
+
109
+ cb(null, result);
110
+ })
111
+ .catch((err) => {
112
+ utils.error("setPostReaction", err.message || err);
113
+ cb(err instanceof Error ? err : new Error(String(err.message || err)));
114
+ });
115
+
116
+ return promise;
117
+ };
118
+ };