fca-phantom 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 +58 -0
- package/README.md +534 -0
- package/index.js +35 -0
- package/package.json +101 -0
- package/phantom/core/builder/bootstrap.js +334 -0
- package/phantom/core/builder/config.js +78 -0
- package/phantom/core/builder/forge.js +113 -0
- package/phantom/core/builder/ignite.js +386 -0
- package/phantom/core/builder/options.js +61 -0
- package/phantom/core/engine.js +71 -0
- package/phantom/core/reactor.js +2 -0
- package/phantom/datastore/appState.js +2 -0
- package/phantom/datastore/appStateBackup.js +34 -0
- package/phantom/datastore/models/cipher/e2ee.js +48 -0
- package/phantom/datastore/models/cipher/vault.js +153 -0
- package/phantom/datastore/models/index.js +3 -0
- package/phantom/datastore/models/matrix/auth.js +151 -0
- package/phantom/datastore/models/matrix/cache.js +3 -0
- package/phantom/datastore/models/matrix/checker.js +2 -0
- package/phantom/datastore/models/matrix/clients.js +2 -0
- package/phantom/datastore/models/matrix/constants.js +2 -0
- package/phantom/datastore/models/matrix/credentials.js +2 -0
- package/phantom/datastore/models/matrix/cycle.js +2 -0
- package/phantom/datastore/models/matrix/gate.js +282 -0
- package/phantom/datastore/models/matrix/ghost.js +332 -0
- package/phantom/datastore/models/matrix/headers.js +193 -0
- package/phantom/datastore/models/matrix/heartbeat.js +298 -0
- package/phantom/datastore/models/matrix/identity.js +235 -0
- package/phantom/datastore/models/matrix/logger.js +271 -0
- package/phantom/datastore/models/matrix/monitor.js +2 -0
- package/phantom/datastore/models/matrix/net.js +316 -0
- package/phantom/datastore/models/matrix/response.js +193 -0
- package/phantom/datastore/models/matrix/revive.js +255 -0
- package/phantom/datastore/models/matrix/signals.js +2 -0
- package/phantom/datastore/models/matrix/store.js +263 -0
- package/phantom/datastore/models/matrix/telemetry.js +272 -0
- package/phantom/datastore/models/matrix/tools.js +93 -0
- package/phantom/datastore/models/matrix/transform/cookieParser.js +2 -0
- package/phantom/datastore/models/matrix/transform/cookies.js +114 -0
- package/phantom/datastore/models/matrix/transform/index.js +203 -0
- package/phantom/datastore/models/matrix/validator.js +157 -0
- package/phantom/datastore/models/types/index.d.ts +498 -0
- package/phantom/datastore/schema.js +167 -0
- package/phantom/datastore/session.js +129 -0
- package/phantom/datastore/threads.js +22 -0
- package/phantom/datastore/users.js +26 -0
- package/phantom/dispatch/addExternalModule.js +239 -0
- package/phantom/dispatch/addUserToGroup.js +161 -0
- package/phantom/dispatch/changeAdminStatus.js +142 -0
- package/phantom/dispatch/changeArchivedStatus.js +135 -0
- package/phantom/dispatch/changeAvatar.js +123 -0
- package/phantom/dispatch/changeBio.js +86 -0
- package/phantom/dispatch/changeBlockedStatus.js +86 -0
- package/phantom/dispatch/changeGroupImage.js +145 -0
- package/phantom/dispatch/changeThreadColor.js +172 -0
- package/phantom/dispatch/changeThreadEmoji.js +130 -0
- package/phantom/dispatch/comment.js +136 -0
- package/phantom/dispatch/createAITheme.js +333 -0
- package/phantom/dispatch/createNewGroup.js +99 -0
- package/phantom/dispatch/createPoll.js +148 -0
- package/phantom/dispatch/deleteMessage.js +131 -0
- package/phantom/dispatch/deleteThread.js +155 -0
- package/phantom/dispatch/e2ee.js +101 -0
- package/phantom/dispatch/editMessage.js +158 -0
- package/phantom/dispatch/emoji.js +143 -0
- package/phantom/dispatch/fetchThemeData.js +233 -0
- package/phantom/dispatch/follow.js +111 -0
- package/phantom/dispatch/forwardMessage.js +110 -0
- package/phantom/dispatch/friend.js +189 -0
- package/phantom/dispatch/gcmember.js +138 -0
- package/phantom/dispatch/gcname.js +131 -0
- package/phantom/dispatch/gcrule.js +111 -0
- package/phantom/dispatch/getAccess.js +109 -0
- package/phantom/dispatch/getBotInfo.js +81 -0
- package/phantom/dispatch/getBotInitialData.js +110 -0
- package/phantom/dispatch/getFriendsList.js +118 -0
- package/phantom/dispatch/getMessage.js +199 -0
- package/phantom/dispatch/getTheme.js +199 -0
- package/phantom/dispatch/getThemeInfo.js +160 -0
- package/phantom/dispatch/getThreadHistory.js +139 -0
- package/phantom/dispatch/getThreadInfo.js +153 -0
- package/phantom/dispatch/getThreadList.js +132 -0
- package/phantom/dispatch/getThreadPictures.js +93 -0
- package/phantom/dispatch/getUserID.js +147 -0
- package/phantom/dispatch/getUserInfo.js +513 -0
- package/phantom/dispatch/getUserInfoV2.js +146 -0
- package/phantom/dispatch/handleMessageRequest.js +50 -0
- package/phantom/dispatch/httpGet.js +63 -0
- package/phantom/dispatch/httpPost.js +89 -0
- package/phantom/dispatch/httpPostFormData.js +69 -0
- package/phantom/dispatch/listenMqtt.js +1236 -0
- package/phantom/dispatch/listenSpeed.js +179 -0
- package/phantom/dispatch/logout.js +93 -0
- package/phantom/dispatch/markAsDelivered.js +92 -0
- package/phantom/dispatch/markAsRead.js +119 -0
- package/phantom/dispatch/markAsReadAll.js +215 -0
- package/phantom/dispatch/markAsSeen.js +70 -0
- package/phantom/dispatch/mqttDeltaValue.js +278 -0
- package/phantom/dispatch/muteThread.js +253 -0
- package/phantom/dispatch/nickname.js +132 -0
- package/phantom/dispatch/notes.js +263 -0
- package/phantom/dispatch/pinMessage.js +238 -0
- package/phantom/dispatch/produceMetaTheme.js +335 -0
- package/phantom/dispatch/realtime.js +291 -0
- package/phantom/dispatch/removeUserFromGroup.js +248 -0
- package/phantom/dispatch/resolvePhotoUrl.js +217 -0
- package/phantom/dispatch/searchForThread.js +258 -0
- package/phantom/dispatch/sendMessage.js +354 -0
- package/phantom/dispatch/sendMessageMqtt.js +249 -0
- package/phantom/dispatch/sendTypingIndicator.js +206 -0
- package/phantom/dispatch/setMessageReaction.js +188 -0
- package/phantom/dispatch/setMessageReactionMqtt.js +248 -0
- package/phantom/dispatch/setThreadTheme.js +330 -0
- package/phantom/dispatch/setThreadThemeMqtt.js +207 -0
- package/phantom/dispatch/share.js +200 -0
- package/phantom/dispatch/shareContact.js +216 -0
- package/phantom/dispatch/stickers.js +395 -0
- package/phantom/dispatch/story.js +240 -0
- package/phantom/dispatch/theme.js +296 -0
- package/phantom/dispatch/unfriend.js +199 -0
- package/phantom/dispatch/unsendMessage.js +124 -0
|
@@ -0,0 +1,131 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* deleteMessage — Advanced Message Deletion for Phantom SDK
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Primary: GraphQL mutation (newer approach, supports recall)
|
|
8
|
+
* - Fallback: Legacy mercury delete endpoint
|
|
9
|
+
* - Batch delete: pass an array of messageIDs
|
|
10
|
+
* - Configurable concurrency for batch operations
|
|
11
|
+
* - Exponential-backoff retry on transient errors
|
|
12
|
+
* - Detailed success/failure per-message result objects
|
|
13
|
+
* - Full callback + Promise dual API
|
|
14
|
+
*/
|
|
15
|
+
|
|
16
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
17
|
+
|
|
18
|
+
async function deleteGraphQL(defaultFuncs, ctx, messageID) {
|
|
19
|
+
const form = {
|
|
20
|
+
av: ctx.userID,
|
|
21
|
+
__user: ctx.userID,
|
|
22
|
+
__a: 1,
|
|
23
|
+
fb_dtsg: ctx.fb_dtsg,
|
|
24
|
+
lsd: ctx.lsd || ctx.fb_dtsg,
|
|
25
|
+
fb_api_caller_class: "RelayModern",
|
|
26
|
+
fb_api_req_friendly_name: "useDeleteMessageMutation",
|
|
27
|
+
variables: JSON.stringify({
|
|
28
|
+
input: {
|
|
29
|
+
client_mutation_id: String(Date.now() % 1e9),
|
|
30
|
+
actor_id: ctx.userID,
|
|
31
|
+
message_id: messageID,
|
|
32
|
+
delete_type: "DELETE_FOR_ME",
|
|
33
|
+
}
|
|
34
|
+
}),
|
|
35
|
+
server_timestamps: true,
|
|
36
|
+
doc_id: "6090627834316949",
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const res = await defaultFuncs.post("https://www.facebook.com/api/graphql/", ctx.jar, form);
|
|
40
|
+
const checked = await utils.parseAndCheckLogin(ctx, defaultFuncs)(res);
|
|
41
|
+
if (checked?.errors) throw Object.assign(new Error(checked.errors[0]?.message || JSON.stringify(checked.errors)), { isFatal: true });
|
|
42
|
+
return { success: true, messageID, method: 'graphql' };
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
async function deleteLegacy(defaultFuncs, ctx, messageID) {
|
|
46
|
+
const res = await defaultFuncs.post(
|
|
47
|
+
"https://www.facebook.com/ajax/mercury/delete_messages.php",
|
|
48
|
+
ctx.jar,
|
|
49
|
+
{ message_id: messageID }
|
|
50
|
+
).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
51
|
+
|
|
52
|
+
if (res?.error) throw new Error(JSON.stringify(res.error));
|
|
53
|
+
return { success: true, messageID, method: 'legacy' };
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function deleteOne(defaultFuncs, ctx, messageID, retries, baseMs) {
|
|
57
|
+
let lastErr;
|
|
58
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
59
|
+
try {
|
|
60
|
+
return await deleteGraphQL(defaultFuncs, ctx, messageID);
|
|
61
|
+
} catch (gqlErr) {
|
|
62
|
+
try {
|
|
63
|
+
return await deleteLegacy(defaultFuncs, ctx, messageID);
|
|
64
|
+
} catch (legacyErr) {
|
|
65
|
+
lastErr = gqlErr;
|
|
66
|
+
if (attempt >= retries || gqlErr.isFatal) throw gqlErr;
|
|
67
|
+
const wait = baseMs * Math.pow(2, attempt - 1) + Math.random() * 200;
|
|
68
|
+
utils.warn("deleteMessage", `Retry ${attempt}/${retries} for ${messageID} in ${Math.round(wait)}ms`);
|
|
69
|
+
await new Promise(r => setTimeout(r, wait));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
}
|
|
73
|
+
throw lastErr;
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
module.exports = (defaultFuncs, api, ctx) => {
|
|
77
|
+
|
|
78
|
+
/**
|
|
79
|
+
* Delete one or more messages.
|
|
80
|
+
*
|
|
81
|
+
* @param {string|string[]} messageID Single ID or array for batch delete
|
|
82
|
+
* @param {object} [options]
|
|
83
|
+
* @param {number} [options.retries=3] Max retry attempts per message
|
|
84
|
+
* @param {number} [options.concurrency=3] Max parallel deletions in batch mode
|
|
85
|
+
* @param {Function} [callback] Node-style (err, result) callback
|
|
86
|
+
* @returns {Promise<object|object[]>}
|
|
87
|
+
*/
|
|
88
|
+
return async function deleteMessage(messageID, options, callback) {
|
|
89
|
+
if (typeof options === 'function') { callback = options; options = {}; }
|
|
90
|
+
options = options || {};
|
|
91
|
+
|
|
92
|
+
const retries = options.retries || 3;
|
|
93
|
+
const concurrency = options.concurrency || 3;
|
|
94
|
+
const baseMs = 500;
|
|
95
|
+
|
|
96
|
+
let resolveFunc, rejectFunc;
|
|
97
|
+
const returnPromise = new Promise((resolve, reject) => { resolveFunc = resolve; rejectFunc = reject; });
|
|
98
|
+
|
|
99
|
+
function done(err, result) {
|
|
100
|
+
if (callback) return err ? callback(err) : callback(null, result);
|
|
101
|
+
if (err) rejectFunc(err); else resolveFunc(result);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
const ids = Array.isArray(messageID) ? messageID : [messageID];
|
|
105
|
+
|
|
106
|
+
if (!ids.length || ids.some(id => !id)) {
|
|
107
|
+
done(new Error("deleteMessage: messageID is required"));
|
|
108
|
+
return returnPromise;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
const results = [];
|
|
113
|
+
// Process in concurrency-limited batches
|
|
114
|
+
for (let i = 0; i < ids.length; i += concurrency) {
|
|
115
|
+
const batch = ids.slice(i, i + concurrency);
|
|
116
|
+
const batchR = await Promise.allSettled(batch.map(id => deleteOne(defaultFuncs, ctx, id, retries, baseMs)));
|
|
117
|
+
for (const r of batchR) {
|
|
118
|
+
results.push(r.status === 'fulfilled' ? r.value : { success: false, messageID: batch[results.length % batch.length], error: r.reason?.message });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const out = ids.length === 1 ? results[0] : { success: results.every(r => r.success), results };
|
|
123
|
+
done(null, out);
|
|
124
|
+
} catch (err) {
|
|
125
|
+
utils.error("deleteMessage", err.message || err);
|
|
126
|
+
done(err);
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
return returnPromise;
|
|
130
|
+
};
|
|
131
|
+
};
|
|
@@ -0,0 +1,155 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
4
|
+
const { globalShield } = require('../datastore/models/matrix/ghost');
|
|
5
|
+
|
|
6
|
+
const CHUNK_SIZE = 10;
|
|
7
|
+
const DELETE_DELAY_MS = 500;
|
|
8
|
+
|
|
9
|
+
async function retryOp(fn, retries = 3, base = 500) {
|
|
10
|
+
for (let i = 0; i < retries; i++) {
|
|
11
|
+
try { return await fn(); } catch (err) {
|
|
12
|
+
if (i === retries - 1) throw err;
|
|
13
|
+
await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 200));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function deleteChunk(defaultFuncs, ctx, chunk) {
|
|
19
|
+
const form = { client: "mercury" };
|
|
20
|
+
chunk.forEach((id, i) => { form[`ids[${i}]`] = id; });
|
|
21
|
+
const res = await defaultFuncs.post(
|
|
22
|
+
"https://www.facebook.com/ajax/mercury/delete_thread.php",
|
|
23
|
+
ctx.jar,
|
|
24
|
+
form
|
|
25
|
+
).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
26
|
+
if (res && res.error) throw res;
|
|
27
|
+
return chunk;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
async function mqttDeleteThread(ctx, threadIDs) {
|
|
31
|
+
return new Promise((resolve, reject) => {
|
|
32
|
+
if (!ctx.mqttClient) return reject(new Error("MQTT not connected"));
|
|
33
|
+
|
|
34
|
+
const reqID = ++ctx.wsReqNumber;
|
|
35
|
+
const tasks = threadIDs.map((id) => ({
|
|
36
|
+
failure_count: null,
|
|
37
|
+
label: "19",
|
|
38
|
+
payload: JSON.stringify({ thread_key: id, sync_group: 1 }),
|
|
39
|
+
queue_name: "delete_thread",
|
|
40
|
+
task_id: ++ctx.wsTaskNumber
|
|
41
|
+
}));
|
|
42
|
+
|
|
43
|
+
const form = JSON.stringify({
|
|
44
|
+
app_id: "2220391788200892",
|
|
45
|
+
payload: JSON.stringify({
|
|
46
|
+
epoch_id: utils.generateOfflineThreadingID(),
|
|
47
|
+
tasks,
|
|
48
|
+
version_id: "8798795233522156"
|
|
49
|
+
}),
|
|
50
|
+
request_id: reqID,
|
|
51
|
+
type: 3
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
let handled = false;
|
|
55
|
+
const onResp = (topic, message) => {
|
|
56
|
+
if (topic !== "/ls_resp" || handled) return;
|
|
57
|
+
let j;
|
|
58
|
+
try { j = JSON.parse(message.toString()); j.payload = JSON.parse(j.payload); } catch { return; }
|
|
59
|
+
if (j.request_id !== reqID) return;
|
|
60
|
+
handled = true;
|
|
61
|
+
clearTimeout(timer);
|
|
62
|
+
ctx.mqttClient.removeListener("message", onResp);
|
|
63
|
+
resolve({ success: true, deletedThreads: threadIDs, method: "mqtt" });
|
|
64
|
+
};
|
|
65
|
+
|
|
66
|
+
const timer = setTimeout(() => {
|
|
67
|
+
if (!handled) {
|
|
68
|
+
handled = true;
|
|
69
|
+
ctx.mqttClient.removeListener("message", onResp);
|
|
70
|
+
resolve({ success: true, deletedThreads: threadIDs, method: "mqtt_no_ack" });
|
|
71
|
+
}
|
|
72
|
+
}, 15000);
|
|
73
|
+
|
|
74
|
+
ctx.mqttClient.on("message", onResp);
|
|
75
|
+
ctx.mqttClient.publish("/ls_req", form, { qos: 1, retain: false }, (err) => {
|
|
76
|
+
if (err && !handled) {
|
|
77
|
+
handled = true;
|
|
78
|
+
clearTimeout(timer);
|
|
79
|
+
ctx.mqttClient.removeListener("message", onResp);
|
|
80
|
+
reject(err);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
module.exports = (defaultFuncs, api, ctx) => {
|
|
87
|
+
return async function deleteThread(threadOrThreads, callback) {
|
|
88
|
+
let resolveFunc, rejectFunc;
|
|
89
|
+
const returnPromise = new Promise((resolve, reject) => {
|
|
90
|
+
resolveFunc = resolve;
|
|
91
|
+
rejectFunc = reject;
|
|
92
|
+
});
|
|
93
|
+
|
|
94
|
+
if (typeof callback !== "function") {
|
|
95
|
+
callback = (err, result) => {
|
|
96
|
+
if (err) return rejectFunc(err);
|
|
97
|
+
resolveFunc(result);
|
|
98
|
+
};
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
try {
|
|
102
|
+
const threads = Array.isArray(threadOrThreads)
|
|
103
|
+
? threadOrThreads.map(String)
|
|
104
|
+
: [String(threadOrThreads)];
|
|
105
|
+
|
|
106
|
+
if (threads.length === 0) throw new Error("deleteThread: no threadIDs provided");
|
|
107
|
+
|
|
108
|
+
await globalShield.addSmartDelay();
|
|
109
|
+
|
|
110
|
+
const deleted = [];
|
|
111
|
+
const failed = [];
|
|
112
|
+
|
|
113
|
+
if (ctx.mqttClient) {
|
|
114
|
+
for (let i = 0; i < threads.length; i += CHUNK_SIZE) {
|
|
115
|
+
const chunk = threads.slice(i, i + CHUNK_SIZE);
|
|
116
|
+
try {
|
|
117
|
+
const res = await retryOp(() => mqttDeleteThread(ctx, chunk));
|
|
118
|
+
deleted.push(...res.deletedThreads);
|
|
119
|
+
} catch (err) {
|
|
120
|
+
utils.warn("deleteThread", "MQTT chunk failed, using HTTP:", err.message);
|
|
121
|
+
try {
|
|
122
|
+
const res = await retryOp(() => deleteChunk(defaultFuncs, ctx, chunk));
|
|
123
|
+
deleted.push(...res);
|
|
124
|
+
} catch (httpErr) {
|
|
125
|
+
failed.push(...chunk);
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
if (i + CHUNK_SIZE < threads.length) {
|
|
129
|
+
await new Promise(r => setTimeout(r, DELETE_DELAY_MS));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
} else {
|
|
133
|
+
for (let i = 0; i < threads.length; i += CHUNK_SIZE) {
|
|
134
|
+
const chunk = threads.slice(i, i + CHUNK_SIZE);
|
|
135
|
+
try {
|
|
136
|
+
const res = await retryOp(() => deleteChunk(defaultFuncs, ctx, chunk));
|
|
137
|
+
deleted.push(...res);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
failed.push(...chunk);
|
|
140
|
+
}
|
|
141
|
+
if (i + CHUNK_SIZE < threads.length) {
|
|
142
|
+
await new Promise(r => setTimeout(r, DELETE_DELAY_MS));
|
|
143
|
+
}
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
callback(null, { success: true, deleted, failed, count: deleted.length });
|
|
148
|
+
} catch (err) {
|
|
149
|
+
utils.error("deleteThread", err);
|
|
150
|
+
callback(err);
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
return returnPromise;
|
|
154
|
+
};
|
|
155
|
+
};
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const e2ee = require('../datastore/models/cipher/e2ee');
|
|
4
|
+
|
|
5
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
6
|
+
const PEER_CACHE = new Map();
|
|
7
|
+
|
|
8
|
+
function getPeerCount() {
|
|
9
|
+
if (!ctx._e2eePeers) return 0;
|
|
10
|
+
return Object.keys(ctx._e2eePeers).length;
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
function getStats() {
|
|
14
|
+
return {
|
|
15
|
+
enabled: e2ee.isEnabled(ctx),
|
|
16
|
+
peerCount: getPeerCount(),
|
|
17
|
+
hasPublicKey: !!e2ee.getPublicKey(ctx),
|
|
18
|
+
peerIDs: ctx._e2eePeers ? Object.keys(ctx._e2eePeers) : []
|
|
19
|
+
};
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function exportKeyring() {
|
|
23
|
+
return {
|
|
24
|
+
publicKey: e2ee.getPublicKey(ctx),
|
|
25
|
+
peers: ctx._e2eePeers ? { ...ctx._e2eePeers } : {},
|
|
26
|
+
exportedAt: Date.now()
|
|
27
|
+
};
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
function importKeyring(keyring) {
|
|
31
|
+
if (!keyring || typeof keyring !== "object") throw new Error("e2ee.importKeyring: invalid keyring object");
|
|
32
|
+
if (keyring.peers && typeof keyring.peers === "object") {
|
|
33
|
+
for (const [threadID, key] of Object.entries(keyring.peers)) {
|
|
34
|
+
e2ee.setPeerKey(ctx, threadID, key);
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
return { success: true, importedPeers: Object.keys(keyring.peers || {}).length };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
function encryptBatch(entries) {
|
|
41
|
+
if (!Array.isArray(entries)) throw new Error("e2ee.encryptBatch: entries must be an array");
|
|
42
|
+
return entries.map(({ threadID, text }) => {
|
|
43
|
+
try {
|
|
44
|
+
return { threadID, encrypted: e2ee.encrypt(ctx, threadID, text), error: null };
|
|
45
|
+
} catch (err) {
|
|
46
|
+
return { threadID, encrypted: null, error: err.message };
|
|
47
|
+
}
|
|
48
|
+
});
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function decryptBatch(entries) {
|
|
52
|
+
if (!Array.isArray(entries)) throw new Error("e2ee.decryptBatch: entries must be an array");
|
|
53
|
+
return entries.map(({ threadID, armored }) => {
|
|
54
|
+
try {
|
|
55
|
+
return { threadID, decrypted: e2ee.decrypt(ctx, threadID, armored), error: null };
|
|
56
|
+
} catch (err) {
|
|
57
|
+
return { threadID, decrypted: null, error: err.message };
|
|
58
|
+
}
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function rotatePeerKey(threadID, newPeerPublicKeyB64) {
|
|
63
|
+
e2ee.clearPeerKey(ctx, threadID);
|
|
64
|
+
e2ee.setPeerKey(ctx, newPeerPublicKeyB64 ? threadID : null, newPeerPublicKeyB64);
|
|
65
|
+
return { success: true, threadID, rotated: !!newPeerPublicKeyB64 };
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
return {
|
|
69
|
+
enable() { e2ee.enable(ctx); return { success: true, enabled: true }; },
|
|
70
|
+
disable() { e2ee.disable(ctx); return { success: true, enabled: false }; },
|
|
71
|
+
isEnabled() { return e2ee.isEnabled(ctx); },
|
|
72
|
+
getPublicKey() { return e2ee.getPublicKey(ctx); },
|
|
73
|
+
setPeerKey(threadID, peerPublicKeyB64) {
|
|
74
|
+
if (!threadID) throw new Error("e2ee.setPeerKey: threadID is required");
|
|
75
|
+
if (!peerPublicKeyB64) throw new Error("e2ee.setPeerKey: peerPublicKeyB64 is required");
|
|
76
|
+
e2ee.setPeerKey(ctx, threadID, peerPublicKeyB64);
|
|
77
|
+
PEER_CACHE.set(threadID, { key: peerPublicKeyB64, ts: Date.now() });
|
|
78
|
+
return { success: true, threadID };
|
|
79
|
+
},
|
|
80
|
+
clearPeerKey(threadID) {
|
|
81
|
+
e2ee.clearPeerKey(ctx, threadID);
|
|
82
|
+
PEER_CACHE.delete(threadID);
|
|
83
|
+
return { success: true, threadID };
|
|
84
|
+
},
|
|
85
|
+
hasPeer(threadID) { return e2ee.hasPeer(ctx, threadID); },
|
|
86
|
+
encrypt(threadID, text) { return e2ee.encrypt(ctx, threadID, text); },
|
|
87
|
+
decrypt(threadID, armored) { return e2ee.decrypt(ctx, threadID, armored); },
|
|
88
|
+
getStats,
|
|
89
|
+
exportKeyring,
|
|
90
|
+
importKeyring,
|
|
91
|
+
encryptBatch,
|
|
92
|
+
decryptBatch,
|
|
93
|
+
rotatePeerKey,
|
|
94
|
+
clearAllPeers() {
|
|
95
|
+
const peers = ctx._e2eePeers ? Object.keys(ctx._e2eePeers) : [];
|
|
96
|
+
peers.forEach(id => e2ee.clearPeerKey(ctx, id));
|
|
97
|
+
PEER_CACHE.clear();
|
|
98
|
+
return { success: true, clearedCount: peers.length };
|
|
99
|
+
}
|
|
100
|
+
};
|
|
101
|
+
};
|
|
@@ -0,0 +1,158 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* editMessage — Advanced Message Editor for Phantom SDK
|
|
5
|
+
*
|
|
6
|
+
* @author RFS-ADRENO (original) — massively extended for phantom-fca
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - MQTT path (primary) — low-latency realtime edit via /ls_req
|
|
10
|
+
* - GraphQL mutation fallback — when MQTT unavailable or fails
|
|
11
|
+
* - Input validation (text length, messageID format)
|
|
12
|
+
* - Retry with exponential backoff on MQTT publish errors
|
|
13
|
+
* - Promise callback support (returns Promise when no callback given)
|
|
14
|
+
* - Preserves originalText + editedAt metadata in resolve value
|
|
15
|
+
*/
|
|
16
|
+
|
|
17
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
18
|
+
|
|
19
|
+
const MAX_TEXT_LENGTH = 20000;
|
|
20
|
+
|
|
21
|
+
async function editViaMqtt(ctx, text, messageID, retries, baseMs) {
|
|
22
|
+
if (!ctx.mqttClient) throw new Error("editMessage: MQTT client is not connected");
|
|
23
|
+
|
|
24
|
+
let lastErr;
|
|
25
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
26
|
+
ctx.wsReqNumber = (ctx.wsReqNumber || 0) + 1;
|
|
27
|
+
ctx.wsTaskNumber = (ctx.wsTaskNumber || 0) + 1;
|
|
28
|
+
|
|
29
|
+
const payload = {
|
|
30
|
+
failure_count: null,
|
|
31
|
+
label: '742',
|
|
32
|
+
payload: JSON.stringify({ message_id: messageID, text }),
|
|
33
|
+
queue_name: 'edit_message',
|
|
34
|
+
task_id: ctx.wsTaskNumber,
|
|
35
|
+
};
|
|
36
|
+
|
|
37
|
+
const content = {
|
|
38
|
+
app_id: '2220391788200892',
|
|
39
|
+
payload: JSON.stringify({
|
|
40
|
+
data_trace_id: null,
|
|
41
|
+
epoch_id: parseInt(utils.generateOfflineThreadingID ? utils.generateOfflineThreadingID() : Date.now()),
|
|
42
|
+
tasks: [payload],
|
|
43
|
+
version_id: '6903494529735864',
|
|
44
|
+
}),
|
|
45
|
+
request_id: ctx.wsReqNumber,
|
|
46
|
+
type: 3,
|
|
47
|
+
};
|
|
48
|
+
|
|
49
|
+
try {
|
|
50
|
+
await new Promise((resolve, reject) => {
|
|
51
|
+
ctx.mqttClient.publish('/ls_req', JSON.stringify(content), { qos: 1, retain: false }, err => {
|
|
52
|
+
if (err) reject(new Error(`MQTT publish failed: ${err.message}`));
|
|
53
|
+
else resolve();
|
|
54
|
+
});
|
|
55
|
+
});
|
|
56
|
+
return { success: true, messageID, text, method: 'mqtt', editedAt: Date.now() };
|
|
57
|
+
} catch (err) {
|
|
58
|
+
lastErr = err;
|
|
59
|
+
if (attempt >= retries) throw err;
|
|
60
|
+
const wait = baseMs * Math.pow(2, attempt - 1) + Math.random() * 150;
|
|
61
|
+
utils.warn("editMessage", `MQTT retry ${attempt}/${retries} in ${Math.round(wait)}ms`);
|
|
62
|
+
await new Promise(r => setTimeout(r, wait));
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
throw lastErr;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function editViaGraphQL(defaultFuncs, ctx, text, messageID) {
|
|
69
|
+
const form = {
|
|
70
|
+
av: ctx.userID,
|
|
71
|
+
__user: ctx.userID,
|
|
72
|
+
__a: 1,
|
|
73
|
+
fb_dtsg: ctx.fb_dtsg,
|
|
74
|
+
lsd: ctx.lsd || ctx.fb_dtsg,
|
|
75
|
+
fb_api_caller_class: "RelayModern",
|
|
76
|
+
fb_api_req_friendly_name: "useEditMessageMutation",
|
|
77
|
+
variables: JSON.stringify({
|
|
78
|
+
input: {
|
|
79
|
+
client_mutation_id: String(Date.now() % 1e9),
|
|
80
|
+
actor_id: ctx.userID,
|
|
81
|
+
message_id: messageID,
|
|
82
|
+
text,
|
|
83
|
+
}
|
|
84
|
+
}),
|
|
85
|
+
server_timestamps: true,
|
|
86
|
+
doc_id: "6363606433743285",
|
|
87
|
+
};
|
|
88
|
+
|
|
89
|
+
const res = await defaultFuncs.post("https://www.facebook.com/api/graphql/", ctx.jar, form);
|
|
90
|
+
const checked = await utils.parseAndCheckLogin(ctx, defaultFuncs)(res);
|
|
91
|
+
if (checked?.errors) throw new Error(checked.errors[0]?.message || JSON.stringify(checked.errors));
|
|
92
|
+
return { success: true, messageID, text, method: 'graphql', editedAt: Date.now() };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
96
|
+
|
|
97
|
+
/**
|
|
98
|
+
* Edit a previously sent message.
|
|
99
|
+
*
|
|
100
|
+
* @param {string} text New message text
|
|
101
|
+
* @param {string} messageID ID of the message to edit
|
|
102
|
+
* @param {object} [options] { retries, preferGraphQL }
|
|
103
|
+
* @param {Function} [callback] Node-style (err, result) callback
|
|
104
|
+
* @returns {Promise<object>} { success, messageID, text, method, editedAt }
|
|
105
|
+
*/
|
|
106
|
+
return async function editMessage(text, messageID, options, callback) {
|
|
107
|
+
if (typeof options === 'function') { callback = options; options = {}; }
|
|
108
|
+
options = options || {};
|
|
109
|
+
|
|
110
|
+
const retries = options.retries || 3;
|
|
111
|
+
const baseMs = 500;
|
|
112
|
+
|
|
113
|
+
let resolveFunc, rejectFunc;
|
|
114
|
+
const promise = new Promise((res, rej) => { resolveFunc = res; rejectFunc = rej; });
|
|
115
|
+
|
|
116
|
+
function done(err, data) {
|
|
117
|
+
if (callback) return err ? callback(err) : callback(null, data);
|
|
118
|
+
if (err) rejectFunc(err); else resolveFunc(data);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
// Validation
|
|
122
|
+
if (typeof text !== 'string' || !text.trim()) {
|
|
123
|
+
done(new Error("editMessage: text must be a non-empty string"));
|
|
124
|
+
return promise;
|
|
125
|
+
}
|
|
126
|
+
if (!messageID) {
|
|
127
|
+
done(new Error("editMessage: messageID is required"));
|
|
128
|
+
return promise;
|
|
129
|
+
}
|
|
130
|
+
if (text.length > MAX_TEXT_LENGTH) {
|
|
131
|
+
done(new Error(`editMessage: text exceeds maximum length of ${MAX_TEXT_LENGTH} characters`));
|
|
132
|
+
return promise;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
const sanitized = text.trimEnd();
|
|
136
|
+
|
|
137
|
+
try {
|
|
138
|
+
let result;
|
|
139
|
+
if (!options.preferGraphQL && ctx.mqttClient) {
|
|
140
|
+
try {
|
|
141
|
+
result = await editViaMqtt(ctx, sanitized, messageID, retries, baseMs);
|
|
142
|
+
} catch (mqttErr) {
|
|
143
|
+
utils.warn("editMessage", `MQTT failed — falling back to GraphQL: ${mqttErr.message}`);
|
|
144
|
+
result = await editViaGraphQL(defaultFuncs, ctx, sanitized, messageID);
|
|
145
|
+
}
|
|
146
|
+
} else {
|
|
147
|
+
result = await editViaGraphQL(defaultFuncs, ctx, sanitized, messageID);
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
done(null, result);
|
|
151
|
+
} catch (err) {
|
|
152
|
+
utils.error("editMessage", err.message || err);
|
|
153
|
+
done(err);
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
return promise;
|
|
157
|
+
};
|
|
158
|
+
};
|
|
@@ -0,0 +1,143 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
4
|
+
const { globalShield } = require('../datastore/models/matrix/ghost');
|
|
5
|
+
|
|
6
|
+
async function retryOp(fn, retries = 3, base = 500) {
|
|
7
|
+
for (let i = 0; i < retries; i++) {
|
|
8
|
+
try { return await fn(); } catch (err) {
|
|
9
|
+
if (i === retries - 1) throw err;
|
|
10
|
+
await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 200));
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function mqttSetEmoji(ctx, emoji, threadID, initiatorID) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
const reqID = ++ctx.wsReqNumber;
|
|
18
|
+
const taskID = ++ctx.wsTaskNumber;
|
|
19
|
+
|
|
20
|
+
const context = JSON.stringify({
|
|
21
|
+
app_id: ctx.appID || "2220391788200892",
|
|
22
|
+
payload: JSON.stringify({
|
|
23
|
+
epoch_id: parseInt(utils.generateOfflineThreadingID()),
|
|
24
|
+
tasks: [{
|
|
25
|
+
failure_count: null,
|
|
26
|
+
label: "100003",
|
|
27
|
+
payload: JSON.stringify({
|
|
28
|
+
thread_key: String(threadID),
|
|
29
|
+
custom_emoji: emoji,
|
|
30
|
+
avatar_sticker_instruction_key_id: null,
|
|
31
|
+
sync_group: 1
|
|
32
|
+
}),
|
|
33
|
+
queue_name: "thread_quick_reaction",
|
|
34
|
+
task_id: taskID
|
|
35
|
+
}],
|
|
36
|
+
version_id: "24631415369801570"
|
|
37
|
+
}),
|
|
38
|
+
request_id: reqID,
|
|
39
|
+
type: 3
|
|
40
|
+
});
|
|
41
|
+
|
|
42
|
+
ctx.mqttClient.publish("/ls_req", context, { qos: 1, retain: false }, (err) => {
|
|
43
|
+
if (err) return reject(err);
|
|
44
|
+
resolve({
|
|
45
|
+
type: "thread_emoji_update",
|
|
46
|
+
threadID,
|
|
47
|
+
newEmoji: emoji,
|
|
48
|
+
senderID: initiatorID,
|
|
49
|
+
BotID: ctx.userID,
|
|
50
|
+
timestamp: Date.now(),
|
|
51
|
+
method: "mqtt"
|
|
52
|
+
});
|
|
53
|
+
});
|
|
54
|
+
});
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async function httpSetEmoji(defaultFuncs, ctx, emoji, threadID, initiatorID) {
|
|
58
|
+
const form = {
|
|
59
|
+
emoji_choice: emoji,
|
|
60
|
+
thread_or_other_fbid: threadID
|
|
61
|
+
};
|
|
62
|
+
const res = await defaultFuncs.post(
|
|
63
|
+
"https://www.facebook.com/messaging/save_thread_emoji/?source=thread_settings&__pc=EXP1%3Amessengerdotcom_pkg",
|
|
64
|
+
ctx.jar, form
|
|
65
|
+
).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
66
|
+
if (res.error === 1357031) throw { error: "Thread has no messages yet. Send a message first." };
|
|
67
|
+
if (res.error) throw res;
|
|
68
|
+
return {
|
|
69
|
+
type: "thread_emoji_update",
|
|
70
|
+
threadID,
|
|
71
|
+
newEmoji: emoji,
|
|
72
|
+
senderID: initiatorID,
|
|
73
|
+
BotID: ctx.userID,
|
|
74
|
+
timestamp: Date.now(),
|
|
75
|
+
method: "http"
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = (defaultFuncs, api, ctx) => {
|
|
80
|
+
return function emojiSet(emoji, threadID, callback, initiatorID) {
|
|
81
|
+
let _callback, _initiatorID;
|
|
82
|
+
let resolveFunc, rejectFunc;
|
|
83
|
+
const returnPromise = new Promise((resolve, reject) => {
|
|
84
|
+
resolveFunc = resolve;
|
|
85
|
+
rejectFunc = reject;
|
|
86
|
+
});
|
|
87
|
+
|
|
88
|
+
const type = utils.getType(callback);
|
|
89
|
+
if (type === "Function" || type === "AsyncFunction") {
|
|
90
|
+
_callback = callback;
|
|
91
|
+
_initiatorID = initiatorID;
|
|
92
|
+
} else if (utils.getType(threadID) === "Function" || utils.getType(threadID) === "AsyncFunction") {
|
|
93
|
+
_callback = threadID;
|
|
94
|
+
threadID = null;
|
|
95
|
+
_initiatorID = callback;
|
|
96
|
+
} else if (type === "String") {
|
|
97
|
+
_initiatorID = callback;
|
|
98
|
+
_callback = undefined;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
if (typeof _callback !== "function") {
|
|
102
|
+
_callback = (err, data) => {
|
|
103
|
+
if (err) return rejectFunc(err);
|
|
104
|
+
resolveFunc(data);
|
|
105
|
+
};
|
|
106
|
+
} else {
|
|
107
|
+
const orig = _callback;
|
|
108
|
+
_callback = (err, data) => {
|
|
109
|
+
orig(err, data);
|
|
110
|
+
if (err) rejectFunc(err); else resolveFunc(data);
|
|
111
|
+
};
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
_initiatorID = _initiatorID || ctx.userID;
|
|
115
|
+
threadID = threadID || ctx.threadID;
|
|
116
|
+
|
|
117
|
+
if (!emoji) return _callback(new Error("emoji: emoji character is required")), returnPromise;
|
|
118
|
+
if (!threadID) return _callback(new Error("emoji: threadID is required")), returnPromise;
|
|
119
|
+
|
|
120
|
+
(async () => {
|
|
121
|
+
try {
|
|
122
|
+
await globalShield.addSmartDelay();
|
|
123
|
+
let result;
|
|
124
|
+
if (ctx.mqttClient) {
|
|
125
|
+
try {
|
|
126
|
+
result = await retryOp(() => mqttSetEmoji(ctx, emoji, threadID, _initiatorID));
|
|
127
|
+
} catch (mqttErr) {
|
|
128
|
+
utils.warn("emoji", "MQTT failed, using HTTP:", mqttErr.message);
|
|
129
|
+
result = await retryOp(() => httpSetEmoji(defaultFuncs, ctx, emoji, threadID, _initiatorID));
|
|
130
|
+
}
|
|
131
|
+
} else {
|
|
132
|
+
result = await retryOp(() => httpSetEmoji(defaultFuncs, ctx, emoji, threadID, _initiatorID));
|
|
133
|
+
}
|
|
134
|
+
_callback(null, result);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
utils.error("emoji", err);
|
|
137
|
+
_callback(err);
|
|
138
|
+
}
|
|
139
|
+
})();
|
|
140
|
+
|
|
141
|
+
return returnPromise;
|
|
142
|
+
};
|
|
143
|
+
};
|