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,142 @@
|
|
|
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 = 600) {
|
|
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() * 300));
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
async function mqttChangeAdmin(ctx, threadID, adminIDs, adminStatus) {
|
|
16
|
+
return new Promise((resolve, reject) => {
|
|
17
|
+
const isAdmin = adminStatus ? 1 : 0;
|
|
18
|
+
const epochID = utils.generateOfflineThreadingID();
|
|
19
|
+
const reqID = ++ctx.wsReqNumber;
|
|
20
|
+
|
|
21
|
+
const tasks = adminIDs.map((id, idx) => ({
|
|
22
|
+
failure_count: null,
|
|
23
|
+
label: "25",
|
|
24
|
+
payload: JSON.stringify({ thread_key: threadID, contact_id: id, is_admin: isAdmin }),
|
|
25
|
+
queue_name: "admin_status",
|
|
26
|
+
task_id: ++ctx.wsTaskNumber
|
|
27
|
+
}));
|
|
28
|
+
|
|
29
|
+
const form = JSON.stringify({
|
|
30
|
+
app_id: "2220391788200892",
|
|
31
|
+
payload: JSON.stringify({ epoch_id: epochID, tasks, version_id: "8798795233522156" }),
|
|
32
|
+
request_id: reqID,
|
|
33
|
+
type: 3
|
|
34
|
+
});
|
|
35
|
+
|
|
36
|
+
let handled = false;
|
|
37
|
+
const onResp = (topic, message) => {
|
|
38
|
+
if (topic !== "/ls_resp" || handled) return;
|
|
39
|
+
let j;
|
|
40
|
+
try { j = JSON.parse(message.toString()); j.payload = JSON.parse(j.payload); } catch { return; }
|
|
41
|
+
if (j.request_id !== reqID) return;
|
|
42
|
+
handled = true;
|
|
43
|
+
clearTimeout(timer);
|
|
44
|
+
ctx.mqttClient.removeListener("message", onResp);
|
|
45
|
+
resolve({ success: true, adminIDs, adminStatus, method: "mqtt" });
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
const timer = setTimeout(() => {
|
|
49
|
+
if (!handled) {
|
|
50
|
+
handled = true;
|
|
51
|
+
ctx.mqttClient.removeListener("message", onResp);
|
|
52
|
+
reject(new Error("MQTT timeout for changeAdminStatus"));
|
|
53
|
+
}
|
|
54
|
+
}, 25000);
|
|
55
|
+
|
|
56
|
+
ctx.mqttClient.on("message", onResp);
|
|
57
|
+
ctx.mqttClient.publish("/ls_req", form, { qos: 1, retain: false }, (err) => {
|
|
58
|
+
if (err && !handled) {
|
|
59
|
+
handled = true;
|
|
60
|
+
clearTimeout(timer);
|
|
61
|
+
ctx.mqttClient.removeListener("message", onResp);
|
|
62
|
+
reject(err);
|
|
63
|
+
}
|
|
64
|
+
});
|
|
65
|
+
});
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
async function httpChangeAdmin(defaultFuncs, ctx, threadID, adminIDs, adminStatus) {
|
|
69
|
+
const tasks = adminIDs.map((id, idx) => ({
|
|
70
|
+
label: "25",
|
|
71
|
+
payload: JSON.stringify({ thread_key: threadID, contact_id: id, is_admin: adminStatus ? 1 : 0 }),
|
|
72
|
+
queue_name: "admin_status",
|
|
73
|
+
task_id: idx + 1,
|
|
74
|
+
failure_count: null
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
const form = {
|
|
78
|
+
fb_dtsg: ctx.fb_dtsg,
|
|
79
|
+
request_id: 1,
|
|
80
|
+
type: 3,
|
|
81
|
+
payload: JSON.stringify({
|
|
82
|
+
version_id: "3816854585040595",
|
|
83
|
+
tasks,
|
|
84
|
+
epoch_id: utils.generateOfflineThreadingID(),
|
|
85
|
+
data_trace_id: null
|
|
86
|
+
}),
|
|
87
|
+
app_id: "772021112871879"
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
await defaultFuncs.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
|
|
91
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
92
|
+
|
|
93
|
+
return { success: true, adminIDs, adminStatus, method: "http" };
|
|
94
|
+
}
|
|
95
|
+
|
|
96
|
+
module.exports = (defaultFuncs, api, ctx) => {
|
|
97
|
+
return async function changeAdminStatus(threadID, adminID, adminStatus, callback) {
|
|
98
|
+
let resolveFunc, rejectFunc;
|
|
99
|
+
const returnPromise = new Promise((resolve, reject) => {
|
|
100
|
+
resolveFunc = resolve;
|
|
101
|
+
rejectFunc = reject;
|
|
102
|
+
});
|
|
103
|
+
|
|
104
|
+
if (typeof callback !== "function") {
|
|
105
|
+
callback = (err, data) => {
|
|
106
|
+
if (err) return rejectFunc(err);
|
|
107
|
+
resolveFunc(data);
|
|
108
|
+
};
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
try {
|
|
112
|
+
if (typeof threadID !== "string" && typeof threadID !== "number")
|
|
113
|
+
throw new Error("changeAdminStatus: threadID must be a string or number");
|
|
114
|
+
if (typeof adminStatus !== "boolean")
|
|
115
|
+
throw new Error("changeAdminStatus: adminStatus must be true or false");
|
|
116
|
+
|
|
117
|
+
const adminIDs = Array.isArray(adminID) ? adminID.map(String) : [String(adminID)];
|
|
118
|
+
if (adminIDs.length === 0) throw new Error("changeAdminStatus: no adminID provided");
|
|
119
|
+
|
|
120
|
+
await globalShield.addSmartDelay();
|
|
121
|
+
|
|
122
|
+
let result;
|
|
123
|
+
if (ctx.mqttClient) {
|
|
124
|
+
try {
|
|
125
|
+
result = await retryOp(() => mqttChangeAdmin(ctx, threadID, adminIDs, adminStatus));
|
|
126
|
+
} catch (mqttErr) {
|
|
127
|
+
utils.warn("changeAdminStatus", "MQTT failed, using HTTP:", mqttErr.message);
|
|
128
|
+
result = await retryOp(() => httpChangeAdmin(defaultFuncs, ctx, threadID, adminIDs, adminStatus));
|
|
129
|
+
}
|
|
130
|
+
} else {
|
|
131
|
+
result = await retryOp(() => httpChangeAdmin(defaultFuncs, ctx, threadID, adminIDs, adminStatus));
|
|
132
|
+
}
|
|
133
|
+
|
|
134
|
+
callback(null, result);
|
|
135
|
+
} catch (err) {
|
|
136
|
+
utils.error("changeAdminStatus", err);
|
|
137
|
+
callback(err);
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
return returnPromise;
|
|
141
|
+
};
|
|
142
|
+
};
|
|
@@ -0,0 +1,135 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
4
|
+
const { globalShield } = require('../datastore/models/matrix/ghost');
|
|
5
|
+
|
|
6
|
+
const CACHE = new Map();
|
|
7
|
+
const CACHE_TTL = 2 * 60 * 1000;
|
|
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 httpArchive(defaultFuncs, ctx, threadIDs, archive) {
|
|
19
|
+
const form = { should_archive: archive };
|
|
20
|
+
threadIDs.forEach(id => { form[`thread_fbids[${id}]`] = true; });
|
|
21
|
+
|
|
22
|
+
const res = await defaultFuncs.post(
|
|
23
|
+
"https://www.facebook.com/ajax/mercury/change_archived_status.php",
|
|
24
|
+
ctx.jar,
|
|
25
|
+
form
|
|
26
|
+
).then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
27
|
+
|
|
28
|
+
if (res && res.error) throw res;
|
|
29
|
+
return { success: true, archivedThreads: threadIDs, archived: archive };
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
async function mqttArchive(ctx, threadIDs, archive) {
|
|
33
|
+
return new Promise((resolve, reject) => {
|
|
34
|
+
if (!ctx.mqttClient) return reject(new Error("MQTT not connected"));
|
|
35
|
+
|
|
36
|
+
const reqID = ++ctx.wsReqNumber;
|
|
37
|
+
const tasks = threadIDs.map((id) => ({
|
|
38
|
+
failure_count: null,
|
|
39
|
+
label: archive ? "57" : "58",
|
|
40
|
+
payload: JSON.stringify({ thread_key: id, sync_group: 1 }),
|
|
41
|
+
queue_name: "archive_thread",
|
|
42
|
+
task_id: ++ctx.wsTaskNumber
|
|
43
|
+
}));
|
|
44
|
+
|
|
45
|
+
const form = JSON.stringify({
|
|
46
|
+
app_id: "2220391788200892",
|
|
47
|
+
payload: JSON.stringify({
|
|
48
|
+
epoch_id: utils.generateOfflineThreadingID(),
|
|
49
|
+
tasks,
|
|
50
|
+
version_id: "8798795233522156"
|
|
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, archivedThreads: threadIDs, archived: archive, method: "mqtt" });
|
|
66
|
+
};
|
|
67
|
+
|
|
68
|
+
const timer = setTimeout(() => {
|
|
69
|
+
if (!handled) {
|
|
70
|
+
handled = true;
|
|
71
|
+
ctx.mqttClient.removeListener("message", onResp);
|
|
72
|
+
reject(new Error("MQTT timeout for changeArchivedStatus"));
|
|
73
|
+
}
|
|
74
|
+
}, 20000);
|
|
75
|
+
|
|
76
|
+
ctx.mqttClient.on("message", onResp);
|
|
77
|
+
ctx.mqttClient.publish("/ls_req", form, { 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
|
+
module.exports = (defaultFuncs, api, ctx) => {
|
|
89
|
+
return async function changeArchivedStatus(threadIDs, archive, callback) {
|
|
90
|
+
let resolveFunc, rejectFunc;
|
|
91
|
+
const returnPromise = new Promise((resolve, reject) => {
|
|
92
|
+
resolveFunc = resolve;
|
|
93
|
+
rejectFunc = reject;
|
|
94
|
+
});
|
|
95
|
+
|
|
96
|
+
if (typeof archive === "function") { callback = archive; archive = true; }
|
|
97
|
+
if (typeof callback !== "function") {
|
|
98
|
+
callback = (err, result) => {
|
|
99
|
+
if (err) return rejectFunc(err);
|
|
100
|
+
resolveFunc(result);
|
|
101
|
+
};
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
try {
|
|
105
|
+
if (typeof archive !== "boolean") throw new Error("changeArchivedStatus: archive must be a boolean");
|
|
106
|
+
|
|
107
|
+
const ids = Array.isArray(threadIDs) ? threadIDs.map(String) : [String(threadIDs)];
|
|
108
|
+
if (ids.length === 0) throw new Error("changeArchivedStatus: no threadIDs provided");
|
|
109
|
+
|
|
110
|
+
await globalShield.addSmartDelay();
|
|
111
|
+
|
|
112
|
+
let result;
|
|
113
|
+
if (ctx.mqttClient) {
|
|
114
|
+
try {
|
|
115
|
+
result = await retryOp(() => mqttArchive(ctx, ids, archive));
|
|
116
|
+
} catch (mqttErr) {
|
|
117
|
+
utils.warn("changeArchivedStatus", "MQTT failed, using HTTP:", mqttErr.message);
|
|
118
|
+
result = await retryOp(() => httpArchive(defaultFuncs, ctx, ids, archive));
|
|
119
|
+
}
|
|
120
|
+
} else {
|
|
121
|
+
result = await retryOp(() => httpArchive(defaultFuncs, ctx, ids, archive));
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
ids.forEach(id => CACHE.delete(`archive_${id}`));
|
|
125
|
+
CACHE.set(`archive_batch_${ids.join("_")}`, { val: result, ts: Date.now() });
|
|
126
|
+
|
|
127
|
+
callback(null, result);
|
|
128
|
+
} catch (err) {
|
|
129
|
+
utils.error("changeArchivedStatus", err);
|
|
130
|
+
callback(err);
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
return returnPromise;
|
|
134
|
+
};
|
|
135
|
+
};
|
|
@@ -0,0 +1,123 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
4
|
+
|
|
5
|
+
async function retryOp(fn, retries = 3, base = 800) {
|
|
6
|
+
for (let i = 0; i < retries; i++) {
|
|
7
|
+
try { return await fn(); } catch (err) {
|
|
8
|
+
if (i === retries - 1) throw err;
|
|
9
|
+
await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 300));
|
|
10
|
+
}
|
|
11
|
+
}
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
async function fetchImageStream(url) {
|
|
15
|
+
const https = require('https');
|
|
16
|
+
const http = require('http');
|
|
17
|
+
const { PassThrough } = require('stream');
|
|
18
|
+
|
|
19
|
+
return new Promise((resolve, reject) => {
|
|
20
|
+
const mod = url.startsWith('https') ? https : http;
|
|
21
|
+
mod.get(url, (res) => {
|
|
22
|
+
if (res.statusCode >= 300 && res.statusCode < 400 && res.headers.location) {
|
|
23
|
+
fetchImageStream(res.headers.location).then(resolve).catch(reject);
|
|
24
|
+
return;
|
|
25
|
+
}
|
|
26
|
+
if (res.statusCode !== 200) {
|
|
27
|
+
reject(new Error(`Failed to fetch image: HTTP ${res.statusCode}`));
|
|
28
|
+
return;
|
|
29
|
+
}
|
|
30
|
+
const pt = new PassThrough();
|
|
31
|
+
pt.path = url.split('?')[0].split('/').pop() || 'avatar.jpg';
|
|
32
|
+
res.pipe(pt);
|
|
33
|
+
resolve(pt);
|
|
34
|
+
}).on('error', reject);
|
|
35
|
+
});
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
async function uploadProfileImage(api, botID, imageStream) {
|
|
39
|
+
const raw = await api.httpPostFormData(
|
|
40
|
+
`https://www.facebook.com/profile/picture/upload/?profile_id=${botID}&photo_source=57&av=${botID}`,
|
|
41
|
+
{ file: imageStream }
|
|
42
|
+
);
|
|
43
|
+
return JSON.parse(raw.split("for (;;);")[1]);
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
module.exports = (defaultFuncs, api, ctx) => {
|
|
47
|
+
return async function changeAvatar(link, caption, callback) {
|
|
48
|
+
let resolveFunc, rejectFunc;
|
|
49
|
+
const returnPromise = new Promise((resolve, reject) => {
|
|
50
|
+
resolveFunc = resolve;
|
|
51
|
+
rejectFunc = reject;
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
if (typeof caption === "function") { callback = caption; caption = ""; }
|
|
55
|
+
if (typeof callback !== "function") {
|
|
56
|
+
callback = (err, data) => {
|
|
57
|
+
if (err) return rejectFunc(err);
|
|
58
|
+
resolveFunc(data);
|
|
59
|
+
};
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
try {
|
|
63
|
+
if (!link || typeof link !== "string") throw new Error("changeAvatar: link must be a non-empty string");
|
|
64
|
+
if (typeof caption !== "string") caption = "";
|
|
65
|
+
|
|
66
|
+
const imageStream = await retryOp(() => fetchImageStream(link));
|
|
67
|
+
|
|
68
|
+
const uploadData = await retryOp(() => uploadProfileImage(api, ctx.userID, imageStream));
|
|
69
|
+
|
|
70
|
+
if (uploadData.error) {
|
|
71
|
+
throw new Error(`Upload failed: ${JSON.stringify(uploadData.error)}`);
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
const fbid = uploadData.payload?.fbid;
|
|
75
|
+
if (!fbid) throw new Error("changeAvatar: could not get fbid from upload response");
|
|
76
|
+
|
|
77
|
+
const form = {
|
|
78
|
+
av: ctx.userID,
|
|
79
|
+
fb_api_req_friendly_name: "ProfileCometProfilePictureSetMutation",
|
|
80
|
+
fb_api_caller_class: "RelayModern",
|
|
81
|
+
doc_id: "5066134240065849",
|
|
82
|
+
variables: JSON.stringify({
|
|
83
|
+
input: {
|
|
84
|
+
caption: caption || "",
|
|
85
|
+
existing_photo_id: fbid,
|
|
86
|
+
expiration_time: null,
|
|
87
|
+
profile_id: ctx.userID,
|
|
88
|
+
profile_pic_method: "EXISTING",
|
|
89
|
+
profile_pic_source: "TIMELINE",
|
|
90
|
+
scaled_crop_rect: { height: 1, width: 1, x: 0, y: 0 },
|
|
91
|
+
skip_cropping: true,
|
|
92
|
+
actor_id: ctx.userID,
|
|
93
|
+
client_mutation_id: String(Math.round(Math.random() * 19))
|
|
94
|
+
},
|
|
95
|
+
isPage: false,
|
|
96
|
+
isProfile: true,
|
|
97
|
+
scale: 3
|
|
98
|
+
})
|
|
99
|
+
};
|
|
100
|
+
|
|
101
|
+
const resData = await retryOp(() =>
|
|
102
|
+
defaultFuncs.post("https://www.facebook.com/api/graphql/", ctx.jar, form)
|
|
103
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
|
104
|
+
);
|
|
105
|
+
|
|
106
|
+
if (resData.error) throw resData;
|
|
107
|
+
|
|
108
|
+
const result = {
|
|
109
|
+
success: true,
|
|
110
|
+
fbid,
|
|
111
|
+
caption,
|
|
112
|
+
timestamp: Date.now()
|
|
113
|
+
};
|
|
114
|
+
|
|
115
|
+
callback(null, result);
|
|
116
|
+
} catch (err) {
|
|
117
|
+
utils.error("changeAvatar", err);
|
|
118
|
+
callback(err);
|
|
119
|
+
}
|
|
120
|
+
|
|
121
|
+
return returnPromise;
|
|
122
|
+
};
|
|
123
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
4
|
+
|
|
5
|
+
const BIO_CACHE = new Map();
|
|
6
|
+
const BIO_CACHE_TTL = 10 * 60 * 1000;
|
|
7
|
+
|
|
8
|
+
async function retryOp(fn, retries = 3, base = 600) {
|
|
9
|
+
for (let i = 0; i < retries; i++) {
|
|
10
|
+
try { return await fn(); } catch (err) {
|
|
11
|
+
if (i === retries - 1) throw err;
|
|
12
|
+
await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 200));
|
|
13
|
+
}
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports = (defaultFuncs, api, ctx) => {
|
|
18
|
+
return async function changeBio(bio, publish, callback) {
|
|
19
|
+
let resolveFunc, rejectFunc;
|
|
20
|
+
const returnPromise = new Promise((resolve, reject) => {
|
|
21
|
+
resolveFunc = resolve;
|
|
22
|
+
rejectFunc = reject;
|
|
23
|
+
});
|
|
24
|
+
|
|
25
|
+
if (typeof publish === "function") { callback = publish; publish = false; }
|
|
26
|
+
if (typeof callback !== "function") {
|
|
27
|
+
callback = (err, data) => {
|
|
28
|
+
if (err) return rejectFunc(err);
|
|
29
|
+
resolveFunc(data);
|
|
30
|
+
};
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
try {
|
|
34
|
+
const actorID = ctx.i_userID || ctx.userID;
|
|
35
|
+
if (!actorID) throw new Error("changeBio: no userID in context");
|
|
36
|
+
|
|
37
|
+
if (typeof bio !== "string") bio = "";
|
|
38
|
+
if (typeof publish !== "boolean") publish = false;
|
|
39
|
+
|
|
40
|
+
const MAX_BIO_LEN = 101;
|
|
41
|
+
if (bio.length > MAX_BIO_LEN) {
|
|
42
|
+
utils.warn("changeBio", `Bio is longer than ${MAX_BIO_LEN} characters, it may be truncated by Facebook.`);
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
const form = {
|
|
46
|
+
fb_api_caller_class: "RelayModern",
|
|
47
|
+
fb_api_req_friendly_name: "ProfileCometSetBioMutation",
|
|
48
|
+
doc_id: "2725043627607610",
|
|
49
|
+
variables: JSON.stringify({
|
|
50
|
+
input: {
|
|
51
|
+
bio,
|
|
52
|
+
publish_bio_feed_story: publish,
|
|
53
|
+
actor_id: actorID,
|
|
54
|
+
client_mutation_id: String(Math.round(Math.random() * 1024))
|
|
55
|
+
},
|
|
56
|
+
hasProfileTileViewID: false,
|
|
57
|
+
profileTileViewID: null,
|
|
58
|
+
scale: 1
|
|
59
|
+
}),
|
|
60
|
+
av: actorID
|
|
61
|
+
};
|
|
62
|
+
|
|
63
|
+
const res = await retryOp(() =>
|
|
64
|
+
defaultFuncs.post("https://www.facebook.com/api/graphql/", ctx.jar, form)
|
|
65
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
|
66
|
+
);
|
|
67
|
+
|
|
68
|
+
if (res.errors) throw res;
|
|
69
|
+
|
|
70
|
+
BIO_CACHE.set(actorID, { bio, ts: Date.now() });
|
|
71
|
+
|
|
72
|
+
callback(null, {
|
|
73
|
+
success: true,
|
|
74
|
+
bio,
|
|
75
|
+
published: publish,
|
|
76
|
+
actorID,
|
|
77
|
+
timestamp: Date.now()
|
|
78
|
+
});
|
|
79
|
+
} catch (err) {
|
|
80
|
+
utils.error("changeBio", err);
|
|
81
|
+
callback(err);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return returnPromise;
|
|
85
|
+
};
|
|
86
|
+
};
|
|
@@ -0,0 +1,86 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
4
|
+
const { globalShield } = require('../datastore/models/matrix/ghost');
|
|
5
|
+
|
|
6
|
+
const BLOCKED_CACHE = new Map();
|
|
7
|
+
const CACHE_TTL = 3 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
async function retryOp(fn, retries = 3, base = 600) {
|
|
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() * 250));
|
|
14
|
+
}
|
|
15
|
+
}
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
async function getBlockStatusViaGraphQL(defaultFuncs, ctx, userID) {
|
|
19
|
+
const form = {
|
|
20
|
+
fb_api_caller_class: "RelayModern",
|
|
21
|
+
fb_api_req_friendly_name: "ProfileCometFriendshipStatusQuery",
|
|
22
|
+
doc_id: "6985929504808743",
|
|
23
|
+
variables: JSON.stringify({ userID })
|
|
24
|
+
};
|
|
25
|
+
const res = await defaultFuncs.post("https://www.facebook.com/api/graphql/", ctx.jar, form)
|
|
26
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
27
|
+
return res?.data?.user?.friendship_status || null;
|
|
28
|
+
}
|
|
29
|
+
|
|
30
|
+
module.exports = (defaultFuncs, api, ctx) => {
|
|
31
|
+
return async function changeBlockedStatus(userID, block, callback) {
|
|
32
|
+
let resolveFunc, rejectFunc;
|
|
33
|
+
const returnPromise = new Promise((resolve, reject) => {
|
|
34
|
+
resolveFunc = resolve;
|
|
35
|
+
rejectFunc = reject;
|
|
36
|
+
});
|
|
37
|
+
|
|
38
|
+
if (typeof block === "function") { callback = block; block = true; }
|
|
39
|
+
if (typeof callback !== "function") {
|
|
40
|
+
callback = (err, result) => {
|
|
41
|
+
if (err) return rejectFunc(err);
|
|
42
|
+
resolveFunc(result);
|
|
43
|
+
};
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
try {
|
|
47
|
+
if (!userID) throw new Error("changeBlockedStatus: userID is required");
|
|
48
|
+
if (typeof block !== "boolean") throw new Error("changeBlockedStatus: block must be a boolean");
|
|
49
|
+
|
|
50
|
+
const userIDs = Array.isArray(userID) ? userID.map(String) : [String(userID)];
|
|
51
|
+
|
|
52
|
+
await globalShield.addSmartDelay();
|
|
53
|
+
|
|
54
|
+
const results = await Promise.all(userIDs.map(async uid => {
|
|
55
|
+
const cacheKey = `blocked_${uid}`;
|
|
56
|
+
const cached = BLOCKED_CACHE.get(cacheKey);
|
|
57
|
+
if (cached && (Date.now() - cached.ts < CACHE_TTL) && cached.blocked === block) {
|
|
58
|
+
return { userID: uid, blocked: block, cached: true };
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
const form = { fbid: uid, block };
|
|
62
|
+
|
|
63
|
+
const res = await retryOp(() =>
|
|
64
|
+
defaultFuncs.post(
|
|
65
|
+
"https://www.facebook.com/ajax/profile/manage_blocking.php",
|
|
66
|
+
ctx.jar,
|
|
67
|
+
form
|
|
68
|
+
).then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
|
69
|
+
);
|
|
70
|
+
|
|
71
|
+
if (res && res.error) throw res;
|
|
72
|
+
|
|
73
|
+
BLOCKED_CACHE.set(cacheKey, { blocked: block, ts: Date.now() });
|
|
74
|
+
return { userID: uid, blocked: block, success: true };
|
|
75
|
+
}));
|
|
76
|
+
|
|
77
|
+
const data = userIDs.length === 1 ? results[0] : { success: true, results };
|
|
78
|
+
callback(null, data);
|
|
79
|
+
} catch (err) {
|
|
80
|
+
utils.error("changeBlockedStatus", err);
|
|
81
|
+
callback(err);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
return returnPromise;
|
|
85
|
+
};
|
|
86
|
+
};
|