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,207 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
4
|
+
const { globalShield } = require('../datastore/models/matrix/ghost');
|
|
5
|
+
|
|
6
|
+
const THEME_MQTT_HISTORY = [];
|
|
7
|
+
const MAX_HISTORY = 200;
|
|
8
|
+
const DEDUP = new Map();
|
|
9
|
+
const DEDUP_TTL = 2000;
|
|
10
|
+
|
|
11
|
+
async function retryOp(fn, retries = 3, base = 400) {
|
|
12
|
+
for (let i = 0; i < retries; i++) {
|
|
13
|
+
try { return await fn(); } catch (err) {
|
|
14
|
+
if (i === retries - 1) throw err;
|
|
15
|
+
const transient = /network|timeout|ECONNRESET|5\d\d|429/i.test(String(err?.message || err));
|
|
16
|
+
if (!transient) throw err;
|
|
17
|
+
await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 200));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
const TASK_DEFINITIONS = [
|
|
23
|
+
{ label: 1013, queue: (tid) => ['ai_generated_theme', String(tid)] },
|
|
24
|
+
{ label: 1037, queue: (tid) => ['msgr_custom_thread_theme', String(tid)] },
|
|
25
|
+
{ label: 1028, queue: (tid) => ['thread_theme_writer', String(tid)] },
|
|
26
|
+
{ label: 43, queue: () => 'thread_theme', extra: { source: null, payload: null } }
|
|
27
|
+
];
|
|
28
|
+
|
|
29
|
+
function buildThemeTask(ctx, threadID, themeFBID, taskDef, baseTaskNumber) {
|
|
30
|
+
const { label, queue, extra = {} } = taskDef;
|
|
31
|
+
const queueName = typeof queue === 'function' ? queue(threadID) : queue;
|
|
32
|
+
|
|
33
|
+
return {
|
|
34
|
+
failure_count: null,
|
|
35
|
+
label: String(label),
|
|
36
|
+
payload: JSON.stringify({
|
|
37
|
+
thread_key: threadID,
|
|
38
|
+
theme_fbid: themeFBID,
|
|
39
|
+
sync_group: 1,
|
|
40
|
+
...extra
|
|
41
|
+
}),
|
|
42
|
+
queue_name: typeof queueName === 'string' ? queueName : JSON.stringify(queueName),
|
|
43
|
+
task_id: baseTaskNumber
|
|
44
|
+
};
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
function buildThemeMessage(ctx, threadID, themeFBID, taskDef) {
|
|
48
|
+
ctx.wsReqNumber = (ctx.wsReqNumber || 0) + 1;
|
|
49
|
+
ctx.wsTaskNumber = (ctx.wsTaskNumber || 0) + 1;
|
|
50
|
+
const task = buildThemeTask(ctx, threadID, themeFBID, taskDef, ctx.wsTaskNumber);
|
|
51
|
+
|
|
52
|
+
return {
|
|
53
|
+
app_id: '772021112871879',
|
|
54
|
+
payload: JSON.stringify({
|
|
55
|
+
epoch_id: parseInt(utils.generateOfflineThreadingID()),
|
|
56
|
+
tasks: [task],
|
|
57
|
+
version_id: '24227364673632991'
|
|
58
|
+
}),
|
|
59
|
+
request_id: ctx.wsReqNumber,
|
|
60
|
+
type: 3
|
|
61
|
+
};
|
|
62
|
+
}
|
|
63
|
+
|
|
64
|
+
function publishMessage(ctx, msg) {
|
|
65
|
+
return new Promise((resolve, reject) => {
|
|
66
|
+
ctx.mqttClient.publish(
|
|
67
|
+
'/ls_req',
|
|
68
|
+
JSON.stringify(msg),
|
|
69
|
+
{ qos: 1, retain: false },
|
|
70
|
+
(err) => { if (err) reject(err); else resolve({ success: true, request_id: msg.request_id }); }
|
|
71
|
+
);
|
|
72
|
+
});
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
function publishMessageWithAck(ctx, msg) {
|
|
76
|
+
return new Promise((resolve, reject) => {
|
|
77
|
+
const reqID = msg.request_id;
|
|
78
|
+
let handled = false;
|
|
79
|
+
|
|
80
|
+
const onResp = (topic, message) => {
|
|
81
|
+
if (topic !== '/ls_resp' || handled) return;
|
|
82
|
+
let j;
|
|
83
|
+
try { j = JSON.parse(message.toString()); j.payload = JSON.parse(j.payload); } catch { return; }
|
|
84
|
+
if (j.request_id !== reqID) return;
|
|
85
|
+
handled = true;
|
|
86
|
+
clearTimeout(timer);
|
|
87
|
+
ctx.mqttClient.removeListener('message', onResp);
|
|
88
|
+
resolve({ success: true, request_id: reqID, acked: true });
|
|
89
|
+
};
|
|
90
|
+
|
|
91
|
+
const timer = setTimeout(() => {
|
|
92
|
+
if (!handled) {
|
|
93
|
+
handled = true;
|
|
94
|
+
ctx.mqttClient.removeListener('message', onResp);
|
|
95
|
+
resolve({ success: true, request_id: reqID, acked: false });
|
|
96
|
+
}
|
|
97
|
+
}, 8000);
|
|
98
|
+
|
|
99
|
+
ctx.mqttClient.on('message', onResp);
|
|
100
|
+
ctx.mqttClient.publish('/ls_req', JSON.stringify(msg), { qos: 1, retain: false }, (err) => {
|
|
101
|
+
if (err && !handled) {
|
|
102
|
+
handled = true;
|
|
103
|
+
clearTimeout(timer);
|
|
104
|
+
ctx.mqttClient.removeListener('message', onResp);
|
|
105
|
+
reject(err);
|
|
106
|
+
}
|
|
107
|
+
});
|
|
108
|
+
});
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
function recordHistory(op) {
|
|
112
|
+
THEME_MQTT_HISTORY.unshift({ ...op, ts: Date.now() });
|
|
113
|
+
if (THEME_MQTT_HISTORY.length > MAX_HISTORY) THEME_MQTT_HISTORY.length = MAX_HISTORY;
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
117
|
+
|
|
118
|
+
const setThreadThemeMqtt = async function setThreadThemeMqtt(threadID, themeFBID, options, callback) {
|
|
119
|
+
if (typeof options === 'function') { callback = options; options = {}; }
|
|
120
|
+
if (!options || typeof options !== 'object') options = {};
|
|
121
|
+
const { useAck = false, dedup = true, addDelay = true } = options;
|
|
122
|
+
|
|
123
|
+
let resolveFunc, rejectFunc;
|
|
124
|
+
const returnPromise = new Promise((resolve, reject) => {
|
|
125
|
+
resolveFunc = resolve;
|
|
126
|
+
rejectFunc = reject;
|
|
127
|
+
});
|
|
128
|
+
|
|
129
|
+
const done = (err, data) => {
|
|
130
|
+
if (typeof callback === 'function') callback(err, data);
|
|
131
|
+
if (err) rejectFunc(err);
|
|
132
|
+
else resolveFunc(data);
|
|
133
|
+
};
|
|
134
|
+
|
|
135
|
+
try {
|
|
136
|
+
if (!ctx.mqttClient) throw new Error('setThreadThemeMqtt: MQTT not connected');
|
|
137
|
+
if (!threadID) throw new Error('setThreadThemeMqtt: threadID is required');
|
|
138
|
+
if (!themeFBID) throw new Error('setThreadThemeMqtt: themeFBID is required');
|
|
139
|
+
|
|
140
|
+
const dedupKey = `theme_${threadID}_${themeFBID}`;
|
|
141
|
+
if (dedup) {
|
|
142
|
+
const last = DEDUP.get(dedupKey);
|
|
143
|
+
if (last && Date.now() - last < DEDUP_TTL) {
|
|
144
|
+
return done(null, { success: true, skipped: true, reason: 'dedup', threadID: String(threadID), themeFBID: String(themeFBID) });
|
|
145
|
+
}
|
|
146
|
+
DEDUP.set(dedupKey, Date.now());
|
|
147
|
+
}
|
|
148
|
+
|
|
149
|
+
if (addDelay) await globalShield.addSmartDelay();
|
|
150
|
+
|
|
151
|
+
const publish = useAck ? publishMessageWithAck : publishMessage;
|
|
152
|
+
const results = [];
|
|
153
|
+
|
|
154
|
+
for (const taskDef of TASK_DEFINITIONS) {
|
|
155
|
+
const msg = buildThemeMessage(ctx, threadID, themeFBID, taskDef);
|
|
156
|
+
try {
|
|
157
|
+
const r = await retryOp(() => publish(ctx, msg));
|
|
158
|
+
results.push({ ...r, label: taskDef.label });
|
|
159
|
+
} catch (err) {
|
|
160
|
+
utils.warn('setThreadThemeMqtt', `Task label ${taskDef.label} failed:`, err.message);
|
|
161
|
+
results.push({ success: false, label: taskDef.label, error: err.message });
|
|
162
|
+
}
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const successCount = results.filter(r => r.success).length;
|
|
166
|
+
const result = {
|
|
167
|
+
success: successCount > 0,
|
|
168
|
+
threadID: String(threadID),
|
|
169
|
+
themeFBID: String(themeFBID),
|
|
170
|
+
tasksPublished: successCount,
|
|
171
|
+
tasksFailed: results.length - successCount,
|
|
172
|
+
results,
|
|
173
|
+
timestamp: Date.now()
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
recordHistory({ threadID: String(threadID), themeFBID: String(themeFBID), successCount });
|
|
177
|
+
utils.log('setThreadThemeMqtt', `Set theme ${themeFBID} on thread ${threadID} (${successCount}/${results.length} tasks succeeded)`);
|
|
178
|
+
|
|
179
|
+
done(null, result);
|
|
180
|
+
} catch (err) {
|
|
181
|
+
utils.error('setThreadThemeMqtt', err);
|
|
182
|
+
done(err);
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
return returnPromise;
|
|
186
|
+
};
|
|
187
|
+
|
|
188
|
+
setThreadThemeMqtt.batch = async function batchSetTheme(items, options = {}) {
|
|
189
|
+
const results = [];
|
|
190
|
+
const errors = [];
|
|
191
|
+
for (const { threadID, themeFBID } of items) {
|
|
192
|
+
try {
|
|
193
|
+
const r = await setThreadThemeMqtt(threadID, themeFBID, options);
|
|
194
|
+
results.push(r);
|
|
195
|
+
} catch (err) {
|
|
196
|
+
errors.push({ threadID, themeFBID, error: err.message });
|
|
197
|
+
}
|
|
198
|
+
await new Promise(r => setTimeout(r, 200 + Math.random() * 100));
|
|
199
|
+
}
|
|
200
|
+
return { success: errors.length === 0, results, errors, timestamp: Date.now() };
|
|
201
|
+
};
|
|
202
|
+
|
|
203
|
+
setThreadThemeMqtt.getHistory = (limit = 30) => THEME_MQTT_HISTORY.slice(0, limit);
|
|
204
|
+
setThreadThemeMqtt.clearDedup = () => { DEDUP.clear(); return { success: true }; };
|
|
205
|
+
|
|
206
|
+
return setThreadThemeMqtt;
|
|
207
|
+
};
|
|
@@ -0,0 +1,200 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
4
|
+
const { globalShield } = require('../datastore/models/matrix/ghost');
|
|
5
|
+
|
|
6
|
+
const PREVIEW_CACHE = new Map();
|
|
7
|
+
const PREVIEW_CACHE_TTL = 15 * 60 * 1000;
|
|
8
|
+
const SHARE_HISTORY = [];
|
|
9
|
+
const MAX_HISTORY = 200;
|
|
10
|
+
const INFLIGHT_PREVIEWS = new Map();
|
|
11
|
+
|
|
12
|
+
const FALLBACK_DOC_IDS = [
|
|
13
|
+
'28939050904374351',
|
|
14
|
+
'9553764061351979',
|
|
15
|
+
'6419764884719809'
|
|
16
|
+
];
|
|
17
|
+
|
|
18
|
+
async function retryOp(fn, retries = 4, base = 600) {
|
|
19
|
+
for (let i = 0; i < retries; i++) {
|
|
20
|
+
try { return await fn(); } catch (err) {
|
|
21
|
+
if (i === retries - 1) throw err;
|
|
22
|
+
const transient = /network|timeout|ECONNRESET|ETIMEDOUT|5\d\d|429/i.test(String(err?.message || err));
|
|
23
|
+
if (!transient) throw err;
|
|
24
|
+
await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 300));
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
function formatPreviewResult(data) {
|
|
30
|
+
if (data.errors) throw data.errors[0];
|
|
31
|
+
const previewData = data.data?.xma_preview_data;
|
|
32
|
+
if (!previewData) throw { error: 'Could not generate a preview for this post.' };
|
|
33
|
+
|
|
34
|
+
return {
|
|
35
|
+
postID: previewData.post_id,
|
|
36
|
+
header: previewData.header_title,
|
|
37
|
+
subtitle: previewData.subtitle_text,
|
|
38
|
+
title: previewData.title_text,
|
|
39
|
+
previewImage: previewData.preview_url,
|
|
40
|
+
favicon: previewData.favicon_url,
|
|
41
|
+
headerImage: previewData.header_image_url,
|
|
42
|
+
timestamp: Date.now()
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
function isPersistedQueryError(resData) {
|
|
47
|
+
if (!resData?.errors) return false;
|
|
48
|
+
return resData.errors.some(e => {
|
|
49
|
+
const msg = (e.message || '').toLowerCase();
|
|
50
|
+
return msg.includes('persistedquerynotfound') ||
|
|
51
|
+
(msg.includes('document') && msg.includes('not found')) ||
|
|
52
|
+
msg.includes('persisted query');
|
|
53
|
+
});
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
async function tryFetchPreview(defaultFuncs, ctx, postID, docId) {
|
|
57
|
+
const form = {
|
|
58
|
+
fb_api_caller_class: 'RelayModern',
|
|
59
|
+
fb_api_req_friendly_name: 'CometXMAProxyShareablePreviewQuery',
|
|
60
|
+
variables: JSON.stringify({ shareable_id: postID.toString(), scale: 3 }),
|
|
61
|
+
doc_id: docId
|
|
62
|
+
};
|
|
63
|
+
|
|
64
|
+
const resData = await defaultFuncs.post('https://www.facebook.com/api/graphql/', ctx.jar, form)
|
|
65
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
66
|
+
|
|
67
|
+
if (isPersistedQueryError(resData)) {
|
|
68
|
+
throw Object.assign(new Error(`doc_id "${docId}" expired or not found`), { isExpiredDocId: true });
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
return formatPreviewResult(resData);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
function recordHistory(op) {
|
|
75
|
+
SHARE_HISTORY.unshift({ ...op, ts: Date.now() });
|
|
76
|
+
if (SHARE_HISTORY.length > MAX_HISTORY) SHARE_HISTORY.length = MAX_HISTORY;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
80
|
+
|
|
81
|
+
const getPostPreview = async function getPostPreview(postID, options, callback) {
|
|
82
|
+
if (typeof options === 'function') { callback = options; options = {}; }
|
|
83
|
+
if (!options || typeof options !== 'object') options = {};
|
|
84
|
+
|
|
85
|
+
const {
|
|
86
|
+
skipCache = false,
|
|
87
|
+
tryFallbackDocIds = true,
|
|
88
|
+
timeout = 15000
|
|
89
|
+
} = options;
|
|
90
|
+
|
|
91
|
+
let resolveFunc, rejectFunc;
|
|
92
|
+
const returnPromise = new Promise((resolve, reject) => {
|
|
93
|
+
resolveFunc = resolve;
|
|
94
|
+
rejectFunc = reject;
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
const cb = (err, data) => {
|
|
98
|
+
if (callback) callback(err, data);
|
|
99
|
+
if (err) return rejectFunc(err);
|
|
100
|
+
resolveFunc(data);
|
|
101
|
+
};
|
|
102
|
+
|
|
103
|
+
if (!postID) {
|
|
104
|
+
cb({ error: 'share: A postID is required to generate a preview.' });
|
|
105
|
+
return returnPromise;
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const pid = postID.toString();
|
|
109
|
+
const cacheKey = `preview_${pid}`;
|
|
110
|
+
|
|
111
|
+
if (!skipCache) {
|
|
112
|
+
const cached = PREVIEW_CACHE.get(cacheKey);
|
|
113
|
+
if (cached && Date.now() - cached.ts < PREVIEW_CACHE_TTL) {
|
|
114
|
+
cb(null, { ...cached.result, fromCache: true });
|
|
115
|
+
return returnPromise;
|
|
116
|
+
}
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
if (INFLIGHT_PREVIEWS.has(pid)) {
|
|
120
|
+
try {
|
|
121
|
+
const r = await INFLIGHT_PREVIEWS.get(pid);
|
|
122
|
+
cb(null, r);
|
|
123
|
+
} catch (err) { cb(err); }
|
|
124
|
+
return returnPromise;
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
const promise = (async () => {
|
|
128
|
+
const docIds = [
|
|
129
|
+
ctx.options?.sharePreviewDocId,
|
|
130
|
+
...FALLBACK_DOC_IDS
|
|
131
|
+
].filter(Boolean);
|
|
132
|
+
|
|
133
|
+
let lastErr;
|
|
134
|
+
|
|
135
|
+
for (const docId of docIds) {
|
|
136
|
+
try {
|
|
137
|
+
await globalShield.addSmartDelay();
|
|
138
|
+
const result = await retryOp(() => tryFetchPreview(defaultFuncs, ctx, pid, docId));
|
|
139
|
+
PREVIEW_CACHE.set(cacheKey, { result, ts: Date.now() });
|
|
140
|
+
recordHistory({ postID: pid, docId, success: true });
|
|
141
|
+
utils.log('share', `Preview fetched for post ${pid} using doc_id ${docId}`);
|
|
142
|
+
return { ...result, docIdUsed: docId };
|
|
143
|
+
} catch (err) {
|
|
144
|
+
lastErr = err;
|
|
145
|
+
if (err.isExpiredDocId) {
|
|
146
|
+
utils.warn('share', `doc_id "${docId}" expired, trying next`);
|
|
147
|
+
if (!tryFallbackDocIds) break;
|
|
148
|
+
} else {
|
|
149
|
+
utils.warn('share', `Preview fetch failed with doc_id "${docId}":`, err.message || err);
|
|
150
|
+
if (!tryFallbackDocIds) break;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
throw Object.assign(lastErr || new Error('All doc_ids exhausted'), {
|
|
156
|
+
hint: 'Update ctx.options.sharePreviewDocId with current value from Messenger traffic (CometXMAProxyShareablePreviewQuery)',
|
|
157
|
+
triedDocIds: docIds
|
|
158
|
+
});
|
|
159
|
+
})();
|
|
160
|
+
|
|
161
|
+
INFLIGHT_PREVIEWS.set(pid, promise);
|
|
162
|
+
|
|
163
|
+
try {
|
|
164
|
+
const r = await promise;
|
|
165
|
+
cb(null, r);
|
|
166
|
+
} catch (err) {
|
|
167
|
+
utils.error('share', err);
|
|
168
|
+
cb(err);
|
|
169
|
+
} finally {
|
|
170
|
+
INFLIGHT_PREVIEWS.delete(pid);
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
return returnPromise;
|
|
174
|
+
};
|
|
175
|
+
|
|
176
|
+
getPostPreview.batch = async function batchPreviews(postIDs, options = {}) {
|
|
177
|
+
const results = [];
|
|
178
|
+
const errors = [];
|
|
179
|
+
for (const postID of postIDs) {
|
|
180
|
+
try {
|
|
181
|
+
const r = await getPostPreview(postID, options);
|
|
182
|
+
results.push(r);
|
|
183
|
+
} catch (err) {
|
|
184
|
+
errors.push({ postID, error: err.message || String(err) });
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
return { success: errors.length === 0, results, errors, timestamp: Date.now() };
|
|
188
|
+
};
|
|
189
|
+
|
|
190
|
+
getPostPreview.clearCache = (postID) => {
|
|
191
|
+
if (postID) PREVIEW_CACHE.delete(`preview_${postID}`);
|
|
192
|
+
else PREVIEW_CACHE.clear();
|
|
193
|
+
return { success: true };
|
|
194
|
+
};
|
|
195
|
+
|
|
196
|
+
getPostPreview.getHistory = (limit = 30) => SHARE_HISTORY.slice(0, limit);
|
|
197
|
+
getPostPreview.updateDocId = (docId) => { if (!ctx.options) ctx.options = {}; ctx.options.sharePreviewDocId = docId; };
|
|
198
|
+
|
|
199
|
+
return getPostPreview;
|
|
200
|
+
};
|
|
@@ -0,0 +1,216 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
4
|
+
const { globalShield } = require('../datastore/models/matrix/ghost');
|
|
5
|
+
|
|
6
|
+
const SHARE_HISTORY = [];
|
|
7
|
+
const MAX_HISTORY = 200;
|
|
8
|
+
const DEDUP_MAP = new Map();
|
|
9
|
+
const DEDUP_TTL = 3000;
|
|
10
|
+
|
|
11
|
+
async function retryOp(fn, retries = 3, base = 400) {
|
|
12
|
+
for (let i = 0; i < retries; i++) {
|
|
13
|
+
try { return await fn(); } catch (err) {
|
|
14
|
+
if (i === retries - 1) throw err;
|
|
15
|
+
const transient = /network|timeout|ECONNRESET|5\d\d|429/i.test(String(err?.message || err));
|
|
16
|
+
if (!transient) throw err;
|
|
17
|
+
await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 200));
|
|
18
|
+
}
|
|
19
|
+
}
|
|
20
|
+
}
|
|
21
|
+
|
|
22
|
+
function mqttShareContact(ctx, text, senderID, threadID) {
|
|
23
|
+
return new Promise((resolve, reject) => {
|
|
24
|
+
if (!ctx.mqttClient) return reject(new Error('MQTT not connected'));
|
|
25
|
+
|
|
26
|
+
ctx.wsReqNumber = (ctx.wsReqNumber || 0) + 1;
|
|
27
|
+
ctx.wsTaskNumber = (ctx.wsTaskNumber || 0) + 1;
|
|
28
|
+
|
|
29
|
+
const reqID = ctx.wsReqNumber;
|
|
30
|
+
const taskID = ctx.wsTaskNumber;
|
|
31
|
+
|
|
32
|
+
const queryPayload = {
|
|
33
|
+
contact_id: String(senderID),
|
|
34
|
+
sync_group: 1,
|
|
35
|
+
text: text || '',
|
|
36
|
+
thread_id: String(threadID)
|
|
37
|
+
};
|
|
38
|
+
|
|
39
|
+
const context = {
|
|
40
|
+
app_id: '2220391788200892',
|
|
41
|
+
payload: JSON.stringify({
|
|
42
|
+
tasks: [{
|
|
43
|
+
failure_count: null,
|
|
44
|
+
label: '359',
|
|
45
|
+
payload: JSON.stringify(queryPayload),
|
|
46
|
+
queue_name: 'messenger_contact_sharing',
|
|
47
|
+
task_id: taskID
|
|
48
|
+
}],
|
|
49
|
+
epoch_id: utils.generateOfflineThreadingID(),
|
|
50
|
+
version_id: '7214102258676893'
|
|
51
|
+
}),
|
|
52
|
+
request_id: reqID,
|
|
53
|
+
type: 3
|
|
54
|
+
};
|
|
55
|
+
|
|
56
|
+
let handled = false;
|
|
57
|
+
const onResp = (topic, message) => {
|
|
58
|
+
if (topic !== '/ls_resp' || handled) return;
|
|
59
|
+
let j;
|
|
60
|
+
try { j = JSON.parse(message.toString()); j.payload = JSON.parse(j.payload); } catch { return; }
|
|
61
|
+
if (j.request_id !== reqID) return;
|
|
62
|
+
handled = true;
|
|
63
|
+
clearTimeout(timer);
|
|
64
|
+
ctx.mqttClient.removeListener('message', onResp);
|
|
65
|
+
resolve({ success: true, method: 'mqtt_ack', senderID: String(senderID), threadID: String(threadID), requestID: reqID });
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const timer = setTimeout(() => {
|
|
69
|
+
if (!handled) {
|
|
70
|
+
handled = true;
|
|
71
|
+
ctx.mqttClient.removeListener('message', onResp);
|
|
72
|
+
resolve({ success: true, method: 'mqtt_noack', senderID: String(senderID), threadID: String(threadID), requestID: reqID });
|
|
73
|
+
}
|
|
74
|
+
}, 12000);
|
|
75
|
+
|
|
76
|
+
ctx.mqttClient.on('message', onResp);
|
|
77
|
+
ctx.mqttClient.publish('/ls_req', JSON.stringify(context), { qos: 1, retain: false }, (err) => {
|
|
78
|
+
if (err && !handled) {
|
|
79
|
+
handled = true;
|
|
80
|
+
clearTimeout(timer);
|
|
81
|
+
ctx.mqttClient.removeListener('message', onResp);
|
|
82
|
+
reject(err);
|
|
83
|
+
}
|
|
84
|
+
});
|
|
85
|
+
});
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
async function graphqlShareContact(defaultFuncs, ctx, text, senderID, threadID) {
|
|
89
|
+
const form = {
|
|
90
|
+
fb_api_caller_class: 'RelayModern',
|
|
91
|
+
fb_api_req_friendly_name: 'MessengerShareContactMutation',
|
|
92
|
+
variables: JSON.stringify({
|
|
93
|
+
input: {
|
|
94
|
+
contact_id: String(senderID),
|
|
95
|
+
thread_fbid: String(threadID),
|
|
96
|
+
text: text || '',
|
|
97
|
+
actor_id: ctx.userID,
|
|
98
|
+
client_mutation_id: String(Math.round(Math.random() * 10000))
|
|
99
|
+
}
|
|
100
|
+
}),
|
|
101
|
+
doc_id: '6219437738097867'
|
|
102
|
+
};
|
|
103
|
+
|
|
104
|
+
const res = await defaultFuncs.post('https://www.facebook.com/api/graphql/', ctx.jar, form)
|
|
105
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
106
|
+
if (res?.errors) throw new Error(JSON.stringify(res.errors));
|
|
107
|
+
return { success: true, method: 'graphql', senderID: String(senderID), threadID: String(threadID) };
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
function recordHistory(op) {
|
|
111
|
+
SHARE_HISTORY.unshift({ ...op, ts: Date.now() });
|
|
112
|
+
if (SHARE_HISTORY.length > MAX_HISTORY) SHARE_HISTORY.length = MAX_HISTORY;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
function isDuplicate(senderID, threadID) {
|
|
116
|
+
const key = `${senderID}|${threadID}`;
|
|
117
|
+
const last = DEDUP_MAP.get(key);
|
|
118
|
+
if (last && Date.now() - last < DEDUP_TTL) return true;
|
|
119
|
+
DEDUP_MAP.set(key, Date.now());
|
|
120
|
+
return false;
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
124
|
+
|
|
125
|
+
const shareContact = async function shareContact(text, senderID, threadOrThreads, options, callback) {
|
|
126
|
+
if (typeof options === 'function') { callback = options; options = {}; }
|
|
127
|
+
if (!options || typeof options !== 'object') options = {};
|
|
128
|
+
const {
|
|
129
|
+
preferMqtt = true,
|
|
130
|
+
useGraphQL = false,
|
|
131
|
+
dedup = true,
|
|
132
|
+
addDelay = true
|
|
133
|
+
} = options;
|
|
134
|
+
|
|
135
|
+
let resolveFunc, rejectFunc;
|
|
136
|
+
const returnPromise = new Promise((resolve, reject) => {
|
|
137
|
+
resolveFunc = resolve;
|
|
138
|
+
rejectFunc = reject;
|
|
139
|
+
});
|
|
140
|
+
|
|
141
|
+
if (typeof callback !== 'function') {
|
|
142
|
+
callback = (err, data) => {
|
|
143
|
+
if (err) return rejectFunc(err);
|
|
144
|
+
resolveFunc(data);
|
|
145
|
+
};
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
try {
|
|
149
|
+
if (!senderID) throw new Error('shareContact: senderID is required');
|
|
150
|
+
|
|
151
|
+
const threadIDs = Array.isArray(threadOrThreads)
|
|
152
|
+
? threadOrThreads.map(String)
|
|
153
|
+
: [String(threadOrThreads)];
|
|
154
|
+
|
|
155
|
+
if (threadIDs.length === 0) throw new Error('shareContact: at least one threadID is required');
|
|
156
|
+
|
|
157
|
+
if (addDelay) await globalShield.addSmartDelay();
|
|
158
|
+
|
|
159
|
+
const results = [];
|
|
160
|
+
const errors = [];
|
|
161
|
+
|
|
162
|
+
for (const tid of threadIDs) {
|
|
163
|
+
if (dedup && isDuplicate(String(senderID), tid)) {
|
|
164
|
+
results.push({ skipped: true, reason: 'dedup', senderID: String(senderID), threadID: tid });
|
|
165
|
+
continue;
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
try {
|
|
169
|
+
let result;
|
|
170
|
+
|
|
171
|
+
if (useGraphQL) {
|
|
172
|
+
result = await retryOp(() => graphqlShareContact(defaultFuncs, ctx, text, senderID, tid));
|
|
173
|
+
} else if (preferMqtt && ctx.mqttClient) {
|
|
174
|
+
try {
|
|
175
|
+
result = await retryOp(() => mqttShareContact(ctx, text, senderID, tid));
|
|
176
|
+
} catch (mqttErr) {
|
|
177
|
+
utils.warn('shareContact', `MQTT failed for thread ${tid}, trying GraphQL:`, mqttErr.message);
|
|
178
|
+
result = await retryOp(() => graphqlShareContact(defaultFuncs, ctx, text, senderID, tid));
|
|
179
|
+
}
|
|
180
|
+
} else {
|
|
181
|
+
result = await retryOp(() => graphqlShareContact(defaultFuncs, ctx, text, senderID, tid));
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
results.push(result);
|
|
185
|
+
recordHistory({ senderID: String(senderID), threadID: tid, text, method: result.method });
|
|
186
|
+
|
|
187
|
+
if (threadIDs.length > 1) await new Promise(r => setTimeout(r, 200 + Math.random() * 150));
|
|
188
|
+
} catch (err) {
|
|
189
|
+
errors.push({ threadID: tid, error: err.message || String(err) });
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
const isSingle = threadIDs.length === 1;
|
|
194
|
+
const finalResult = isSingle && results.length === 1
|
|
195
|
+
? results[0]
|
|
196
|
+
: { success: errors.length === 0, results, errors: errors.length ? errors : undefined, timestamp: Date.now() };
|
|
197
|
+
|
|
198
|
+
if (errors.length === threadIDs.length) {
|
|
199
|
+
return callback(new Error(`shareContact: all ${threadIDs.length} sends failed — ${errors[0]?.error}`));
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
utils.log('shareContact', `Shared contact ${senderID} to ${results.length} thread(s)`);
|
|
203
|
+
callback(null, finalResult);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
utils.error('shareContact', err);
|
|
206
|
+
callback(err);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
return returnPromise;
|
|
210
|
+
};
|
|
211
|
+
|
|
212
|
+
shareContact.getHistory = (limit = 50) => SHARE_HISTORY.slice(0, limit);
|
|
213
|
+
shareContact.clearDedup = () => { DEDUP_MAP.clear(); return { success: true }; };
|
|
214
|
+
|
|
215
|
+
return shareContact;
|
|
216
|
+
};
|