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,395 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Sticker API Module — Advanced for Phantom SDK
|
|
5
|
+
*
|
|
6
|
+
* Provides access to Facebook's GraphQL-based sticker endpoints.
|
|
7
|
+
* Made by @ChoruOfficial — massively extended for phantom-fca
|
|
8
|
+
*
|
|
9
|
+
* Features:
|
|
10
|
+
* - Sticker search with pagination support
|
|
11
|
+
* - Pack listing, store browsing, and full auto-pagination
|
|
12
|
+
* - AI-generated & trending sticker fetching
|
|
13
|
+
* - Individual sticker metadata resolution
|
|
14
|
+
* - Pack add/remove management
|
|
15
|
+
* - In-memory caching (5 minutes)
|
|
16
|
+
* - Exponential-backoff retry on transient GraphQL errors
|
|
17
|
+
* - Parallel batch sticker resolution
|
|
18
|
+
* - Recent sticker tray management
|
|
19
|
+
*/
|
|
20
|
+
|
|
21
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
22
|
+
|
|
23
|
+
const _cache = new Map();
|
|
24
|
+
const CACHE_TTL = 5 * 60 * 1000;
|
|
25
|
+
|
|
26
|
+
// ── Shared helpers ────────────────────────────────────────────────────────────
|
|
27
|
+
function cached(key, fn) {
|
|
28
|
+
const hit = _cache.get(key);
|
|
29
|
+
if (hit && Date.now() - hit.ts < CACHE_TTL) return Promise.resolve(hit.data);
|
|
30
|
+
return fn().then(data => { _cache.set(key, { data, ts: Date.now() }); return data; });
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function normalizeSticker(node) {
|
|
34
|
+
if (!node) return null;
|
|
35
|
+
return {
|
|
36
|
+
type: 'sticker',
|
|
37
|
+
ID: node.id,
|
|
38
|
+
stickerID: node.id,
|
|
39
|
+
url: node.image?.uri || node.url || null,
|
|
40
|
+
animatedUrl: node.animated_image?.uri || node.animatedUrl || null,
|
|
41
|
+
spriteUrl: node.sprite_image?.uri || null,
|
|
42
|
+
packID: node.pack?.id || node.packID || null,
|
|
43
|
+
packName: node.pack?.name || null,
|
|
44
|
+
label: node.label || node.accessibility_label || null,
|
|
45
|
+
width: node.image?.width || null,
|
|
46
|
+
height: node.image?.height || null,
|
|
47
|
+
isAnimated: !!(node.animated_image?.uri),
|
|
48
|
+
};
|
|
49
|
+
}
|
|
50
|
+
|
|
51
|
+
function normalizePack(node) {
|
|
52
|
+
if (!node) return null;
|
|
53
|
+
return {
|
|
54
|
+
id: node.id,
|
|
55
|
+
name: node.name,
|
|
56
|
+
thumbnail: node.thumbnail_image?.uri || null,
|
|
57
|
+
stickerCount: node.sticker_count || null,
|
|
58
|
+
isOwned: node.is_owned !== undefined ? node.is_owned : null,
|
|
59
|
+
isNew: node.is_new || false,
|
|
60
|
+
};
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// ── Formatters ────────────────────────────────────────────────────────────────
|
|
64
|
+
function formatPackList(data) {
|
|
65
|
+
const trayPacks = data?.data?.picker_plugins?.sticker_picker?.sticker_store?.tray_packs;
|
|
66
|
+
const storePacks = data?.data?.viewer?.sticker_store?.available_packs;
|
|
67
|
+
const packData = storePacks || trayPacks;
|
|
68
|
+
|
|
69
|
+
if (!packData) return { packs: [], page_info: { has_next_page: false }, store_id: null };
|
|
70
|
+
|
|
71
|
+
const edges = Array.isArray(packData) ? packData : packData.edges || [];
|
|
72
|
+
return {
|
|
73
|
+
packs: edges.map(e => normalizePack(e.node || e)).filter(Boolean),
|
|
74
|
+
page_info: packData.page_info || { has_next_page: false },
|
|
75
|
+
store_id: data?.data?.viewer?.sticker_store?.id || null,
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function formatStickerSearchResults(data) {
|
|
80
|
+
const edges = data?.data?.sticker_search?.sticker_results?.edges || [];
|
|
81
|
+
return edges.map(e => normalizeSticker(e.node)).filter(Boolean);
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
function formatStickerPackResults(data) {
|
|
85
|
+
const edges = data?.data?.sticker_pack?.stickers?.edges || [];
|
|
86
|
+
return edges.map(e => normalizeSticker(e.node)).filter(Boolean);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
function formatAiStickers(data) {
|
|
90
|
+
const nodes = data?.data?.xfb_trending_generated_ai_stickers?.nodes || [];
|
|
91
|
+
return nodes.map(n => normalizeSticker(n)).filter(Boolean);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
function formatRecentStickers(data) {
|
|
95
|
+
const edges = data?.data?.viewer?.recent_stickers?.edges || [];
|
|
96
|
+
return edges.map(e => normalizeSticker(e.node)).filter(Boolean);
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
function formatTrayStickers(data) {
|
|
100
|
+
const plugins = data?.data?.picker_plugins?.sticker_picker?.recent_stickers;
|
|
101
|
+
if (!plugins) return [];
|
|
102
|
+
const stickers = Array.isArray(plugins) ? plugins : plugins.edges?.map(e => e.node) || [];
|
|
103
|
+
return stickers.map(normalizeSticker).filter(Boolean);
|
|
104
|
+
}
|
|
105
|
+
|
|
106
|
+
// ── Module export ─────────────────────────────────────────────────────────────
|
|
107
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
108
|
+
|
|
109
|
+
// ── Request helper with retry ─────────────────────────────────────────────
|
|
110
|
+
async function makeRequest(form, retries = 3, baseMs = 500) {
|
|
111
|
+
let lastErr;
|
|
112
|
+
for (let attempt = 1; attempt <= retries; attempt++) {
|
|
113
|
+
try {
|
|
114
|
+
const resData = await defaultFuncs
|
|
115
|
+
.post("https://www.facebook.com/api/graphql/", ctx.jar, form)
|
|
116
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
117
|
+
|
|
118
|
+
if (!resData) throw new Error("GraphQL returned no data");
|
|
119
|
+
if (resData.errors) {
|
|
120
|
+
const msg = resData.errors[0]?.message || JSON.stringify(resData.errors);
|
|
121
|
+
throw Object.assign(new Error(msg), { isFatal: /auth|permission|checkpoint/i.test(msg) });
|
|
122
|
+
}
|
|
123
|
+
return resData;
|
|
124
|
+
} catch (err) {
|
|
125
|
+
lastErr = err;
|
|
126
|
+
if (err.isFatal || attempt >= retries) throw err;
|
|
127
|
+
const wait = baseMs * Math.pow(2, attempt - 1) + Math.random() * 200;
|
|
128
|
+
utils.warn("StickerAPI", `Retry ${attempt}/${retries} in ${Math.round(wait)}ms`);
|
|
129
|
+
await new Promise(r => setTimeout(r, wait));
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
throw lastErr;
|
|
133
|
+
}
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
|
|
137
|
+
/**
|
|
138
|
+
* Search for stickers by keyword.
|
|
139
|
+
* @param {string} query
|
|
140
|
+
* @param {object} [options] { limit, width, height }
|
|
141
|
+
* @returns {Promise<Array>}
|
|
142
|
+
*/
|
|
143
|
+
search: async function (query, options = {}) {
|
|
144
|
+
if (!query) throw new Error("stickers.search: query is required");
|
|
145
|
+
const w = options.width || 128;
|
|
146
|
+
const h = options.height || 128;
|
|
147
|
+
const form = {
|
|
148
|
+
fb_api_caller_class: 'RelayModern',
|
|
149
|
+
fb_api_req_friendly_name: 'CometStickerPickerSearchResultsRootQuery',
|
|
150
|
+
variables: JSON.stringify({
|
|
151
|
+
scale: 3,
|
|
152
|
+
search_query: query.trim(),
|
|
153
|
+
sticker_height: h,
|
|
154
|
+
sticker_width: w,
|
|
155
|
+
stickerInterface: "MESSAGES",
|
|
156
|
+
}),
|
|
157
|
+
doc_id: '24004987559125954',
|
|
158
|
+
};
|
|
159
|
+
const res = await makeRequest(form);
|
|
160
|
+
let stickers = formatStickerSearchResults(res);
|
|
161
|
+
if (options.limit) stickers = stickers.slice(0, options.limit);
|
|
162
|
+
return stickers;
|
|
163
|
+
},
|
|
164
|
+
|
|
165
|
+
/**
|
|
166
|
+
* List the user's installed sticker packs (tray).
|
|
167
|
+
* @returns {Promise<Array>}
|
|
168
|
+
*/
|
|
169
|
+
listPacks: async function () {
|
|
170
|
+
return cached('listPacks', async () => {
|
|
171
|
+
const form = {
|
|
172
|
+
fb_api_caller_class: 'RelayModern',
|
|
173
|
+
fb_api_req_friendly_name: 'CometStickerPickerCardQuery',
|
|
174
|
+
variables: JSON.stringify({ scale: 3, stickerInterface: "MESSAGES" }),
|
|
175
|
+
doc_id: '10095807770482952',
|
|
176
|
+
};
|
|
177
|
+
const res = await makeRequest(form);
|
|
178
|
+
return formatPackList(res).packs;
|
|
179
|
+
});
|
|
180
|
+
},
|
|
181
|
+
|
|
182
|
+
/**
|
|
183
|
+
* Get all available sticker packs from the store (fully auto-paginated).
|
|
184
|
+
* @param {object} [options] { maxPages }
|
|
185
|
+
* @returns {Promise<Array>}
|
|
186
|
+
*/
|
|
187
|
+
getStorePacks: async function (options = {}) {
|
|
188
|
+
const maxPages = options.maxPages || 50;
|
|
189
|
+
utils.log("StickerAPI", "Fetching all store packs (auto-paginated)...");
|
|
190
|
+
let allPacks = [], page = 0;
|
|
191
|
+
|
|
192
|
+
const firstForm = {
|
|
193
|
+
fb_api_caller_class: 'RelayModern',
|
|
194
|
+
fb_api_req_friendly_name: 'CometStickersStoreDialogQuery',
|
|
195
|
+
variables: JSON.stringify({}),
|
|
196
|
+
doc_id: '29237828849196584',
|
|
197
|
+
};
|
|
198
|
+
let res = await makeRequest(firstForm);
|
|
199
|
+
let { packs, page_info, store_id } = formatPackList(res);
|
|
200
|
+
allPacks.push(...packs);
|
|
201
|
+
page++;
|
|
202
|
+
|
|
203
|
+
while (page_info?.has_next_page && page < maxPages) {
|
|
204
|
+
const nextForm = {
|
|
205
|
+
fb_api_caller_class: 'RelayModern',
|
|
206
|
+
fb_api_req_friendly_name: 'CometStickersStorePackListPaginationQuery',
|
|
207
|
+
variables: JSON.stringify({ count: 20, cursor: page_info.end_cursor, id: store_id }),
|
|
208
|
+
doc_id: '9898634630218439',
|
|
209
|
+
};
|
|
210
|
+
res = await makeRequest(nextForm);
|
|
211
|
+
const next = formatPackList(res);
|
|
212
|
+
allPacks.push(...next.packs);
|
|
213
|
+
page_info = next.page_info;
|
|
214
|
+
page++;
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
utils.log("StickerAPI", `Fetched ${allPacks.length} store packs in ${page} pages`);
|
|
218
|
+
return allPacks;
|
|
219
|
+
},
|
|
220
|
+
|
|
221
|
+
/**
|
|
222
|
+
* Merge user's tray packs with store packs into a deduplicated list.
|
|
223
|
+
* @returns {Promise<Array>}
|
|
224
|
+
*/
|
|
225
|
+
listAllPacks: async function () {
|
|
226
|
+
const [myPacks, storePacks] = await Promise.all([this.listPacks(), this.getStorePacks()]);
|
|
227
|
+
const map = new Map();
|
|
228
|
+
[...myPacks, ...storePacks].forEach(p => map.set(p.id, p));
|
|
229
|
+
return Array.from(map.values());
|
|
230
|
+
},
|
|
231
|
+
|
|
232
|
+
/**
|
|
233
|
+
* Add a sticker pack by ID.
|
|
234
|
+
* @param {string} packID
|
|
235
|
+
* @returns {Promise<Object>}
|
|
236
|
+
*/
|
|
237
|
+
addPack: async function (packID) {
|
|
238
|
+
if (!packID) throw new Error("stickers.addPack: packID is required");
|
|
239
|
+
const form = {
|
|
240
|
+
fb_api_caller_class: 'RelayModern',
|
|
241
|
+
fb_api_req_friendly_name: 'CometStickersStorePackMutationAddMutation',
|
|
242
|
+
variables: JSON.stringify({
|
|
243
|
+
input: {
|
|
244
|
+
pack_id: packID,
|
|
245
|
+
actor_id: ctx.userID,
|
|
246
|
+
client_mutation_id: String(Date.now() % 1e9),
|
|
247
|
+
}
|
|
248
|
+
}),
|
|
249
|
+
doc_id: '9877489362345320',
|
|
250
|
+
};
|
|
251
|
+
const res = await makeRequest(form);
|
|
252
|
+
return normalizePack(res.data?.sticker_pack_add?.sticker_pack) || res.data;
|
|
253
|
+
},
|
|
254
|
+
|
|
255
|
+
/**
|
|
256
|
+
* Remove a sticker pack by ID.
|
|
257
|
+
* @param {string} packID
|
|
258
|
+
* @returns {Promise<Object>}
|
|
259
|
+
*/
|
|
260
|
+
removePack: async function (packID) {
|
|
261
|
+
if (!packID) throw new Error("stickers.removePack: packID is required");
|
|
262
|
+
const form = {
|
|
263
|
+
fb_api_caller_class: 'RelayModern',
|
|
264
|
+
fb_api_req_friendly_name: 'CometStickersStorePackMutationRemoveMutation',
|
|
265
|
+
variables: JSON.stringify({
|
|
266
|
+
input: {
|
|
267
|
+
pack_id: packID,
|
|
268
|
+
actor_id: ctx.userID,
|
|
269
|
+
client_mutation_id: String(Date.now() % 1e9),
|
|
270
|
+
}
|
|
271
|
+
}),
|
|
272
|
+
doc_id: '25244534060527988',
|
|
273
|
+
};
|
|
274
|
+
const res = await makeRequest(form);
|
|
275
|
+
return res.data;
|
|
276
|
+
},
|
|
277
|
+
|
|
278
|
+
/**
|
|
279
|
+
* Get all stickers inside a specific pack.
|
|
280
|
+
* @param {string} packID
|
|
281
|
+
* @param {object} [options] { width, height }
|
|
282
|
+
* @returns {Promise<Array>}
|
|
283
|
+
*/
|
|
284
|
+
getStickersInPack: async function (packID, options = {}) {
|
|
285
|
+
if (!packID) throw new Error("stickers.getStickersInPack: packID is required");
|
|
286
|
+
return cached(`pack::${packID}`, async () => {
|
|
287
|
+
const form = {
|
|
288
|
+
fb_api_caller_class: 'RelayModern',
|
|
289
|
+
fb_api_req_friendly_name: 'CometStickerPickerPackContentRootQuery',
|
|
290
|
+
variables: JSON.stringify({
|
|
291
|
+
packID,
|
|
292
|
+
stickerWidth: options.width || 128,
|
|
293
|
+
stickerHeight: options.height || 128,
|
|
294
|
+
scale: 3,
|
|
295
|
+
}),
|
|
296
|
+
doc_id: '23982341384707469',
|
|
297
|
+
};
|
|
298
|
+
const res = await makeRequest(form);
|
|
299
|
+
return formatStickerPackResults(res);
|
|
300
|
+
});
|
|
301
|
+
},
|
|
302
|
+
|
|
303
|
+
/**
|
|
304
|
+
* Get trending AI-generated stickers.
|
|
305
|
+
* @param {object} [options] { limit }
|
|
306
|
+
* @returns {Promise<Array>}
|
|
307
|
+
*/
|
|
308
|
+
getAiStickers: async function ({ limit = 20 } = {}) {
|
|
309
|
+
return cached(`ai_stickers::${limit}`, async () => {
|
|
310
|
+
const form = {
|
|
311
|
+
fb_api_caller_class: 'RelayModern',
|
|
312
|
+
fb_api_req_friendly_name: 'CometStickerPickerStickerGeneratedCardQuery',
|
|
313
|
+
variables: JSON.stringify({ limit }),
|
|
314
|
+
doc_id: '24151467751156443',
|
|
315
|
+
};
|
|
316
|
+
const res = await makeRequest(form);
|
|
317
|
+
return formatAiStickers(res);
|
|
318
|
+
});
|
|
319
|
+
},
|
|
320
|
+
|
|
321
|
+
/**
|
|
322
|
+
* Get the user's recent stickers.
|
|
323
|
+
* @param {object} [options] { limit }
|
|
324
|
+
* @returns {Promise<Array>}
|
|
325
|
+
*/
|
|
326
|
+
getRecentStickers: async function ({ limit = 30 } = {}) {
|
|
327
|
+
const form = {
|
|
328
|
+
fb_api_caller_class: 'RelayModern',
|
|
329
|
+
fb_api_req_friendly_name: 'CometStickerPickerRecentStickersQuery',
|
|
330
|
+
variables: JSON.stringify({ scale: 3, stickerInterface: "MESSAGES" }),
|
|
331
|
+
doc_id: '10095807770482952',
|
|
332
|
+
};
|
|
333
|
+
const res = await makeRequest(form);
|
|
334
|
+
const tray = formatTrayStickers(res);
|
|
335
|
+
return tray.slice(0, limit);
|
|
336
|
+
},
|
|
337
|
+
|
|
338
|
+
/**
|
|
339
|
+
* Resolve full metadata for one or many sticker IDs in parallel.
|
|
340
|
+
* @param {string|string[]} stickerIDs
|
|
341
|
+
* @returns {Promise<object|object[]>}
|
|
342
|
+
*/
|
|
343
|
+
resolveSticker: async function (stickerIDs) {
|
|
344
|
+
const ids = Array.isArray(stickerIDs) ? stickerIDs : [stickerIDs];
|
|
345
|
+
const form = {
|
|
346
|
+
fb_api_caller_class: 'RelayModern',
|
|
347
|
+
fb_api_req_friendly_name: 'CometStickerPickerStickerQuery',
|
|
348
|
+
variables: JSON.stringify({ stickerIDs: ids, scale: 3 }),
|
|
349
|
+
doc_id: '9580756598638278',
|
|
350
|
+
};
|
|
351
|
+
try {
|
|
352
|
+
const res = await makeRequest(form);
|
|
353
|
+
const nodes = (res?.data?.stickers || ids.map(() => null));
|
|
354
|
+
const resolved = (Array.isArray(nodes) ? nodes : Object.values(nodes))
|
|
355
|
+
.map(n => normalizeSticker(n)).filter(Boolean);
|
|
356
|
+
return ids.length === 1 ? (resolved[0] || null) : resolved;
|
|
357
|
+
} catch {
|
|
358
|
+
// Fallback: return minimal objects
|
|
359
|
+
const fallback = ids.map(id => ({ type: 'sticker', ID: id, stickerID: id }));
|
|
360
|
+
return ids.length === 1 ? fallback[0] : fallback;
|
|
361
|
+
}
|
|
362
|
+
},
|
|
363
|
+
|
|
364
|
+
/**
|
|
365
|
+
* Generate an AI sticker from a text prompt.
|
|
366
|
+
* @param {string} prompt
|
|
367
|
+
* @param {object} [options] { numResults }
|
|
368
|
+
* @returns {Promise<Array>}
|
|
369
|
+
*/
|
|
370
|
+
generateAiSticker: async function (prompt, { numResults = 4 } = {}) {
|
|
371
|
+
if (!prompt) throw new Error("stickers.generateAiSticker: prompt is required");
|
|
372
|
+
const form = {
|
|
373
|
+
av: ctx.userID,
|
|
374
|
+
__user: ctx.userID,
|
|
375
|
+
fb_dtsg: ctx.fb_dtsg,
|
|
376
|
+
fb_api_caller_class: 'RelayModern',
|
|
377
|
+
fb_api_req_friendly_name: 'useGenerateAIStickerMutation',
|
|
378
|
+
variables: JSON.stringify({
|
|
379
|
+
input: {
|
|
380
|
+
client_mutation_id: String(Date.now() % 1e9),
|
|
381
|
+
actor_id: ctx.userID,
|
|
382
|
+
num_results: Math.min(numResults, 8),
|
|
383
|
+
prompt: prompt.trim(),
|
|
384
|
+
}
|
|
385
|
+
}),
|
|
386
|
+
server_timestamps: true,
|
|
387
|
+
doc_id: '6774614342659830',
|
|
388
|
+
};
|
|
389
|
+
const res = await makeRequest(form);
|
|
390
|
+
const stickers = res?.data?.xfb_generate_ai_sticker?.stickers || [];
|
|
391
|
+
return stickers.map(normalizeSticker).filter(Boolean);
|
|
392
|
+
},
|
|
393
|
+
|
|
394
|
+
};
|
|
395
|
+
};
|
|
@@ -0,0 +1,240 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
4
|
+
const { URL } = require('url');
|
|
5
|
+
const { globalShield } = require('../datastore/models/matrix/ghost');
|
|
6
|
+
|
|
7
|
+
const STORY_CACHE = new Map();
|
|
8
|
+
const STORY_CACHE_TTL = 5 * 60 * 1000;
|
|
9
|
+
const STORY_HISTORY = [];
|
|
10
|
+
const MAX_HISTORY = 300;
|
|
11
|
+
|
|
12
|
+
const ALLOWED_REACTIONS = new Set(['❤️', '👍', '🤗', '😆', '😡', '😢', '😮']);
|
|
13
|
+
|
|
14
|
+
const FONT_MAP = {
|
|
15
|
+
headline: '1919119914775364',
|
|
16
|
+
classic: '516266749248495',
|
|
17
|
+
casual: '516266749248495',
|
|
18
|
+
fancy: '1790435664339626',
|
|
19
|
+
bold: '1919119914775364'
|
|
20
|
+
};
|
|
21
|
+
|
|
22
|
+
const BG_MAP = {
|
|
23
|
+
orange: '2163607613910521',
|
|
24
|
+
blue: '401372137331149',
|
|
25
|
+
green: '367314917184744',
|
|
26
|
+
modern: '554617635055752',
|
|
27
|
+
dark: '2163607613910521',
|
|
28
|
+
light: '401372137331149'
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
async function retryOp(fn, retries = 4, base = 600) {
|
|
32
|
+
for (let i = 0; i < retries; i++) {
|
|
33
|
+
try { return await fn(); } catch (err) {
|
|
34
|
+
if (i === retries - 1) throw err;
|
|
35
|
+
const transient = /network|timeout|ECONNRESET|ETIMEDOUT|5\d\d|429/i.test(String(err?.message || err));
|
|
36
|
+
if (!transient) throw err;
|
|
37
|
+
await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 300));
|
|
38
|
+
}
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
function extractStoryIDFromURL(url) {
|
|
43
|
+
try {
|
|
44
|
+
const u = new URL(url);
|
|
45
|
+
const parts = u.pathname.split('/');
|
|
46
|
+
const idx = parts.indexOf('stories');
|
|
47
|
+
if (idx !== -1 && parts.length > idx + 2) return parts[idx + 2];
|
|
48
|
+
const storyParam = u.searchParams.get('story_fbid');
|
|
49
|
+
if (storyParam) return storyParam;
|
|
50
|
+
return null;
|
|
51
|
+
} catch {
|
|
52
|
+
return null;
|
|
53
|
+
}
|
|
54
|
+
}
|
|
55
|
+
|
|
56
|
+
function resolveStoryID(storyIdOrUrl) {
|
|
57
|
+
if (!storyIdOrUrl) throw new Error('story: storyID or URL is required');
|
|
58
|
+
const extracted = extractStoryIDFromURL(storyIdOrUrl);
|
|
59
|
+
return extracted || storyIdOrUrl;
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
function recordHistory(op) {
|
|
63
|
+
STORY_HISTORY.unshift({ ...op, ts: Date.now() });
|
|
64
|
+
if (STORY_HISTORY.length > MAX_HISTORY) STORY_HISTORY.length = MAX_HISTORY;
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
68
|
+
|
|
69
|
+
async function sendStoryReply(storyIdOrUrl, message, isReaction, options = {}) {
|
|
70
|
+
const storyID = resolveStoryID(storyIdOrUrl);
|
|
71
|
+
const { retry = true, timeout = 15000 } = options;
|
|
72
|
+
|
|
73
|
+
if (!message) throw new Error('story: message or reaction is required');
|
|
74
|
+
|
|
75
|
+
if (isReaction && !ALLOWED_REACTIONS.has(message)) {
|
|
76
|
+
throw new Error(`story: invalid reaction "${message}". Allowed: ${Array.from(ALLOWED_REACTIONS).join(' ')}`);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
const variables = {
|
|
80
|
+
input: {
|
|
81
|
+
attribution_id_v2: 'StoriesCometSuspenseRoot.react,comet.stories.viewer,via_cold_start',
|
|
82
|
+
message,
|
|
83
|
+
story_id: storyID,
|
|
84
|
+
story_reply_type: isReaction ? 'LIGHT_WEIGHT' : 'TEXT',
|
|
85
|
+
actor_id: ctx.userID,
|
|
86
|
+
client_mutation_id: String(Math.floor(Math.random() * 10 + 1))
|
|
87
|
+
}
|
|
88
|
+
};
|
|
89
|
+
|
|
90
|
+
if (isReaction) {
|
|
91
|
+
variables.input.lightweight_reaction_actions = { offsets: [0], reaction: message };
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
const form = {
|
|
95
|
+
av: ctx.userID,
|
|
96
|
+
__user: ctx.userID,
|
|
97
|
+
__a: '1',
|
|
98
|
+
fb_dtsg: ctx.fb_dtsg,
|
|
99
|
+
jazoest: ctx.jazoest,
|
|
100
|
+
fb_api_caller_class: 'RelayModern',
|
|
101
|
+
fb_api_req_friendly_name: 'useStoriesSendReplyMutation',
|
|
102
|
+
variables: JSON.stringify(variables),
|
|
103
|
+
doc_id: '9697491553691692'
|
|
104
|
+
};
|
|
105
|
+
|
|
106
|
+
await globalShield.addSmartDelay();
|
|
107
|
+
|
|
108
|
+
const exec = () => defaultFuncs.post('https://www.facebook.com/api/graphql/', ctx.jar, form, {});
|
|
109
|
+
const res = retry ? await retryOp(exec) : await exec();
|
|
110
|
+
|
|
111
|
+
if (res.data?.errors) throw new Error(JSON.stringify(res.data.errors));
|
|
112
|
+
|
|
113
|
+
const storyReplyData = res.data?.data?.direct_message_reply;
|
|
114
|
+
if (!storyReplyData) throw new Error('story: "direct_message_reply" not found in response');
|
|
115
|
+
|
|
116
|
+
recordHistory({ action: isReaction ? 'react' : 'reply', storyID, message });
|
|
117
|
+
return { success: true, storyID, [isReaction ? 'reaction' : 'message']: message, result: storyReplyData, timestamp: Date.now() };
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
async function create(message, fontName = 'classic', backgroundName = 'blue', options = {}) {
|
|
121
|
+
const { retry = true } = options;
|
|
122
|
+
if (!message || typeof message !== 'string') throw new Error('story.create: message must be a non-empty string');
|
|
123
|
+
|
|
124
|
+
const fontId = FONT_MAP[fontName.toLowerCase()] || FONT_MAP.classic;
|
|
125
|
+
const bgId = BG_MAP[backgroundName.toLowerCase()] || BG_MAP.blue;
|
|
126
|
+
|
|
127
|
+
const variables = {
|
|
128
|
+
input: {
|
|
129
|
+
audiences: [{ stories: { self: { target_id: ctx.userID } } }],
|
|
130
|
+
audiences_is_complete: true,
|
|
131
|
+
logging: { composer_session_id: `createStoriesText-${Date.now()}` },
|
|
132
|
+
navigation_data: { attribution_id_v2: 'StoriesCreateRoot.react,comet.stories.create' },
|
|
133
|
+
source: 'WWW',
|
|
134
|
+
message: { ranges: [], text: message },
|
|
135
|
+
text_format_metadata: { inspirations_custom_font_id: fontId },
|
|
136
|
+
text_format_preset_id: bgId,
|
|
137
|
+
tracking: [null],
|
|
138
|
+
actor_id: ctx.userID,
|
|
139
|
+
client_mutation_id: String(Math.floor(Math.random() * 9999))
|
|
140
|
+
}
|
|
141
|
+
};
|
|
142
|
+
|
|
143
|
+
const form = {
|
|
144
|
+
__a: '1',
|
|
145
|
+
fb_dtsg: ctx.fb_dtsg,
|
|
146
|
+
jazoest: ctx.jazoest,
|
|
147
|
+
fb_api_caller_class: 'RelayModern',
|
|
148
|
+
fb_api_req_friendly_name: 'StoriesCreateMutation',
|
|
149
|
+
variables: JSON.stringify(variables),
|
|
150
|
+
doc_id: '24226878183562473'
|
|
151
|
+
};
|
|
152
|
+
|
|
153
|
+
await globalShield.addSmartDelay();
|
|
154
|
+
|
|
155
|
+
const exec = () => defaultFuncs.post('https://www.facebook.com/api/graphql/', ctx.jar, form, {});
|
|
156
|
+
const res = retry ? await retryOp(exec) : await exec();
|
|
157
|
+
|
|
158
|
+
if (res.data?.errors) throw new Error(JSON.stringify(res.data.errors));
|
|
159
|
+
|
|
160
|
+
const storyNode = res.data?.data?.story_create?.viewer?.actor?.story_bucket?.nodes?.[0]?.first_story_to_show;
|
|
161
|
+
if (!storyNode?.id) throw new Error('story.create: storyID not found in response');
|
|
162
|
+
|
|
163
|
+
recordHistory({ action: 'create', message: message.slice(0, 80), font: fontName, bg: backgroundName, storyID: storyNode.id });
|
|
164
|
+
utils.log('story.create', `Story created: ${storyNode.id}`);
|
|
165
|
+
|
|
166
|
+
return { success: true, storyID: storyNode.id, font: fontName, background: backgroundName, timestamp: Date.now() };
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
async function getStory(storyIdOrUrl, options = {}) {
|
|
170
|
+
const storyID = resolveStoryID(storyIdOrUrl);
|
|
171
|
+
const { skipCache = false } = options;
|
|
172
|
+
const cacheKey = `story_${storyID}`;
|
|
173
|
+
|
|
174
|
+
if (!skipCache) {
|
|
175
|
+
const cached = STORY_CACHE.get(cacheKey);
|
|
176
|
+
if (cached && Date.now() - cached.ts < STORY_CACHE_TTL) return { ...cached.result, fromCache: true };
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const form = {
|
|
180
|
+
av: ctx.userID,
|
|
181
|
+
fb_api_caller_class: 'RelayModern',
|
|
182
|
+
fb_api_req_friendly_name: 'StoriesRootQuery',
|
|
183
|
+
variables: JSON.stringify({ story_bucket_id: storyID }),
|
|
184
|
+
doc_id: '9781019695323982'
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
const res = await retryOp(() => defaultFuncs.post('https://www.facebook.com/api/graphql/', ctx.jar, form, {}));
|
|
188
|
+
if (res.data?.errors) throw new Error(JSON.stringify(res.data.errors));
|
|
189
|
+
|
|
190
|
+
const result = { success: true, storyID, data: res.data, timestamp: Date.now() };
|
|
191
|
+
STORY_CACHE.set(cacheKey, { result, ts: Date.now() });
|
|
192
|
+
return result;
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
async function deleteStory(storyID, options = {}) {
|
|
196
|
+
if (!storyID) throw new Error('story.delete: storyID is required');
|
|
197
|
+
const { retry = true } = options;
|
|
198
|
+
|
|
199
|
+
const form = {
|
|
200
|
+
av: ctx.userID,
|
|
201
|
+
__user: ctx.userID,
|
|
202
|
+
fb_dtsg: ctx.fb_dtsg,
|
|
203
|
+
fb_api_caller_class: 'RelayModern',
|
|
204
|
+
fb_api_req_friendly_name: 'StoriesDeleteMutation',
|
|
205
|
+
variables: JSON.stringify({
|
|
206
|
+
input: {
|
|
207
|
+
story_id: String(storyID),
|
|
208
|
+
actor_id: ctx.userID,
|
|
209
|
+
client_mutation_id: String(Math.floor(Math.random() * 9999))
|
|
210
|
+
}
|
|
211
|
+
}),
|
|
212
|
+
doc_id: '5697804170303568'
|
|
213
|
+
};
|
|
214
|
+
|
|
215
|
+
await globalShield.addSmartDelay();
|
|
216
|
+
|
|
217
|
+
const exec = () => defaultFuncs.post('https://www.facebook.com/api/graphql/', ctx.jar, form, {});
|
|
218
|
+
const res = retry ? await retryOp(exec) : await exec();
|
|
219
|
+
if (res.data?.errors) throw new Error(JSON.stringify(res.data.errors));
|
|
220
|
+
|
|
221
|
+
STORY_CACHE.delete(`story_${storyID}`);
|
|
222
|
+
recordHistory({ action: 'delete', storyID });
|
|
223
|
+
return { success: true, storyID, timestamp: Date.now() };
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
return {
|
|
227
|
+
create,
|
|
228
|
+
react: (storyIdOrUrl, reaction, options) => sendStoryReply(storyIdOrUrl, reaction, true, options),
|
|
229
|
+
msg: (storyIdOrUrl, message, options) => sendStoryReply(storyIdOrUrl, message, false, options),
|
|
230
|
+
reply: (storyIdOrUrl, message, options) => sendStoryReply(storyIdOrUrl, message, false, options),
|
|
231
|
+
get: getStory,
|
|
232
|
+
delete: deleteStory,
|
|
233
|
+
extractID: extractStoryIDFromURL,
|
|
234
|
+
getHistory: (limit = 50) => STORY_HISTORY.slice(0, limit),
|
|
235
|
+
clearCache: () => { STORY_CACHE.clear(); return { success: true }; },
|
|
236
|
+
allowedReactions: Array.from(ALLOWED_REACTIONS),
|
|
237
|
+
fonts: Object.keys(FONT_MAP),
|
|
238
|
+
backgrounds: Object.keys(BG_MAP)
|
|
239
|
+
};
|
|
240
|
+
};
|