@lazyneoaz/nkxchat 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.
- package/LICENSE +3 -0
- package/README.md +199 -0
- package/examples/login-with-cookies.js +102 -0
- package/examples/verify.js +70 -0
- package/index.js +2 -0
- package/package.json +84 -0
- package/src/apis/addExternalModule.js +24 -0
- package/src/apis/addUserToGroup.js +108 -0
- package/src/apis/changeAdminStatus.js +148 -0
- package/src/apis/changeArchivedStatus.js +61 -0
- package/src/apis/changeAvatar.js +103 -0
- package/src/apis/changeBio.js +69 -0
- package/src/apis/changeBlockedStatus.js +54 -0
- package/src/apis/changeGroupImage.js +136 -0
- package/src/apis/changeThreadColor.js +116 -0
- package/src/apis/changeThreadEmoji.js +53 -0
- package/src/apis/comment.js +207 -0
- package/src/apis/createAITheme.js +129 -0
- package/src/apis/createNewGroup.js +79 -0
- package/src/apis/createPoll.js +73 -0
- package/src/apis/deleteMessage.js +44 -0
- package/src/apis/deleteThread.js +52 -0
- package/src/apis/editMessage.js +70 -0
- package/src/apis/emoji.js +124 -0
- package/src/apis/enableAutoSaveAppState.js +69 -0
- package/src/apis/fetchThemeData.js +113 -0
- package/src/apis/follow.js +81 -0
- package/src/apis/forwardAttachment.js +178 -0
- package/src/apis/forwardMessage.js +52 -0
- package/src/apis/friend.js +243 -0
- package/src/apis/gcmember.js +122 -0
- package/src/apis/gcname.js +123 -0
- package/src/apis/gcrule.js +119 -0
- package/src/apis/getAccess.js +111 -0
- package/src/apis/getBotInfo.js +88 -0
- package/src/apis/getBotInitialData.js +43 -0
- package/src/apis/getEmojiUrl.js +40 -0
- package/src/apis/getFriendsList.js +79 -0
- package/src/apis/getMessage.js +423 -0
- package/src/apis/getTheme.js +123 -0
- package/src/apis/getThemeInfo.js +116 -0
- package/src/apis/getThemePictures.js +87 -0
- package/src/apis/getThreadColors.js +119 -0
- package/src/apis/getThreadHistory.js +239 -0
- package/src/apis/getThreadInfo.js +267 -0
- package/src/apis/getThreadList.js +232 -0
- package/src/apis/getThreadPictures.js +58 -0
- package/src/apis/getUserID.js +117 -0
- package/src/apis/getUserInfo.js +513 -0
- package/src/apis/getUserInfoV2.js +146 -0
- package/src/apis/handleFriendRequest.js +66 -0
- package/src/apis/handleMessageRequest.js +50 -0
- package/src/apis/httpGet.js +63 -0
- package/src/apis/httpPost.js +89 -0
- package/src/apis/httpPostFormData.js +69 -0
- package/src/apis/listenMqtt.js +924 -0
- package/src/apis/listenSpeed.js +178 -0
- package/src/apis/logout.js +63 -0
- package/src/apis/markAsDelivered.js +47 -0
- package/src/apis/markAsRead.js +95 -0
- package/src/apis/markAsReadAll.js +40 -0
- package/src/apis/markAsSeen.js +70 -0
- package/src/apis/mqttDeltaValue.js +252 -0
- package/src/apis/muteThread.js +45 -0
- package/src/apis/nickname.js +132 -0
- package/src/apis/notes.js +163 -0
- package/src/apis/pinMessage.js +150 -0
- package/src/apis/produceMetaTheme.js +160 -0
- package/src/apis/realtime.js +182 -0
- package/src/apis/refreshFb_dtsg.js +94 -0
- package/src/apis/removeUserFromGroup.js +117 -0
- package/src/apis/resolvePhotoUrl.js +58 -0
- package/src/apis/searchForThread.js +154 -0
- package/src/apis/sendEffect.js +306 -0
- package/src/apis/sendMessage.js +353 -0
- package/src/apis/sendMessageMqtt.js +255 -0
- package/src/apis/sendTypingIndicator.js +40 -0
- package/src/apis/setMessageReaction.js +27 -0
- package/src/apis/setMessageReactionMqtt.js +61 -0
- package/src/apis/setPostReaction.js +118 -0
- package/src/apis/setThreadTheme.js +210 -0
- package/src/apis/setThreadThemeMqtt.js +94 -0
- package/src/apis/setTitle.js +26 -0
- package/src/apis/share.js +106 -0
- package/src/apis/shareContact.js +66 -0
- package/src/apis/stickers.js +257 -0
- package/src/apis/story.js +181 -0
- package/src/apis/theme.js +233 -0
- package/src/apis/unfriend.js +47 -0
- package/src/apis/unsendMessage.js +17 -0
- package/src/apis/uploadAttachment.js +87 -0
- package/src/database/appStateBackup.js +189 -0
- package/src/database/models/index.js +56 -0
- package/src/database/models/thread.js +31 -0
- package/src/database/models/user.js +32 -0
- package/src/database/threadData.js +101 -0
- package/src/database/userData.js +90 -0
- package/src/engine/client.js +92 -0
- package/src/engine/models/buildAPI.js +118 -0
- package/src/engine/models/loginHelper.js +492 -0
- package/src/engine/models/setOptions.js +88 -0
- package/src/types/index.d.ts +498 -0
- package/src/utils/antiSuspension.js +516 -0
- package/src/utils/auth-helpers.js +149 -0
- package/src/utils/autoReLogin.js +237 -0
- package/src/utils/axios.js +368 -0
- package/src/utils/cache.js +54 -0
- package/src/utils/clients.js +279 -0
- package/src/utils/constants.js +525 -0
- package/src/utils/formatters/data/formatAttachment.js +370 -0
- package/src/utils/formatters/data/formatDelta.js +109 -0
- package/src/utils/formatters/index.js +159 -0
- package/src/utils/formatters/value/formatCookie.js +91 -0
- package/src/utils/formatters/value/formatDate.js +36 -0
- package/src/utils/formatters/value/formatID.js +16 -0
- package/src/utils/formatters.js +1369 -0
- package/src/utils/headers.js +235 -0
- package/src/utils/index.js +152 -0
- package/src/utils/monitoring.js +333 -0
- package/src/utils/rateLimiter.js +251 -0
- package/src/utils/tokenRefresh.js +285 -0
- package/src/utils/user-agents.js +238 -0
- package/src/utils/validation.js +157 -0
|
@@ -0,0 +1,117 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../utils');
|
|
4
|
+
|
|
5
|
+
module.exports = (defaultFuncs, api, ctx) => {
|
|
6
|
+
return async function removeUserFromGroup(userID, threadID, callback) {
|
|
7
|
+
let resolveFunc = () => {};
|
|
8
|
+
let rejectFunc = () => {};
|
|
9
|
+
const returnPromise = new Promise((resolve, reject) => {
|
|
10
|
+
resolveFunc = resolve;
|
|
11
|
+
rejectFunc = reject;
|
|
12
|
+
});
|
|
13
|
+
|
|
14
|
+
if (!callback) {
|
|
15
|
+
callback = (err, result) => {
|
|
16
|
+
if (err) return rejectFunc(err);
|
|
17
|
+
resolveFunc(result);
|
|
18
|
+
};
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
try {
|
|
22
|
+
if (utils.getType(threadID) !== "Number" && utils.getType(threadID) !== "String") {
|
|
23
|
+
throw new Error("threadID should be of type Number or String");
|
|
24
|
+
}
|
|
25
|
+
if (utils.getType(userID) !== "Number" && utils.getType(userID) !== "String") {
|
|
26
|
+
throw new Error("userID should be of type Number or String");
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
if (ctx.mqttClient) {
|
|
30
|
+
const reqID = ++ctx.wsReqNumber;
|
|
31
|
+
const taskID = ++ctx.wsTaskNumber;
|
|
32
|
+
|
|
33
|
+
const payload = {
|
|
34
|
+
epoch_id: utils.generateOfflineThreadingID(),
|
|
35
|
+
tasks: [
|
|
36
|
+
{
|
|
37
|
+
failure_count: null,
|
|
38
|
+
label: '140',
|
|
39
|
+
payload: JSON.stringify({
|
|
40
|
+
thread_id: threadID,
|
|
41
|
+
contact_id: userID,
|
|
42
|
+
sync_group: 1
|
|
43
|
+
}),
|
|
44
|
+
queue_name: 'remove_participant_v2',
|
|
45
|
+
task_id: taskID
|
|
46
|
+
}
|
|
47
|
+
],
|
|
48
|
+
version_id: '8798795233522156'
|
|
49
|
+
};
|
|
50
|
+
|
|
51
|
+
const form = JSON.stringify({
|
|
52
|
+
app_id: "2220391788200892",
|
|
53
|
+
payload: JSON.stringify(payload),
|
|
54
|
+
request_id: reqID,
|
|
55
|
+
type: 3
|
|
56
|
+
});
|
|
57
|
+
|
|
58
|
+
let responseHandled = false;
|
|
59
|
+
const handleRes = (topic, message) => {
|
|
60
|
+
if (topic !== "/ls_resp" || responseHandled) return;
|
|
61
|
+
let jsonMsg;
|
|
62
|
+
try {
|
|
63
|
+
jsonMsg = JSON.parse(message.toString());
|
|
64
|
+
jsonMsg.payload = JSON.parse(jsonMsg.payload);
|
|
65
|
+
} catch {
|
|
66
|
+
return;
|
|
67
|
+
}
|
|
68
|
+
if (jsonMsg.request_id !== reqID) return;
|
|
69
|
+
responseHandled = true;
|
|
70
|
+
ctx.mqttClient.removeListener("message", handleRes);
|
|
71
|
+
callback(null, { success: true });
|
|
72
|
+
resolveFunc({ success: true });
|
|
73
|
+
};
|
|
74
|
+
|
|
75
|
+
const timeout = setTimeout(() => {
|
|
76
|
+
if (!responseHandled) {
|
|
77
|
+
responseHandled = true;
|
|
78
|
+
ctx.mqttClient.removeListener("message", handleRes);
|
|
79
|
+
const err = new Error("MQTT request timeout");
|
|
80
|
+
callback(err);
|
|
81
|
+
rejectFunc(err);
|
|
82
|
+
}
|
|
83
|
+
}, 30000);
|
|
84
|
+
|
|
85
|
+
ctx.mqttClient.on("message", handleRes);
|
|
86
|
+
ctx.mqttClient.publish("/ls_req", form, { qos: 1, retain: false }, (err) => {
|
|
87
|
+
if (err && !responseHandled) {
|
|
88
|
+
responseHandled = true;
|
|
89
|
+
clearTimeout(timeout);
|
|
90
|
+
ctx.mqttClient.removeListener("message", handleRes);
|
|
91
|
+
callback(err);
|
|
92
|
+
rejectFunc(err);
|
|
93
|
+
}
|
|
94
|
+
});
|
|
95
|
+
} else {
|
|
96
|
+
const form = {
|
|
97
|
+
uid: userID,
|
|
98
|
+
tid: threadID
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const res = await defaultFuncs.post("https://www.facebook.com/chat/remove_participants", ctx.jar, form)
|
|
102
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
103
|
+
|
|
104
|
+
if (!res || res.error) {
|
|
105
|
+
throw res || new Error("Remove from group failed");
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
callback(null, { success: true });
|
|
109
|
+
}
|
|
110
|
+
} catch (err) {
|
|
111
|
+
utils.error("removeUserFromGroup", err);
|
|
112
|
+
callback(err);
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
return returnPromise;
|
|
116
|
+
};
|
|
117
|
+
};
|
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../utils');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* @module resolvePhotoUrl
|
|
7
|
+
* @description Fetches the direct URL of a Facebook photo using its photo ID.
|
|
8
|
+
* @param {Object} defaultFuncs - An object containing default request functions.
|
|
9
|
+
* @param {Object} api - Facebook API object (unused here but kept for compatibility).
|
|
10
|
+
* @param {Object} ctx - Context object containing cookies (jar) and other session info.
|
|
11
|
+
* @returns {Function} resolvePhotoUrl - A function that takes a photo ID and optional callback, and returns a Promise resolving to the photo URL.
|
|
12
|
+
*/
|
|
13
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
14
|
+
/**
|
|
15
|
+
* @function resolvePhotoUrl
|
|
16
|
+
* @param {string} photoID - The ID of the Facebook photo to resolve.
|
|
17
|
+
* @param {Function} [callback] - Optional Node-style callback function `(err, photoUrl)`.
|
|
18
|
+
* @returns {Promise<string>} A Promise that resolves to the direct photo URL.
|
|
19
|
+
*/
|
|
20
|
+
return function resolvePhotoUrl(photoID, callback) {
|
|
21
|
+
let resolveFunc = function () {};
|
|
22
|
+
let rejectFunc = function () {};
|
|
23
|
+
const returnPromise = new Promise(function (resolve, reject) {
|
|
24
|
+
resolveFunc = resolve;
|
|
25
|
+
rejectFunc = reject;
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
if (!callback) {
|
|
29
|
+
callback = function (err, photoUrl) {
|
|
30
|
+
if (err) {
|
|
31
|
+
return rejectFunc(err);
|
|
32
|
+
}
|
|
33
|
+
resolveFunc(photoUrl);
|
|
34
|
+
};
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
defaultFuncs
|
|
38
|
+
.get("https://www.facebook.com/mercury/attachments/photo", ctx.jar, {
|
|
39
|
+
photo_id: photoID,
|
|
40
|
+
})
|
|
41
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
|
42
|
+
.then((resData) => {
|
|
43
|
+
if (resData.error) {
|
|
44
|
+
throw resData;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
const photoUrl = resData.jsmods.require[0][3][0];
|
|
48
|
+
|
|
49
|
+
return callback(null, photoUrl);
|
|
50
|
+
})
|
|
51
|
+
.catch((err) => {
|
|
52
|
+
utils.error("resolvePhotoUrl", err);
|
|
53
|
+
return callback(err);
|
|
54
|
+
});
|
|
55
|
+
|
|
56
|
+
return returnPromise;
|
|
57
|
+
};
|
|
58
|
+
};
|
|
@@ -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,306 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../utils');
|
|
4
|
+
|
|
5
|
+
/**
|
|
6
|
+
* Facebook Messenger send effects — visual animations that play when a message is sent/received.
|
|
7
|
+
*
|
|
8
|
+
* Usage:
|
|
9
|
+
* api.sendEffect.fire('Hello!', threadID)
|
|
10
|
+
* api.sendEffect.send('love', 'Hello!', threadID)
|
|
11
|
+
* api.sendEffect.list() → [{tag, aliases, emoji}, ...]
|
|
12
|
+
*/
|
|
13
|
+
|
|
14
|
+
const EFFECTS = {
|
|
15
|
+
fire: { tag: 'FIRE', emoji: '🔥', label: 'Fire' },
|
|
16
|
+
love: { tag: 'LOVE', emoji: '❤️', label: 'Love' },
|
|
17
|
+
heart: { tag: 'LOVE', emoji: '❤️', label: 'Love' },
|
|
18
|
+
hearts: { tag: 'LOVE', emoji: '❤️', label: 'Love' },
|
|
19
|
+
confetti: { tag: 'CELEBRATION', emoji: '🎉', label: 'Confetti' },
|
|
20
|
+
celebration: { tag: 'CELEBRATION', emoji: '🎉', label: 'Confetti' },
|
|
21
|
+
wand: { tag: 'WAND', emoji: '🪄', label: 'Wand' },
|
|
22
|
+
magic: { tag: 'WAND', emoji: '🪄', label: 'Wand' },
|
|
23
|
+
fireworks: { tag: 'FIREWORKS', emoji: '🎆', label: 'Fireworks' },
|
|
24
|
+
shooting_star: { tag: 'SHOOTING_STAR', emoji: '🌠', label: 'Shooting Star' },
|
|
25
|
+
stars: { tag: 'SHOOTING_STAR', emoji: '🌠', label: 'Shooting Star' },
|
|
26
|
+
starstruck: { tag: 'SHOOTING_STAR', emoji: '🌠', label: 'Shooting Star' },
|
|
27
|
+
balloons: { tag: 'BIRTHDAY', emoji: '🎈', label: 'Balloons' },
|
|
28
|
+
birthday: { tag: 'BIRTHDAY', emoji: '🎈', label: 'Balloons' },
|
|
29
|
+
horror: { tag: 'HORROR', emoji: '👻', label: 'Horror' },
|
|
30
|
+
halloween: { tag: 'HORROR', emoji: '👻', label: 'Horror' },
|
|
31
|
+
las_vegas: { tag: 'LAS_VEGAS', emoji: '🎰', label: 'Las Vegas' },
|
|
32
|
+
vegas: { tag: 'LAS_VEGAS', emoji: '🎰', label: 'Las Vegas' },
|
|
33
|
+
anniversary: { tag: 'ANNIVERSARY', emoji: '💍', label: 'Anniversary' },
|
|
34
|
+
spotlight: { tag: 'SPOTLIGHT', emoji: '✨', label: 'Spotlight' },
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
function resolveEffect(name) {
|
|
38
|
+
if (!name) return null;
|
|
39
|
+
const key = String(name).toLowerCase().replace(/[\s\-]+/g, '_');
|
|
40
|
+
return EFFECTS[key] ? EFFECTS[key].tag : name.toUpperCase();
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
function buildForm(effectTag, msgObj, threadID, ctx) {
|
|
44
|
+
const messageAndOTID = utils.generateOfflineThreadingID();
|
|
45
|
+
const timestamp = Date.now();
|
|
46
|
+
const messagingTag = 'fb.messaging.effects.' + effectTag;
|
|
47
|
+
const body = typeof msgObj === 'string' ? msgObj : (msgObj.body || '');
|
|
48
|
+
|
|
49
|
+
const form = {
|
|
50
|
+
client: 'mercury',
|
|
51
|
+
action_type: 'ma-type:user-generated-message',
|
|
52
|
+
author: 'fbid:' + ctx.userID,
|
|
53
|
+
timestamp,
|
|
54
|
+
timestamp_absolute: 'Today',
|
|
55
|
+
timestamp_relative: utils.generateTimestampRelative(),
|
|
56
|
+
timestamp_time_passed:'0',
|
|
57
|
+
is_unread: false,
|
|
58
|
+
is_cleared: false,
|
|
59
|
+
is_forward: false,
|
|
60
|
+
is_filtered_content: false,
|
|
61
|
+
is_filtered_content_bh: false,
|
|
62
|
+
is_filtered_content_account: false,
|
|
63
|
+
is_filtered_content_quasar: false,
|
|
64
|
+
is_filtered_content_invalid_app: false,
|
|
65
|
+
is_spoof_warning: false,
|
|
66
|
+
source: 'source:chat:web',
|
|
67
|
+
'source_tags[0]': 'source:chat',
|
|
68
|
+
body,
|
|
69
|
+
html_body: false,
|
|
70
|
+
ui_push_phase: 'V3',
|
|
71
|
+
status: '0',
|
|
72
|
+
offline_threading_id: messageAndOTID,
|
|
73
|
+
message_id: messageAndOTID,
|
|
74
|
+
threading_id: utils.generateThreadingID(ctx.clientID),
|
|
75
|
+
'ephemeral_ttl_mode:':'0',
|
|
76
|
+
manual_retry_cnt: '0',
|
|
77
|
+
has_attachment: !!(msgObj && msgObj.sticker),
|
|
78
|
+
signatureID: utils.getSignatureID(),
|
|
79
|
+
has_lightweight_action: true,
|
|
80
|
+
'lightweight_action_attached[message_id]': messageAndOTID,
|
|
81
|
+
'lightweight_action_attached[messaging_tag]': messagingTag,
|
|
82
|
+
};
|
|
83
|
+
|
|
84
|
+
if (msgObj && msgObj.sticker) {
|
|
85
|
+
form.sticker_id = msgObj.sticker;
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
if (typeof msgObj === 'object' && msgObj.reply_to_message_id) {
|
|
89
|
+
form.replied_to_message_id = msgObj.reply_to_message_id;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
const tid = String(threadID);
|
|
93
|
+
const isGroup = tid.length >= 16;
|
|
94
|
+
if (isGroup) {
|
|
95
|
+
form.thread_fbid = tid;
|
|
96
|
+
} else {
|
|
97
|
+
form['specific_to_list[0]'] = 'fbid:' + tid;
|
|
98
|
+
form['specific_to_list[1]'] = 'fbid:' + ctx.userID;
|
|
99
|
+
form.other_user_fbid = tid;
|
|
100
|
+
form.client_thread_id = 'root:' + messageAndOTID;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
return { form, messageAndOTID, timestamp, messagingTag };
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
function buildMqttPayload(effectTag, msgObj, threadID, ctx) {
|
|
107
|
+
const otid = utils.generateOfflineThreadingID();
|
|
108
|
+
const epoch_id = utils.generateOfflineThreadingID();
|
|
109
|
+
const timestamp = Date.now();
|
|
110
|
+
const body = typeof msgObj === 'string' ? msgObj : (msgObj && msgObj.body || '');
|
|
111
|
+
const tid = String(threadID);
|
|
112
|
+
|
|
113
|
+
const sendPayload = {
|
|
114
|
+
thread_id: tid,
|
|
115
|
+
otid: otid.toString(),
|
|
116
|
+
source: 0,
|
|
117
|
+
send_type: 1,
|
|
118
|
+
sync_group: 1,
|
|
119
|
+
text: body,
|
|
120
|
+
initiating_source: 1,
|
|
121
|
+
skip_url_preview_gen: 0,
|
|
122
|
+
lightweight_action_attached: {
|
|
123
|
+
messaging_tag: 'fb.messaging.effects.' + effectTag,
|
|
124
|
+
},
|
|
125
|
+
};
|
|
126
|
+
|
|
127
|
+
if (typeof msgObj === 'object' && msgObj && msgObj.sticker) {
|
|
128
|
+
sendPayload.send_type = 2;
|
|
129
|
+
sendPayload.sticker_id = msgObj.sticker;
|
|
130
|
+
sendPayload.text = null;
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
const request_id = ++ctx.wsReqNumber;
|
|
134
|
+
const content = {
|
|
135
|
+
app_id: '2220391788200892',
|
|
136
|
+
payload: JSON.stringify({
|
|
137
|
+
tasks: [
|
|
138
|
+
{
|
|
139
|
+
label: '46',
|
|
140
|
+
payload: JSON.stringify(sendPayload),
|
|
141
|
+
queue_name: tid,
|
|
142
|
+
task_id: ++ctx.wsTaskNumber,
|
|
143
|
+
failure_count: null,
|
|
144
|
+
},
|
|
145
|
+
{
|
|
146
|
+
label: '21',
|
|
147
|
+
payload: JSON.stringify({
|
|
148
|
+
thread_id: tid,
|
|
149
|
+
last_read_watermark_ts: timestamp,
|
|
150
|
+
sync_group: 1,
|
|
151
|
+
}),
|
|
152
|
+
queue_name: tid,
|
|
153
|
+
task_id: ++ctx.wsTaskNumber,
|
|
154
|
+
failure_count: null,
|
|
155
|
+
},
|
|
156
|
+
],
|
|
157
|
+
epoch_id,
|
|
158
|
+
version_id: '6120284488008082',
|
|
159
|
+
data_trace_id: null,
|
|
160
|
+
}),
|
|
161
|
+
request_id,
|
|
162
|
+
type: 3,
|
|
163
|
+
};
|
|
164
|
+
|
|
165
|
+
return { content, request_id, otid, timestamp };
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
169
|
+
/**
|
|
170
|
+
* Lists all available send effects.
|
|
171
|
+
* @returns {Array<{tag, label, emoji, aliases}>}
|
|
172
|
+
*/
|
|
173
|
+
function list() {
|
|
174
|
+
const seen = new Set();
|
|
175
|
+
const result = [];
|
|
176
|
+
for (const [alias, { tag, label, emoji }] of Object.entries(EFFECTS)) {
|
|
177
|
+
if (!seen.has(tag)) {
|
|
178
|
+
seen.add(tag);
|
|
179
|
+
result.push({
|
|
180
|
+
tag,
|
|
181
|
+
label,
|
|
182
|
+
emoji,
|
|
183
|
+
aliases: Object.entries(EFFECTS).filter(([, v]) => v.tag === tag).map(([k]) => k),
|
|
184
|
+
});
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return result;
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
/**
|
|
191
|
+
* Sends a message with a visual effect animation.
|
|
192
|
+
*
|
|
193
|
+
* @param {string} effectName Effect alias or tag (e.g. 'fire', 'LOVE', 'confetti')
|
|
194
|
+
* @param {string|object} message Message text or object ({ body, sticker, reply_to_message_id })
|
|
195
|
+
* @param {string|number} threadID Thread / conversation ID
|
|
196
|
+
* @param {Function} [callback] Optional callback(err, { messageID, threadID, effect, timestamp })
|
|
197
|
+
* @returns {Promise}
|
|
198
|
+
*/
|
|
199
|
+
async function send(effectName, message, threadID, callback) {
|
|
200
|
+
let resolveFunc, rejectFunc;
|
|
201
|
+
const returnPromise = new Promise((resolve, reject) => {
|
|
202
|
+
resolveFunc = resolve;
|
|
203
|
+
rejectFunc = reject;
|
|
204
|
+
});
|
|
205
|
+
if (!callback) {
|
|
206
|
+
callback = (err, data) => { if (err) return rejectFunc(err); resolveFunc(data); };
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
try {
|
|
210
|
+
if (!effectName) return callback(new Error('effectName is required.'));
|
|
211
|
+
if (message === undefined || message === null) return callback(new Error('message is required.'));
|
|
212
|
+
if (!threadID) return callback(new Error('threadID is required.'));
|
|
213
|
+
|
|
214
|
+
const effectTag = resolveEffect(effectName);
|
|
215
|
+
utils.log('sendEffect', `Effect "${effectTag}" → thread ${threadID}`);
|
|
216
|
+
|
|
217
|
+
// ── Try HTTP first ───────────────────────────────────────────────────
|
|
218
|
+
try {
|
|
219
|
+
const { form, messageAndOTID, timestamp } = buildForm(effectTag, message, threadID, ctx);
|
|
220
|
+
|
|
221
|
+
const resData = await defaultFuncs
|
|
222
|
+
.post('https://www.facebook.com/messaging/send/', ctx.jar, form, { ...ctx, requestThreadID: String(threadID) })
|
|
223
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
224
|
+
|
|
225
|
+
if (!resData) throw new Error('Empty response from messaging/send');
|
|
226
|
+
if (resData.error) throw new Error(JSON.stringify(resData));
|
|
227
|
+
|
|
228
|
+
const actions = (resData.payload && resData.payload.actions) || [];
|
|
229
|
+
const msgInfo = actions.reduce((p, v) => ({
|
|
230
|
+
threadID: v.thread_fbid || p.threadID,
|
|
231
|
+
messageID: v.message_id || p.messageID,
|
|
232
|
+
timestamp: v.timestamp || p.timestamp,
|
|
233
|
+
}), { threadID, messageID: messageAndOTID, timestamp });
|
|
234
|
+
|
|
235
|
+
return callback(null, { ...msgInfo, effect: effectTag, method: 'http' });
|
|
236
|
+
} catch (httpErr) {
|
|
237
|
+
utils.warn('sendEffect', `HTTP failed: ${httpErr.message}. Falling back to MQTT.`);
|
|
238
|
+
}
|
|
239
|
+
|
|
240
|
+
// ── MQTT fallback ────────────────────────────────────────────────────
|
|
241
|
+
if (!ctx.mqttClient) {
|
|
242
|
+
throw new Error('MQTT is not connected. Call api.listenMqtt() first.');
|
|
243
|
+
}
|
|
244
|
+
|
|
245
|
+
const { content, request_id, otid, timestamp } = buildMqttPayload(effectTag, message, threadID, ctx);
|
|
246
|
+
|
|
247
|
+
await new Promise((res, rej) => {
|
|
248
|
+
let done = false;
|
|
249
|
+
const timer = setTimeout(() => {
|
|
250
|
+
if (done) return;
|
|
251
|
+
done = true;
|
|
252
|
+
ctx.mqttClient.removeListener('message', onMsg);
|
|
253
|
+
rej(new Error('MQTT effect send timeout'));
|
|
254
|
+
}, 15000);
|
|
255
|
+
|
|
256
|
+
const onMsg = (topic, raw) => {
|
|
257
|
+
if (topic !== '/ls_resp') return;
|
|
258
|
+
let parsed;
|
|
259
|
+
try { parsed = JSON.parse(raw.toString()); parsed.payload = JSON.parse(parsed.payload); } catch { return; }
|
|
260
|
+
if (parsed.request_id !== request_id) return;
|
|
261
|
+
if (done) return;
|
|
262
|
+
done = true;
|
|
263
|
+
clearTimeout(timer);
|
|
264
|
+
ctx.mqttClient.removeListener('message', onMsg);
|
|
265
|
+
callback(null, { threadID, messageID: otid, timestamp, effect: effectTag, method: 'mqtt' });
|
|
266
|
+
res();
|
|
267
|
+
};
|
|
268
|
+
|
|
269
|
+
ctx.mqttClient.on('message', onMsg);
|
|
270
|
+
ctx.mqttClient.publish('/ls_req', JSON.stringify(content), { qos: 1, retain: false }, (err) => {
|
|
271
|
+
if (err && !done) {
|
|
272
|
+
done = true;
|
|
273
|
+
clearTimeout(timer);
|
|
274
|
+
ctx.mqttClient.removeListener('message', onMsg);
|
|
275
|
+
rej(err);
|
|
276
|
+
}
|
|
277
|
+
});
|
|
278
|
+
});
|
|
279
|
+
|
|
280
|
+
} catch (err) {
|
|
281
|
+
utils.error('sendEffect', err.message || err);
|
|
282
|
+
callback(err instanceof Error ? err : new Error(String(err.message || err)));
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
return returnPromise;
|
|
286
|
+
}
|
|
287
|
+
|
|
288
|
+
// ── Convenience shortcuts ──────────────────────────────────────────────────
|
|
289
|
+
const shortcuts = {};
|
|
290
|
+
const seen = new Set();
|
|
291
|
+
for (const [alias, { tag }] of Object.entries(EFFECTS)) {
|
|
292
|
+
if (!seen.has(tag)) {
|
|
293
|
+
seen.add(tag);
|
|
294
|
+
const _tag = tag;
|
|
295
|
+
shortcuts[alias] = (message, threadID, callback) => send(_tag, message, threadID, callback);
|
|
296
|
+
}
|
|
297
|
+
}
|
|
298
|
+
|
|
299
|
+
return {
|
|
300
|
+
list,
|
|
301
|
+
send,
|
|
302
|
+
effects: EFFECTS,
|
|
303
|
+
resolveEffect,
|
|
304
|
+
...shortcuts,
|
|
305
|
+
};
|
|
306
|
+
};
|