shadowx-fca 2.4.0 → 2.6.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.
- package/checkUpdate.js +1 -1
- package/index.js +663 -418
- package/package.json +2 -2
- package/src/GetBotInfo.js +66 -0
- package/src/OldMessage.js +96 -76
- package/src/comment.js +226 -0
- package/src/emoji.js +124 -0
- package/src/friend.js +243 -0
- package/src/gcmember.js +122 -0
- package/src/listenMqtt.js +4 -4
- package/src/nickname.js +140 -0
- package/src/sendMessage.js +398 -234
- package/src/sendMessage2.js +243 -0
- package/src/share.js +62 -0
- package/src/shareContact.js +73 -69
- package/src/shareContact.js.bak +110 -0
- package/src/stickers.js +117 -0
- package/src/story.js +181 -0
- package/src/theme.js +233 -0
- package/utils.js +24 -1311
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "shadowx-fca",
|
|
3
|
-
"version": "2.
|
|
3
|
+
"version": "2.6.0",
|
|
4
4
|
"description": "Unofficial Facebook Chat API for Node.js with Auto-Update System - modify by Mueid Mursalin Rifat",
|
|
5
5
|
"main": "index.js",
|
|
6
6
|
"files": [
|
|
@@ -50,6 +50,6 @@
|
|
|
50
50
|
"ws": "^8.18.1"
|
|
51
51
|
},
|
|
52
52
|
"engines": {
|
|
53
|
-
"node": ">=
|
|
53
|
+
"node": ">=16.0.0"
|
|
54
54
|
}
|
|
55
55
|
}
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
// shadowx-fca — GetBotInfo
|
|
3
|
+
// Author: Modified for shadowx-fca
|
|
4
|
+
|
|
5
|
+
const utils = require('../utils');
|
|
6
|
+
|
|
7
|
+
module.exports = (defaultFuncs, api, ctx) => {
|
|
8
|
+
/**
|
|
9
|
+
* Returns bot's current login info, tokens, and context accessor functions.
|
|
10
|
+
* @param {Array<object>} netData - JSON blobs extracted from Facebook HTML
|
|
11
|
+
* @returns {object|null}
|
|
12
|
+
*/
|
|
13
|
+
return function GetBotInfo(netData) {
|
|
14
|
+
if (!netData || !Array.isArray(netData)) {
|
|
15
|
+
utils.log("GetBotInfo", "netData is not a valid array.");
|
|
16
|
+
return null;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const findConfig = (key) => {
|
|
20
|
+
for (const scriptData of netData) {
|
|
21
|
+
if (!scriptData.require) continue;
|
|
22
|
+
for (const req of scriptData.require) {
|
|
23
|
+
if (Array.isArray(req) && req[0] === key && req[2]) {
|
|
24
|
+
return req[2];
|
|
25
|
+
}
|
|
26
|
+
if (Array.isArray(req) && req[3] && req[3][0] && req[3][0].__bbox && req[3][0].__bbox.define) {
|
|
27
|
+
for (const def of req[3][0].__bbox.define) {
|
|
28
|
+
if (Array.isArray(def) && def[0] && def[0].endsWith(key) && def[2]) {
|
|
29
|
+
return def[2];
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
}
|
|
34
|
+
}
|
|
35
|
+
return null;
|
|
36
|
+
};
|
|
37
|
+
|
|
38
|
+
const currentUserData = findConfig("CurrentUserInitialData");
|
|
39
|
+
const dtsgInitialData = findConfig("DTSGInitialData");
|
|
40
|
+
const dtsgInitData = findConfig("DTSGInitData");
|
|
41
|
+
const lsdData = findConfig("LSD");
|
|
42
|
+
|
|
43
|
+
if (!currentUserData || !dtsgInitialData) {
|
|
44
|
+
utils.log("GetBotInfo", "Could not find required data (CurrentUserInitialData or DTSGInitialData).");
|
|
45
|
+
return null;
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
return {
|
|
49
|
+
name: currentUserData.NAME || null,
|
|
50
|
+
firstName: currentUserData.SHORT_NAME || null,
|
|
51
|
+
uid: currentUserData.USER_ID || null,
|
|
52
|
+
appID: currentUserData.APP_ID || null,
|
|
53
|
+
dtsgToken: dtsgInitialData.token || null,
|
|
54
|
+
lsdToken: lsdData?.token || null,
|
|
55
|
+
dtsgInit: dtsgInitData ? {
|
|
56
|
+
token: dtsgInitData.token,
|
|
57
|
+
async_get_token: dtsgInitData.async_get_token
|
|
58
|
+
} : undefined,
|
|
59
|
+
getCtx: (key) => ctx ? ctx[key] : null,
|
|
60
|
+
getOptions: (key) => ctx && ctx.globalOptions ? ctx.globalOptions[key] : null,
|
|
61
|
+
getRegion: () => ctx?.region || null,
|
|
62
|
+
getUserID: () => ctx?.userID || currentUserData?.USER_ID || null,
|
|
63
|
+
getJar: () => ctx?.jar || null
|
|
64
|
+
};
|
|
65
|
+
};
|
|
66
|
+
};
|
package/src/OldMessage.js
CHANGED
|
@@ -19,9 +19,10 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
19
19
|
function uploadAttachment(attachments, callback) {
|
|
20
20
|
var uploads = [];
|
|
21
21
|
|
|
22
|
-
// create an array of promises
|
|
23
22
|
for (var i = 0; i < attachments.length; i++) {
|
|
24
|
-
if (!utils.isReadableStream(attachments[i]))
|
|
23
|
+
if (!utils.isReadableStream(attachments[i])) {
|
|
24
|
+
throw { error: "Attachment should be a readable stream and not " + utils.getType(attachments[i]) + "." };
|
|
25
|
+
}
|
|
25
26
|
var form = {
|
|
26
27
|
upload_1024: attachments[i],
|
|
27
28
|
voice_clip: "true"
|
|
@@ -33,14 +34,11 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
33
34
|
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
|
34
35
|
.then(function (resData) {
|
|
35
36
|
if (resData.error) throw resData;
|
|
36
|
-
// We have to return the data unformatted unless we want to change it
|
|
37
|
-
// back in sendMessage.
|
|
38
37
|
return resData.payload.metadata[0];
|
|
39
38
|
})
|
|
40
39
|
);
|
|
41
40
|
}
|
|
42
41
|
|
|
43
|
-
// resolve all promises
|
|
44
42
|
bluebird
|
|
45
43
|
.all(uploads)
|
|
46
44
|
.then(resData => callback(null, resData))
|
|
@@ -72,26 +70,25 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
72
70
|
}
|
|
73
71
|
|
|
74
72
|
function sendContent(form, threadID, isSingleUser, messageAndOTID, callback) {
|
|
75
|
-
// There are three cases here:
|
|
76
|
-
// 1. threadID is of type array, where we're starting a new group chat with users
|
|
77
|
-
// specified in the array.
|
|
78
|
-
// 2. User is sending a message to a specific user.
|
|
79
|
-
// 3. No additional form params and the message goes to an existing group chat.
|
|
80
73
|
if (utils.getType(threadID) === "Array") {
|
|
81
|
-
for (var i = 0; i < threadID.length; i++)
|
|
74
|
+
for (var i = 0; i < threadID.length; i++) {
|
|
75
|
+
form["specific_to_list[" + i + "]"] = "fbid:" + threadID[i];
|
|
76
|
+
}
|
|
82
77
|
form["specific_to_list[" + threadID.length + "]"] = "fbid:" + ctx.userID;
|
|
83
78
|
form["client_thread_id"] = "root:" + messageAndOTID;
|
|
84
79
|
log.info("sendMessage", "Sending message to multiple users: " + threadID);
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
|
|
89
|
-
|
|
80
|
+
} else {
|
|
81
|
+
// Auto-detect if it's a DM
|
|
82
|
+
const threadIDStr = threadID.toString();
|
|
83
|
+
const isDM = isSingleUser === true || threadIDStr.length === 15 || !threadIDStr.match(/^\d{16,}$/);
|
|
84
|
+
|
|
85
|
+
if (isDM) {
|
|
90
86
|
form["specific_to_list[0]"] = "fbid:" + threadID;
|
|
91
87
|
form["specific_to_list[1]"] = "fbid:" + ctx.userID;
|
|
92
88
|
form["other_user_fbid"] = threadID;
|
|
89
|
+
} else {
|
|
90
|
+
form["thread_fbid"] = threadID;
|
|
93
91
|
}
|
|
94
|
-
else form["thread_fbid"] = threadID;
|
|
95
92
|
}
|
|
96
93
|
|
|
97
94
|
if (ctx.globalOptions.pageID) {
|
|
@@ -113,21 +110,18 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
113
110
|
if (resData.error) {
|
|
114
111
|
if (resData.error === 1545012) {
|
|
115
112
|
log.warn("sendMessage", "Got error 1545012. This might mean that you're not part of the conversation " + threadID);
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
113
|
+
} else {
|
|
118
114
|
log.error("sendMessage", resData);
|
|
119
115
|
}
|
|
120
116
|
return callback(resData);
|
|
121
117
|
}
|
|
122
118
|
|
|
123
119
|
var messageInfo = resData.payload.actions.reduce(function (p, v) {
|
|
124
|
-
return
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
|
|
128
|
-
|
|
129
|
-
} || p
|
|
130
|
-
);
|
|
120
|
+
return {
|
|
121
|
+
threadID: v.thread_fbid,
|
|
122
|
+
messageID: v.message_id,
|
|
123
|
+
timestamp: v.timestamp
|
|
124
|
+
} || p;
|
|
131
125
|
}, null);
|
|
132
126
|
|
|
133
127
|
return callback(null, messageInfo);
|
|
@@ -139,17 +133,6 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
139
133
|
});
|
|
140
134
|
}
|
|
141
135
|
|
|
142
|
-
function send(form, threadID, messageAndOTID, callback, isGroup) {
|
|
143
|
-
// We're doing a query to this to check if the given id is the id of
|
|
144
|
-
// a user or of a group chat. The form will be different depending
|
|
145
|
-
// on that.
|
|
146
|
-
if (utils.getType(threadID) === "Array") sendContent(form, threadID, false, messageAndOTID, callback);
|
|
147
|
-
else {
|
|
148
|
-
if (utils.getType(isGroup) != "Boolean") sendContent(form, threadID, threadID.length === 15, messageAndOTID, callback);
|
|
149
|
-
else sendContent(form, threadID, !isGroup, messageAndOTID, callback);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
|
|
153
136
|
function handleUrl(msg, form, callback, cb) {
|
|
154
137
|
if (msg.url) {
|
|
155
138
|
form["shareable_attachment[share_type]"] = "100";
|
|
@@ -158,13 +141,16 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
158
141
|
form["shareable_attachment[share_params]"] = params;
|
|
159
142
|
cb();
|
|
160
143
|
});
|
|
144
|
+
} else {
|
|
145
|
+
cb();
|
|
161
146
|
}
|
|
162
|
-
else cb();
|
|
163
147
|
}
|
|
164
148
|
|
|
165
149
|
function handleLocation(msg, form, callback, cb) {
|
|
166
150
|
if (msg.location) {
|
|
167
|
-
if (msg.location.latitude == null || msg.location.longitude == null)
|
|
151
|
+
if (msg.location.latitude == null || msg.location.longitude == null) {
|
|
152
|
+
return callback({ error: "location property needs both latitude and longitude" });
|
|
153
|
+
}
|
|
168
154
|
form["location_attachment[coordinates][latitude]"] = msg.location.latitude;
|
|
169
155
|
form["location_attachment[coordinates][longitude]"] = msg.location.longitude;
|
|
170
156
|
form["location_attachment[is_current_location]"] = !!msg.location.current;
|
|
@@ -178,11 +164,17 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
178
164
|
}
|
|
179
165
|
|
|
180
166
|
function handleEmoji(msg, form, callback, cb) {
|
|
181
|
-
if (msg.emojiSize != null && msg.emoji == null)
|
|
167
|
+
if (msg.emojiSize != null && msg.emoji == null) {
|
|
168
|
+
return callback({ error: "emoji property is empty" });
|
|
169
|
+
}
|
|
182
170
|
if (msg.emoji) {
|
|
183
171
|
if (msg.emojiSize == null) msg.emojiSize = "medium";
|
|
184
|
-
if (msg.emojiSize != "small" && msg.emojiSize != "medium" && msg.emojiSize != "large")
|
|
185
|
-
|
|
172
|
+
if (msg.emojiSize != "small" && msg.emojiSize != "medium" && msg.emojiSize != "large") {
|
|
173
|
+
return callback({ error: "emojiSize property is invalid" });
|
|
174
|
+
}
|
|
175
|
+
if (form["body"] != null && form["body"] != "") {
|
|
176
|
+
return callback({ error: "body is not empty" });
|
|
177
|
+
}
|
|
186
178
|
form["body"] = msg.emoji;
|
|
187
179
|
form["tags[0]"] = "hot_emoji_size:" + msg.emojiSize;
|
|
188
180
|
}
|
|
@@ -197,23 +189,22 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
197
189
|
form["video_ids"] = [];
|
|
198
190
|
form["audio_ids"] = [];
|
|
199
191
|
|
|
200
|
-
if (utils.getType(msg.attachment) !== "Array")
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
return cb();
|
|
205
|
-
}
|
|
192
|
+
if (utils.getType(msg.attachment) !== "Array") {
|
|
193
|
+
msg.attachment = [msg.attachment];
|
|
194
|
+
}
|
|
195
|
+
|
|
206
196
|
uploadAttachment(msg.attachment, function (err, files) {
|
|
207
197
|
if (err) return callback(err);
|
|
208
198
|
files.forEach(function (file) {
|
|
209
199
|
var key = Object.keys(file);
|
|
210
|
-
var type = key[0];
|
|
211
|
-
form[
|
|
200
|
+
var type = key[0];
|
|
201
|
+
form[type + "s"].push(file[type]);
|
|
212
202
|
});
|
|
213
203
|
cb();
|
|
214
204
|
});
|
|
205
|
+
} else {
|
|
206
|
+
cb();
|
|
215
207
|
}
|
|
216
|
-
else cb();
|
|
217
208
|
}
|
|
218
209
|
|
|
219
210
|
function handleMention(msg, form, callback, cb) {
|
|
@@ -221,10 +212,16 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
221
212
|
for (let i = 0; i < msg.mentions.length; i++) {
|
|
222
213
|
const mention = msg.mentions[i];
|
|
223
214
|
const tag = mention.tag;
|
|
224
|
-
if (typeof tag !== "string")
|
|
215
|
+
if (typeof tag !== "string") {
|
|
216
|
+
return callback({ error: "Mention tags must be strings." });
|
|
217
|
+
}
|
|
225
218
|
const offset = msg.body.indexOf(tag, mention.fromIndex || 0);
|
|
226
|
-
if (offset < 0)
|
|
227
|
-
|
|
219
|
+
if (offset < 0) {
|
|
220
|
+
log.warn("handleMention", 'Mention for "' + tag + '" not found in message string.');
|
|
221
|
+
}
|
|
222
|
+
if (mention.id == null) {
|
|
223
|
+
log.warn("handleMention", "Mention id should be non-null.");
|
|
224
|
+
}
|
|
228
225
|
|
|
229
226
|
const id = mention.id || 0;
|
|
230
227
|
const emptyChar = '\u200E';
|
|
@@ -239,44 +236,67 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
239
236
|
}
|
|
240
237
|
|
|
241
238
|
return function sendMessage(msg, threadID, callback, replyToMessage, isGroup) {
|
|
242
|
-
|
|
243
|
-
if (
|
|
244
|
-
|
|
239
|
+
// Handle parameter shifting
|
|
240
|
+
if (typeof callback === "string") {
|
|
241
|
+
isGroup = replyToMessage;
|
|
245
242
|
replyToMessage = callback;
|
|
246
|
-
callback = function () {
|
|
243
|
+
callback = function () {};
|
|
244
|
+
} else if (typeof threadID === "function") {
|
|
245
|
+
callback = threadID;
|
|
246
|
+
threadID = null;
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (!callback || typeof callback !== "function") {
|
|
250
|
+
callback = function () {};
|
|
247
251
|
}
|
|
248
252
|
|
|
249
|
-
var resolveFunc = function () {
|
|
250
|
-
var rejectFunc = function () {
|
|
253
|
+
var resolveFunc = function () {};
|
|
254
|
+
var rejectFunc = function () {};
|
|
251
255
|
var returnPromise = new Promise(function (resolve, reject) {
|
|
252
256
|
resolveFunc = resolve;
|
|
253
257
|
rejectFunc = reject;
|
|
254
258
|
});
|
|
255
259
|
|
|
256
|
-
|
|
257
|
-
|
|
258
|
-
|
|
260
|
+
var originalCallback = callback;
|
|
261
|
+
callback = function (err, data) {
|
|
262
|
+
if (err) {
|
|
263
|
+
originalCallback(err);
|
|
264
|
+
rejectFunc(err);
|
|
265
|
+
} else {
|
|
266
|
+
originalCallback(null, data);
|
|
259
267
|
resolveFunc(data);
|
|
260
|
-
}
|
|
261
|
-
}
|
|
268
|
+
}
|
|
269
|
+
};
|
|
262
270
|
|
|
263
271
|
var msgType = utils.getType(msg);
|
|
264
272
|
var threadIDType = utils.getType(threadID);
|
|
265
|
-
var messageIDType = utils.getType(replyToMessage);
|
|
266
273
|
|
|
267
|
-
if (msgType !== "String" && msgType !== "Object")
|
|
274
|
+
if (msgType !== "String" && msgType !== "Object") {
|
|
275
|
+
return callback({ error: "Message should be of type string or object and not " + msgType + "." });
|
|
276
|
+
}
|
|
268
277
|
|
|
269
|
-
|
|
270
|
-
|
|
278
|
+
if (threadIDType !== "Array" && threadIDType !== "Number" && threadIDType !== "String") {
|
|
279
|
+
return callback({ error: "ThreadID should be of type number, string, or array and not " + threadIDType + "." });
|
|
280
|
+
}
|
|
271
281
|
|
|
272
|
-
if (
|
|
282
|
+
if (msgType === "String") {
|
|
283
|
+
msg = { body: msg };
|
|
284
|
+
}
|
|
285
|
+
|
|
286
|
+
// Determine isSingleUser
|
|
287
|
+
var isSingleUser = !isGroup;
|
|
288
|
+
if (isGroup === null || typeof isGroup === "undefined") {
|
|
289
|
+
const threadIDStr = threadID.toString();
|
|
290
|
+
isSingleUser = threadIDStr.length === 15 || !threadIDStr.match(/^\d{16,}$/);
|
|
291
|
+
}
|
|
273
292
|
|
|
274
|
-
if (msgType === "String") msg = { body: msg };
|
|
275
293
|
var disallowedProperties = Object.keys(msg).filter(prop => !allowedProperties[prop]);
|
|
276
|
-
if (disallowedProperties.length > 0)
|
|
294
|
+
if (disallowedProperties.length > 0) {
|
|
295
|
+
return callback({ error: "Dissallowed props: `" + disallowedProperties.join(", ") + "`" });
|
|
296
|
+
}
|
|
277
297
|
|
|
278
298
|
var messageAndOTID = utils.generateOfflineThreadingID();
|
|
279
|
-
|
|
299
|
+
|
|
280
300
|
var form = {
|
|
281
301
|
client: "mercury",
|
|
282
302
|
action_type: "ma-type:user-generated-message",
|
|
@@ -307,9 +327,8 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
307
327
|
manual_retry_cnt: "0",
|
|
308
328
|
has_attachment: !!(msg.attachment || msg.url || msg.sticker),
|
|
309
329
|
signatureID: utils.getSignatureID(),
|
|
310
|
-
replied_to_message_id: replyToMessage
|
|
330
|
+
replied_to_message_id: replyToMessage || undefined
|
|
311
331
|
};
|
|
312
|
-
// console.log(form)
|
|
313
332
|
|
|
314
333
|
handleLocation(msg, form, callback, () =>
|
|
315
334
|
handleSticker(msg, form, callback, () =>
|
|
@@ -317,13 +336,14 @@ module.exports = function (defaultFuncs, api, ctx) {
|
|
|
317
336
|
handleUrl(msg, form, callback, () =>
|
|
318
337
|
handleEmoji(msg, form, callback, () =>
|
|
319
338
|
handleMention(msg, form, callback, () =>
|
|
320
|
-
|
|
339
|
+
sendContent(form, threadID, isSingleUser, messageAndOTID, callback)
|
|
321
340
|
)
|
|
322
341
|
)
|
|
323
342
|
)
|
|
324
343
|
)
|
|
325
344
|
)
|
|
326
345
|
);
|
|
346
|
+
|
|
327
347
|
return returnPromise;
|
|
328
348
|
};
|
|
329
349
|
};
|
package/src/comment.js
ADDED
|
@@ -0,0 +1,226 @@
|
|
|
1
|
+
'use strict';
|
|
2
|
+
|
|
3
|
+
const utils = require('../utils');
|
|
4
|
+
const log = require('npmlog'); // Add npmlog for proper logging
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Handles the upload of attachments (images/videos) for a comment.
|
|
8
|
+
* @param {object} defaultFuncs - The default functions for making API requests.
|
|
9
|
+
* @param {object} ctx - The context object.
|
|
10
|
+
* @param {object} msg - The message object, containing attachments.
|
|
11
|
+
* @param {object} form - The main form object to be populated.
|
|
12
|
+
* @returns {Promise<void>}
|
|
13
|
+
*/
|
|
14
|
+
async function handleUpload(defaultFuncs, ctx, msg, form) {
|
|
15
|
+
if (!msg.attachments || msg.attachments.length === 0) {
|
|
16
|
+
return;
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
const uploads = msg.attachments.map(item => {
|
|
20
|
+
if (!utils.isReadableStream(item)) {
|
|
21
|
+
throw new Error('Attachments must be a readable stream.');
|
|
22
|
+
}
|
|
23
|
+
return defaultFuncs
|
|
24
|
+
.postFormData('https://www.facebook.com/ajax/ufi/upload/', ctx.jar, {
|
|
25
|
+
profile_id: ctx.userID,
|
|
26
|
+
source: 19,
|
|
27
|
+
target_id: ctx.userID,
|
|
28
|
+
file: item
|
|
29
|
+
})
|
|
30
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
|
31
|
+
.then(res => {
|
|
32
|
+
if (res.error || !res.payload?.fbid) {
|
|
33
|
+
throw res;
|
|
34
|
+
}
|
|
35
|
+
return { media: { id: res.payload.fbid } };
|
|
36
|
+
});
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
const results = await Promise.all(uploads);
|
|
40
|
+
form.input.attachments.push(...results);
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
/**
|
|
44
|
+
* Handles a URL attachment for a comment.
|
|
45
|
+
* @param {object} msg - The message object.
|
|
46
|
+
* @param {object} form - The main form object.
|
|
47
|
+
*/
|
|
48
|
+
function handleURL(msg, form) {
|
|
49
|
+
if (typeof msg.url === 'string' && msg.url.trim()) {
|
|
50
|
+
form.input.attachments.push({
|
|
51
|
+
link: {
|
|
52
|
+
external: {
|
|
53
|
+
url: msg.url
|
|
54
|
+
}
|
|
55
|
+
}
|
|
56
|
+
});
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
|
|
60
|
+
/**
|
|
61
|
+
* Handles mentions in the comment body.
|
|
62
|
+
* @param {object} msg - The message object.
|
|
63
|
+
* @param {object} form - The main form object.
|
|
64
|
+
*/
|
|
65
|
+
function handleMentions(msg, form) {
|
|
66
|
+
if (!msg.mentions || !Array.isArray(msg.mentions)) return;
|
|
67
|
+
|
|
68
|
+
for (const item of msg.mentions) {
|
|
69
|
+
const { tag, id, fromIndex } = item;
|
|
70
|
+
if (typeof tag !== 'string' || !id) {
|
|
71
|
+
log.warn('createCommentPost', 'Mentions must have a string "tag" and an "id".');
|
|
72
|
+
continue;
|
|
73
|
+
}
|
|
74
|
+
const offset = msg.body.indexOf(tag, fromIndex || 0);
|
|
75
|
+
if (offset < 0) {
|
|
76
|
+
log.warn('createCommentPost', `Mention for "${tag}" not found in message string.`);
|
|
77
|
+
continue;
|
|
78
|
+
}
|
|
79
|
+
form.input.message.ranges.push({
|
|
80
|
+
entity: { id: String(id) },
|
|
81
|
+
length: tag.length,
|
|
82
|
+
offset
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Handles a sticker attachment for a comment.
|
|
89
|
+
* @param {object} msg - The message object.
|
|
90
|
+
* @param {object} form - The main form object.
|
|
91
|
+
*/
|
|
92
|
+
function handleSticker(msg, form) {
|
|
93
|
+
if (msg.sticker) {
|
|
94
|
+
form.input.attachments.push({
|
|
95
|
+
media: {
|
|
96
|
+
id: String(msg.sticker)
|
|
97
|
+
}
|
|
98
|
+
});
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
/**
|
|
103
|
+
* Submits the final comment form to the GraphQL endpoint.
|
|
104
|
+
* @param {object} defaultFuncs - The default functions.
|
|
105
|
+
* @param {object} ctx - The context object.
|
|
106
|
+
* @param {object} form - The fully constructed form object.
|
|
107
|
+
* @returns {Promise<object>} A promise that resolves with the comment info.
|
|
108
|
+
*/
|
|
109
|
+
async function createContent(defaultFuncs, ctx, form) {
|
|
110
|
+
const res = await defaultFuncs
|
|
111
|
+
.post('https://www.facebook.com/api/graphql/', ctx.jar, {
|
|
112
|
+
fb_api_caller_class: 'RelayModern',
|
|
113
|
+
fb_api_req_friendly_name: 'useCometUFICreateCommentMutation',
|
|
114
|
+
variables: JSON.stringify(form),
|
|
115
|
+
server_timestamps: true,
|
|
116
|
+
doc_id: 6993516810709754
|
|
117
|
+
})
|
|
118
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
119
|
+
|
|
120
|
+
// Hard error from Facebook — comment was NOT sent
|
|
121
|
+
if (res.errors && res.errors.length > 0) {
|
|
122
|
+
throw new Error(res.errors[0].message || JSON.stringify(res.errors[0]));
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
// Try to extract comment info from response
|
|
126
|
+
try {
|
|
127
|
+
const commentEdge = res?.data?.comment_create?.feedback_comment_edge;
|
|
128
|
+
const id = commentEdge?.node?.id || null;
|
|
129
|
+
const url = commentEdge?.node?.feedback?.url || null;
|
|
130
|
+
const count = res?.data?.comment_create?.feedback?.total_comment_count || 0;
|
|
131
|
+
return { id, url, count };
|
|
132
|
+
} catch (_) {
|
|
133
|
+
// Comment went through but response structure was unexpected — still a success
|
|
134
|
+
return { id: null, url: null, count: 0 };
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
/**
|
|
139
|
+
* Creates a comment on a Facebook post. Can also reply to an existing comment.
|
|
140
|
+
* @param {string|object} msg - The message to post. Can be a string or an object with `body`, `attachments`, `mentions`, etc.
|
|
141
|
+
* @param {string} postID - The ID of the post to comment on.
|
|
142
|
+
* @param {string} [replyCommentID] - (Optional) The ID of the comment to reply to.
|
|
143
|
+
* @param {function} [callback] - (Optional) A callback function.
|
|
144
|
+
* @returns {Promise<object>} A promise that resolves with the new comment's information.
|
|
145
|
+
*/
|
|
146
|
+
module.exports = function(defaultFuncs, api, ctx) {
|
|
147
|
+
return async function createCommentPost(msg, postID, replyCommentID, callback) {
|
|
148
|
+
let cb;
|
|
149
|
+
const returnPromise = new Promise((resolve, reject) => {
|
|
150
|
+
cb = (error, info) => {
|
|
151
|
+
if (error) reject(error);
|
|
152
|
+
else resolve(info);
|
|
153
|
+
};
|
|
154
|
+
});
|
|
155
|
+
|
|
156
|
+
if (typeof replyCommentID === 'function') {
|
|
157
|
+
callback = replyCommentID;
|
|
158
|
+
replyCommentID = null;
|
|
159
|
+
}
|
|
160
|
+
if (typeof callback === 'function') {
|
|
161
|
+
const originalCb = cb;
|
|
162
|
+
cb = (error, info) => {
|
|
163
|
+
if (error) {
|
|
164
|
+
callback(error);
|
|
165
|
+
originalCb(error);
|
|
166
|
+
} else {
|
|
167
|
+
callback(null, info);
|
|
168
|
+
originalCb(null, info);
|
|
169
|
+
}
|
|
170
|
+
};
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
if (typeof msg !== 'string' && typeof msg !== 'object') {
|
|
174
|
+
const error = 'Message must be a string or an object.';
|
|
175
|
+
log.error('createCommentPost', error);
|
|
176
|
+
return cb(error);
|
|
177
|
+
}
|
|
178
|
+
if (typeof postID !== 'string') {
|
|
179
|
+
const error = 'postID must be a string.';
|
|
180
|
+
log.error('createCommentPost', error);
|
|
181
|
+
return cb(error);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
let messageObject = typeof msg === 'string' ? { body: msg } : { ...msg };
|
|
185
|
+
messageObject.mentions = messageObject.mentions || [];
|
|
186
|
+
messageObject.attachments = messageObject.attachments || [];
|
|
187
|
+
|
|
188
|
+
const form = {
|
|
189
|
+
feedLocation: 'NEWSFEED',
|
|
190
|
+
feedbackSource: 1,
|
|
191
|
+
groupID: null,
|
|
192
|
+
input: {
|
|
193
|
+
client_mutation_id: Math.round(Math.random() * 19).toString(),
|
|
194
|
+
actor_id: ctx.userID,
|
|
195
|
+
attachments: [],
|
|
196
|
+
feedback_id: Buffer.from('feedback:' + postID).toString('base64'),
|
|
197
|
+
message: {
|
|
198
|
+
ranges: [],
|
|
199
|
+
text: messageObject.body || ''
|
|
200
|
+
},
|
|
201
|
+
reply_comment_parent_fbid: replyCommentID || null,
|
|
202
|
+
is_tracking_encrypted: true,
|
|
203
|
+
tracking: [],
|
|
204
|
+
feedback_source: 'NEWS_FEED',
|
|
205
|
+
idempotence_token: 'client:' + utils.getGUID(),
|
|
206
|
+
session_id: utils.getGUID()
|
|
207
|
+
},
|
|
208
|
+
scale: 1,
|
|
209
|
+
useDefaultActor: false
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
try {
|
|
213
|
+
await handleUpload(defaultFuncs, ctx, messageObject, form);
|
|
214
|
+
handleURL(messageObject, form);
|
|
215
|
+
handleMentions(messageObject, form);
|
|
216
|
+
handleSticker(messageObject, form);
|
|
217
|
+
const info = await createContent(defaultFuncs, ctx, form);
|
|
218
|
+
cb(null, info);
|
|
219
|
+
return info;
|
|
220
|
+
} catch (err) {
|
|
221
|
+
log.error('createCommentPost', err);
|
|
222
|
+
cb(err);
|
|
223
|
+
throw err;
|
|
224
|
+
}
|
|
225
|
+
};
|
|
226
|
+
};
|