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,118 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
4
|
+
|
|
5
|
+
const FRIENDS_CACHE = new Map();
|
|
6
|
+
const CACHE_TTL = 5 * 60 * 1000;
|
|
7
|
+
|
|
8
|
+
const GENDERS = {
|
|
9
|
+
0: "unknown", 1: "female_singular", 2: "male_singular",
|
|
10
|
+
3: "female_singular_guess", 4: "male_singular_guess",
|
|
11
|
+
5: "mixed", 6: "neuter_singular", 7: "unknown_singular",
|
|
12
|
+
8: "female_plural", 9: "male_plural", 10: "neuter_plural",
|
|
13
|
+
11: "unknown_plural"
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
function formatData(obj) {
|
|
17
|
+
return Object.keys(obj).map(key => {
|
|
18
|
+
const user = obj[key];
|
|
19
|
+
return {
|
|
20
|
+
alternateName: user.alternateName || null,
|
|
21
|
+
firstName: user.firstName || null,
|
|
22
|
+
gender: GENDERS[user.gender] || "unknown",
|
|
23
|
+
userID: utils.formatID(String(user.id)),
|
|
24
|
+
isFriend: user.is_friend != null ? !!user.is_friend : false,
|
|
25
|
+
fullName: user.name || null,
|
|
26
|
+
profilePicture: user.thumbSrc || null,
|
|
27
|
+
type: user.type || null,
|
|
28
|
+
profileUrl: user.uri || null,
|
|
29
|
+
vanity: user.vanity || null,
|
|
30
|
+
isBirthday: !!user.is_birthday
|
|
31
|
+
};
|
|
32
|
+
});
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
async function retryOp(fn, retries = 3, base = 700) {
|
|
36
|
+
for (let i = 0; i < retries; i++) {
|
|
37
|
+
try { return await fn(); } catch (err) {
|
|
38
|
+
if (i === retries - 1) throw err;
|
|
39
|
+
await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 300));
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
async function fetchViaGraphQL(defaultFuncs, ctx) {
|
|
45
|
+
const form = {
|
|
46
|
+
fb_api_caller_class: "RelayModern",
|
|
47
|
+
fb_api_req_friendly_name: "FriendingCometFriendListQuery",
|
|
48
|
+
doc_id: "28046706820346836",
|
|
49
|
+
variables: JSON.stringify({ count: 1000, cursor: null, scale: 2 })
|
|
50
|
+
};
|
|
51
|
+
const res = await defaultFuncs.post("https://www.facebook.com/api/graphql/", ctx.jar, form)
|
|
52
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
53
|
+
if (res.errors) throw new Error(JSON.stringify(res.errors));
|
|
54
|
+
const edges = res.data?.viewer?.all_friends?.edges || [];
|
|
55
|
+
return edges.map(edge => ({
|
|
56
|
+
userID: edge.node?.id,
|
|
57
|
+
fullName: edge.node?.name,
|
|
58
|
+
profilePicture: edge.node?.profile_picture?.uri,
|
|
59
|
+
profileUrl: edge.node?.url,
|
|
60
|
+
alternateName: null,
|
|
61
|
+
firstName: edge.node?.short_name || null,
|
|
62
|
+
gender: "unknown",
|
|
63
|
+
isFriend: true,
|
|
64
|
+
type: "friend",
|
|
65
|
+
vanity: null,
|
|
66
|
+
isBirthday: false
|
|
67
|
+
}));
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
module.exports = (defaultFuncs, api, ctx) => {
|
|
71
|
+
return async function getFriendsList(callback) {
|
|
72
|
+
let resolveFunc, rejectFunc;
|
|
73
|
+
const returnPromise = new Promise((resolve, reject) => {
|
|
74
|
+
resolveFunc = resolve;
|
|
75
|
+
rejectFunc = reject;
|
|
76
|
+
});
|
|
77
|
+
|
|
78
|
+
if (typeof callback !== "function") {
|
|
79
|
+
callback = (err, result) => {
|
|
80
|
+
if (err) return rejectFunc(err);
|
|
81
|
+
resolveFunc(result);
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
try {
|
|
86
|
+
const cacheKey = `friendslist_${ctx.userID}`;
|
|
87
|
+
const cached = FRIENDS_CACHE.get(cacheKey);
|
|
88
|
+
if (cached && (Date.now() - cached.ts < CACHE_TTL)) {
|
|
89
|
+
return callback(null, cached.data);
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
let result;
|
|
93
|
+
|
|
94
|
+
try {
|
|
95
|
+
const res = await retryOp(() =>
|
|
96
|
+
defaultFuncs.postFormData(
|
|
97
|
+
"https://www.facebook.com/chat/user_info_all",
|
|
98
|
+
ctx.jar, {},
|
|
99
|
+
{ viewer: ctx.userID }
|
|
100
|
+
).then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
|
101
|
+
);
|
|
102
|
+
if (!res || res.error) throw res || new Error("Empty response");
|
|
103
|
+
result = formatData(res.payload);
|
|
104
|
+
} catch (legacyErr) {
|
|
105
|
+
utils.warn("getFriendsList", "Legacy API failed, trying GraphQL:", legacyErr.message || legacyErr.error);
|
|
106
|
+
result = await retryOp(() => fetchViaGraphQL(defaultFuncs, ctx));
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
FRIENDS_CACHE.set(cacheKey, { data: result, ts: Date.now() });
|
|
110
|
+
callback(null, result);
|
|
111
|
+
} catch (err) {
|
|
112
|
+
utils.error("getFriendsList", err);
|
|
113
|
+
callback(err);
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return returnPromise;
|
|
117
|
+
};
|
|
118
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
4
|
+
const { formatAttachment: _formatAttachment } = require('../datastore/models/matrix/transform/index');
|
|
5
|
+
|
|
6
|
+
const MSG_CACHE = new Map();
|
|
7
|
+
const CACHE_TTL = 3 * 60 * 1000;
|
|
8
|
+
|
|
9
|
+
const THEME_COLORS = [
|
|
10
|
+
{ theme_color: "FF000000", theme_id: "788274591712841", theme_name_with_subtitle: "Monochrome" },
|
|
11
|
+
{ theme_color: "FF0084FF", theme_id: "196241301102133", theme_name_with_subtitle: "Default Blue" },
|
|
12
|
+
{ theme_color: "FFFF5CA1", theme_id: "169463077092846", theme_name_with_subtitle: "Hot Pink" },
|
|
13
|
+
{ theme_color: "FF2825B5", theme_id: "271607034185782", theme_name_with_subtitle: "Shadow" },
|
|
14
|
+
{ theme_color: "FF7646FF", theme_id: "234137870477637", theme_name_with_subtitle: "Bright Purple" },
|
|
15
|
+
{ theme_color: "FF44BEC7", theme_id: "1928399724138152", theme_name_with_subtitle: "Teal Blue" },
|
|
16
|
+
{ theme_color: "FFFE5F03", theme_id: "175615189761153", theme_name_with_subtitle: "Orange" },
|
|
17
|
+
{ theme_color: "FF13CF13", theme_id: "2136751179887052", theme_name_with_subtitle: "Green" },
|
|
18
|
+
{ theme_color: "FFFA3C4C", theme_id: "2129984390566328", theme_name_with_subtitle: "Red" },
|
|
19
|
+
{ theme_color: "FFF25C54", theme_id: "3022526817824329", theme_name_with_subtitle: "Peach" },
|
|
20
|
+
{ theme_color: "FFF01D6A", theme_id: "724096885023603", theme_name_with_subtitle: "Berry" },
|
|
21
|
+
{ theme_color: "FFFF7CA8", theme_id: "624266884847972", theme_name_with_subtitle: "Candy" }
|
|
22
|
+
];
|
|
23
|
+
|
|
24
|
+
async function retryOp(fn, retries = 3, base = 600) {
|
|
25
|
+
for (let i = 0; i < retries; i++) {
|
|
26
|
+
try { return await fn(); } catch (err) {
|
|
27
|
+
if (i === retries - 1) throw err;
|
|
28
|
+
await new Promise(r => setTimeout(r, base * Math.pow(2, i) + Math.random() * 250));
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
function formatMessage(threadID, data) {
|
|
34
|
+
const baseMessage = {
|
|
35
|
+
threadID,
|
|
36
|
+
messageID: data.message_id,
|
|
37
|
+
timestamp: data.timestamp_precise,
|
|
38
|
+
author: data.message_sender ? data.message_sender.id : null
|
|
39
|
+
};
|
|
40
|
+
|
|
41
|
+
switch (data.__typename) {
|
|
42
|
+
case "ThreadNameMessage":
|
|
43
|
+
return { ...baseMessage, type: "event", logMessageType: "log:thread-name", logMessageData: { name: data.thread_name }, logMessageBody: data.snippet };
|
|
44
|
+
|
|
45
|
+
case "ThreadImageMessage":
|
|
46
|
+
const meta = data.image_with_metadata;
|
|
47
|
+
return { ...baseMessage, type: "event", logMessageType: "log:thread-image", logMessageData: meta ? { attachmentID: meta.legacy_attachment_id, width: meta.original_dimensions?.x, height: meta.original_dimensions?.y, url: meta.preview?.uri } : null, logMessageBody: data.snippet };
|
|
48
|
+
|
|
49
|
+
case "GenericAdminTextMessage": {
|
|
50
|
+
const adminType = data.extensible_message_admin_text_type;
|
|
51
|
+
const adminText = data.extensible_message_admin_text;
|
|
52
|
+
|
|
53
|
+
const adminTypeMap = {
|
|
54
|
+
CHANGE_THREAD_THEME: () => {
|
|
55
|
+
const colorMatch = THEME_COLORS.find(c => c.theme_color === adminText?.theme_color);
|
|
56
|
+
return { logMessageType: "log:thread-color", logMessageData: colorMatch || { theme_color: adminText?.theme_color } };
|
|
57
|
+
},
|
|
58
|
+
CHANGE_THREAD_ICON: () => {
|
|
59
|
+
const icon = adminText?.thread_icon;
|
|
60
|
+
let iconUrl = null;
|
|
61
|
+
if (icon) {
|
|
62
|
+
try { iconUrl = `https://static.xx.fbcdn.net/images/emoji.php/v9/t3c/1/16/${icon.codePointAt(0).toString(16)}.png`; } catch {}
|
|
63
|
+
}
|
|
64
|
+
return { logMessageType: "log:thread-icon", logMessageData: { thread_icon: icon || null, thread_icon_url: iconUrl } };
|
|
65
|
+
},
|
|
66
|
+
CHANGE_THREAD_NICKNAME: () => ({ logMessageType: "log:user-nickname", logMessageData: { nickname: adminText?.nickname, participant_id: adminText?.participant_id } }),
|
|
67
|
+
GROUP_POLL: () => {
|
|
68
|
+
const q = adminText?.question;
|
|
69
|
+
if (!q) return { logMessageType: "log:thread-poll", logMessageData: { error: "Missing poll question data" } };
|
|
70
|
+
return { logMessageType: "log:thread-poll", logMessageData: { question_json: JSON.stringify({ id: q.id, text: q.text, options: (q.options?.nodes || []).map(o => ({ id: o.id, text: o.text, voters: (o.voters?.nodes || []).map(v => v.id) })) }), event_type: (adminText.event_type || "").toLowerCase(), question_id: q.id } };
|
|
71
|
+
},
|
|
72
|
+
CHANGE_THREAD_QUICK_REACTION: () => ({ logMessageType: "log:thread-icon", logMessageData: { thread_quick_reaction: adminText?.thread_quick_reaction } }),
|
|
73
|
+
CHANGE_THREAD_ADMINS: () => ({ logMessageType: "log:thread-admins", logMessageData: { admin_type: adminText?.admin_type, target_id: adminText?.target_id } }),
|
|
74
|
+
CHANGE_THREAD_APPROVAL_MODE: () => ({ logMessageType: "log:thread-approval-mode", logMessageData: { approval_mode: adminText?.approval_mode } }),
|
|
75
|
+
PIN_MESSAGES_V2: () => ({ logMessageType: "log:thread-pinned", logMessageData: { pinned_message_id: adminText?.pinned_message_id } }),
|
|
76
|
+
UNPIN_MESSAGES_V2: () => ({ logMessageType: "log:unpin-message", logMessageData: { unpinned_message_id: adminText?.unpinned_message_id } }),
|
|
77
|
+
JOINABLE_GROUP_LINK_MODE_CHANGE: () => ({ logMessageType: "log:link-status", logMessageData: { link_status: adminText?.joinable_mode } }),
|
|
78
|
+
MAGIC_WORDS: () => ({ logMessageType: "log:magic-words", logMessageData: { magic_word: adminText?.magic_word } }),
|
|
79
|
+
MESSENGER_CALL_LOG: () => ({ logMessageType: "log:thread-call", logMessageData: { event_type: adminType, call_duration: adminText?.call_duration || 0 } }),
|
|
80
|
+
PARTICIPANT_JOINED_GROUP_CALL: () => ({ logMessageType: "log:thread-call", logMessageData: { event_type: adminType, call_duration: adminText?.call_duration || 0 } })
|
|
81
|
+
};
|
|
82
|
+
|
|
83
|
+
const handler = adminTypeMap[adminType];
|
|
84
|
+
const extra = handler ? handler() : { logMessageType: "log:generic-admin", logMessageData: { admin_type: adminType } };
|
|
85
|
+
return { ...baseMessage, type: "event", ...extra, logMessageBody: data.snippet };
|
|
86
|
+
}
|
|
87
|
+
|
|
88
|
+
case "UserMessage": {
|
|
89
|
+
const attachments = [];
|
|
90
|
+
if (data.blob_attachments?.length) {
|
|
91
|
+
for (const att of data.blob_attachments) {
|
|
92
|
+
try { attachments.push(_formatAttachment(att)); } catch (ex) { attachments.push({ type: "unknown", error: ex.message, rawAttachment: att }); }
|
|
93
|
+
}
|
|
94
|
+
} else if (data.extensible_attachment && Object.keys(data.extensible_attachment).length) {
|
|
95
|
+
try {
|
|
96
|
+
attachments.push(_formatAttachment({ extensible_attachment: data.extensible_attachment }));
|
|
97
|
+
} catch (ex) {
|
|
98
|
+
const sa = data.extensible_attachment.story_attachment || {};
|
|
99
|
+
attachments.push({ type: "share", ID: data.extensible_attachment.legacy_attachment_id, url: sa.url, title: sa.title_with_entities?.text, description: sa.description?.text, source: sa.source?.text, image: sa.media?.image?.uri, playable: sa.media?.is_playable || false });
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const mentions = {};
|
|
104
|
+
if (data.message?.ranges) {
|
|
105
|
+
for (const mention of data.message.ranges) {
|
|
106
|
+
if (mention.entity?.id && data.message.text) {
|
|
107
|
+
mentions[mention.entity.id] = data.message.text.substring(mention.offset, mention.offset + mention.length);
|
|
108
|
+
}
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
return {
|
|
113
|
+
type: "message",
|
|
114
|
+
senderID: data.message_sender?.id || null,
|
|
115
|
+
body: data.message?.text || "",
|
|
116
|
+
threadID,
|
|
117
|
+
messageID: data.message_id,
|
|
118
|
+
reactions: (data.message_reactions || []).map(r => ({ [r.user.id]: r.reaction })),
|
|
119
|
+
attachments,
|
|
120
|
+
mentions,
|
|
121
|
+
timestamp: data.timestamp_precise,
|
|
122
|
+
isUnsent: !!data.is_unsent
|
|
123
|
+
};
|
|
124
|
+
}
|
|
125
|
+
|
|
126
|
+
default:
|
|
127
|
+
return { ...baseMessage, type: "unknown", data };
|
|
128
|
+
}
|
|
129
|
+
}
|
|
130
|
+
|
|
131
|
+
function parseDelta(threadID, delta) {
|
|
132
|
+
if (delta.replied_to_message) {
|
|
133
|
+
return { type: "message_reply", ...formatMessage(threadID, delta), messageReply: formatMessage(threadID, delta.replied_to_message.message) };
|
|
134
|
+
}
|
|
135
|
+
return formatMessage(threadID, delta);
|
|
136
|
+
}
|
|
137
|
+
|
|
138
|
+
module.exports = (defaultFuncs, api, ctx) => {
|
|
139
|
+
return function getMessage(threadID, messageID, callback) {
|
|
140
|
+
let resolveFunc, rejectFunc;
|
|
141
|
+
const returnPromise = new Promise((resolve, reject) => {
|
|
142
|
+
resolveFunc = resolve;
|
|
143
|
+
rejectFunc = reject;
|
|
144
|
+
});
|
|
145
|
+
|
|
146
|
+
if (typeof callback !== "function") {
|
|
147
|
+
callback = (err, info) => {
|
|
148
|
+
if (err) return rejectFunc(err);
|
|
149
|
+
resolveFunc(info);
|
|
150
|
+
};
|
|
151
|
+
}
|
|
152
|
+
|
|
153
|
+
if (!threadID || !messageID) {
|
|
154
|
+
callback({ error: "getMessage: threadID and messageID are required" });
|
|
155
|
+
return returnPromise;
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
const cacheKey = `msg_${threadID}_${messageID}`;
|
|
159
|
+
const cached = MSG_CACHE.get(cacheKey);
|
|
160
|
+
if (cached && (Date.now() - cached.ts < CACHE_TTL)) {
|
|
161
|
+
callback(null, cached.data);
|
|
162
|
+
return returnPromise;
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
const form = {
|
|
166
|
+
av: ctx.userID,
|
|
167
|
+
fb_dtsg: ctx.fb_dtsg,
|
|
168
|
+
queries: JSON.stringify({
|
|
169
|
+
o0: {
|
|
170
|
+
doc_id: "1768656253222505",
|
|
171
|
+
query_params: {
|
|
172
|
+
thread_and_message_id: { thread_id: String(threadID), message_id: String(messageID) }
|
|
173
|
+
}
|
|
174
|
+
}
|
|
175
|
+
})
|
|
176
|
+
};
|
|
177
|
+
|
|
178
|
+
retryOp(() =>
|
|
179
|
+
defaultFuncs.post("https://www.facebook.com/api/graphqlbatch/", ctx.jar, form)
|
|
180
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs))
|
|
181
|
+
).then(resData => {
|
|
182
|
+
if (!resData || !resData.length) throw { error: "getMessage: no response data" };
|
|
183
|
+
if (resData[resData.length - 1].error_results > 0) throw resData[0].o0.errors;
|
|
184
|
+
if (resData[resData.length - 1].successful_results === 0) throw { error: "getMessage: no successful results" };
|
|
185
|
+
|
|
186
|
+
const fetchData = resData[0].o0.data.message;
|
|
187
|
+
if (!fetchData) throw { error: "getMessage: message not found" };
|
|
188
|
+
|
|
189
|
+
const result = parseDelta(String(threadID), fetchData);
|
|
190
|
+
MSG_CACHE.set(cacheKey, { data: result, ts: Date.now() });
|
|
191
|
+
callback(null, result);
|
|
192
|
+
}).catch(err => {
|
|
193
|
+
utils.error("getMessage", err);
|
|
194
|
+
callback(err);
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
return returnPromise;
|
|
198
|
+
};
|
|
199
|
+
};
|
|
@@ -0,0 +1,199 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* getTheme — Advanced Thread Theme Catalogue Fetcher for Phantom SDK
|
|
5
|
+
*
|
|
6
|
+
* Features:
|
|
7
|
+
* - Fetches full catalogue of available Messenger themes for a thread
|
|
8
|
+
* - Parallel detailed metadata resolution for all themes via fetchThemeData
|
|
9
|
+
* - In-memory catalogue cache (8-minute TTL)
|
|
10
|
+
* - Rich normalisation: gradients, images, dark-mode flag, accessibility score
|
|
11
|
+
* - Search helpers: findByName(), findByColor(), findByID()
|
|
12
|
+
* - Full callback + Promise dual API
|
|
13
|
+
*/
|
|
14
|
+
|
|
15
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
16
|
+
|
|
17
|
+
const _catalogueCache = new Map();
|
|
18
|
+
const CACHE_TTL = 8 * 60 * 1000; // 8 minutes
|
|
19
|
+
|
|
20
|
+
function parseGradient(raw) {
|
|
21
|
+
if (!raw) return [];
|
|
22
|
+
if (Array.isArray(raw)) return raw;
|
|
23
|
+
if (typeof raw === 'string') { try { return JSON.parse(raw); } catch { return [raw]; } }
|
|
24
|
+
return [];
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
function hexToRgb(hex) {
|
|
28
|
+
if (!hex) return null;
|
|
29
|
+
const c = hex.replace(/^#/, '').replace(/^[Ff]{2}/, '');
|
|
30
|
+
if (c.length !== 6) return null;
|
|
31
|
+
return { r: parseInt(c.slice(0, 2), 16), g: parseInt(c.slice(2, 4), 16), b: parseInt(c.slice(4, 6), 16) };
|
|
32
|
+
}
|
|
33
|
+
function lum({ r, g, b }) {
|
|
34
|
+
const f = v => { v /= 255; return v <= 0.03928 ? v / 12.92 : Math.pow((v + 0.055) / 1.055, 2.4); };
|
|
35
|
+
return 0.2126 * f(r) + 0.7152 * f(g) + 0.0722 * f(b);
|
|
36
|
+
}
|
|
37
|
+
function isDark(hex) { const r = hexToRgb(hex); return r ? lum(r) < 0.179 : null; }
|
|
38
|
+
|
|
39
|
+
function normalizeThemeEntry(raw) {
|
|
40
|
+
if (!raw || !raw.id) return null;
|
|
41
|
+
const gradients = parseGradient(raw.gradient_colors || raw.gradientColors);
|
|
42
|
+
const bg = raw.background_asset?.image?.uri || raw.backgroundImage || null;
|
|
43
|
+
const primary = gradients[0] || raw.fallback_color || raw.fallbackColor || null;
|
|
44
|
+
|
|
45
|
+
return {
|
|
46
|
+
id: raw.id,
|
|
47
|
+
name: raw.accessibility_label || raw.name || '',
|
|
48
|
+
description: raw.description || null,
|
|
49
|
+
|
|
50
|
+
// Colors
|
|
51
|
+
primary_color: primary,
|
|
52
|
+
fallback_color: raw.fallback_color || raw.fallbackColor || null,
|
|
53
|
+
gradient_colors: gradients,
|
|
54
|
+
background_gradient_colors: parseGradient(raw.background_gradient_colors || raw.backgroundGradientColors),
|
|
55
|
+
inbound_message_gradient_colors: parseGradient(raw.inbound_message_gradient_colors || raw.inboundMessageGradientColors),
|
|
56
|
+
message_text_color: raw.message_text_color || raw.messageTextColor || null,
|
|
57
|
+
inbound_message_text_color: raw.inbound_message_text_color || raw.inboundMessageTextColor || null,
|
|
58
|
+
title_bar_text_color: raw.title_bar_text_color || raw.titleBarTextColor || null,
|
|
59
|
+
title_bar_button_tint_color: raw.title_bar_button_tint_color || raw.titleBarButtonTintColor || null,
|
|
60
|
+
title_bar_background_color: raw.title_bar_background_color || raw.titleBarBackgroundColor || null,
|
|
61
|
+
title_bar_attribution_color: raw.title_bar_attribution_color || raw.titleBarAttributionColor || null,
|
|
62
|
+
composer_background_color: raw.composer_background_color || raw.composerBackgroundColor || null,
|
|
63
|
+
composer_input_background_color: raw.composer_input_background_color || raw.composerInputBackgroundColor || null,
|
|
64
|
+
composer_tint_color: raw.composer_tint_color || raw.composerTintColor || null,
|
|
65
|
+
primary_button_background_color: raw.primary_button_background_color || raw.primaryButtonBackgroundColor || null,
|
|
66
|
+
reaction_pill_background_color: raw.reaction_pill_background_color || raw.reactionPillBackgroundColor || null,
|
|
67
|
+
secondary_text_color: raw.secondary_text_color || raw.secondaryTextColor || null,
|
|
68
|
+
tertiary_text_color: raw.tertiary_text_color || raw.tertiaryTextColor || null,
|
|
69
|
+
hot_like_color: raw.hot_like_color || raw.hotLikeColor || null,
|
|
70
|
+
app_color_mode: raw.app_color_mode || raw.appColorMode || null,
|
|
71
|
+
normal_theme_id: raw.normal_theme_id || raw.normalThemeId || null,
|
|
72
|
+
theme_idx: raw.theme_idx,
|
|
73
|
+
|
|
74
|
+
// Images
|
|
75
|
+
backgroundImage: bg,
|
|
76
|
+
iconImage: raw.icon_asset?.image?.uri || raw.iconAsset || null,
|
|
77
|
+
background_asset: raw.background_asset || null,
|
|
78
|
+
icon_asset: raw.icon_asset || null,
|
|
79
|
+
|
|
80
|
+
// Derived
|
|
81
|
+
is_dark: isDark(primary),
|
|
82
|
+
};
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
86
|
+
|
|
87
|
+
/**
|
|
88
|
+
* Fetch the full theme catalogue for a Messenger thread.
|
|
89
|
+
*
|
|
90
|
+
* @param {string} threadID Target thread ID
|
|
91
|
+
* @param {object} [options]
|
|
92
|
+
* @param {boolean} [options.resolveDetails=true] Fetch full metadata for each theme in parallel
|
|
93
|
+
* @param {boolean} [options.useCache=true] Use catalogue cache
|
|
94
|
+
* @param {string} [options.search] If set, return only themes matching this string
|
|
95
|
+
* @param {string} [options.searchColor] If set, return only themes whose gradient starts with this color
|
|
96
|
+
* @param {Function} [callback] Node-style (err, themes[]) callback
|
|
97
|
+
* @returns {Promise<Array>} Fully normalised theme catalogue
|
|
98
|
+
*/
|
|
99
|
+
return async function getTheme(threadID, options, callback) {
|
|
100
|
+
if (typeof options === 'function') { callback = options; options = {}; }
|
|
101
|
+
options = options || {};
|
|
102
|
+
|
|
103
|
+
if (!threadID) {
|
|
104
|
+
const err = new Error("getTheme: threadID is required");
|
|
105
|
+
if (callback) return callback(err);
|
|
106
|
+
throw err;
|
|
107
|
+
}
|
|
108
|
+
|
|
109
|
+
let resolveFunc, rejectFunc;
|
|
110
|
+
const promise = new Promise((res, rej) => { resolveFunc = res; rejectFunc = rej; });
|
|
111
|
+
|
|
112
|
+
function done(err, data) {
|
|
113
|
+
if (callback) return err ? callback(err) : callback(null, data);
|
|
114
|
+
if (err) rejectFunc(err); else resolveFunc(data);
|
|
115
|
+
}
|
|
116
|
+
|
|
117
|
+
// Cache
|
|
118
|
+
const cacheKey = `catalogue::${threadID}`;
|
|
119
|
+
if (options.useCache !== false) {
|
|
120
|
+
const hit = _catalogueCache.get(cacheKey);
|
|
121
|
+
if (hit && Date.now() - hit.ts < CACHE_TTL) {
|
|
122
|
+
utils.log("getTheme", `Cache hit for thread ${threadID}`);
|
|
123
|
+
const data = applyFilters(hit.data, options);
|
|
124
|
+
done(null, data);
|
|
125
|
+
return promise;
|
|
126
|
+
}
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
const form = {
|
|
130
|
+
av: ctx.userID,
|
|
131
|
+
__user: ctx.userID,
|
|
132
|
+
fb_dtsg: ctx.fb_dtsg,
|
|
133
|
+
lsd: ctx.lsd || ctx.fb_dtsg,
|
|
134
|
+
fb_api_caller_class: 'RelayModern',
|
|
135
|
+
fb_api_req_friendly_name: 'MWPThreadThemeQuery_AllThemesQuery',
|
|
136
|
+
variables: JSON.stringify({ version: "default" }),
|
|
137
|
+
server_timestamps: true,
|
|
138
|
+
doc_id: '24474714052117636',
|
|
139
|
+
};
|
|
140
|
+
|
|
141
|
+
try {
|
|
142
|
+
const resData = await defaultFuncs
|
|
143
|
+
.post("https://www.facebook.com/api/graphql/", ctx.jar, form, null, {
|
|
144
|
+
"x-fb-friendly-name": "MWPThreadThemeQuery_AllThemesQuery",
|
|
145
|
+
"x-fb-lsd": ctx.lsd,
|
|
146
|
+
"referer": `https://www.facebook.com/messages/t/${threadID}`,
|
|
147
|
+
})
|
|
148
|
+
.then(utils.parseAndCheckLogin(ctx, defaultFuncs));
|
|
149
|
+
|
|
150
|
+
if (resData.errors) throw new Error(JSON.stringify(resData.errors));
|
|
151
|
+
if (!resData.data?.messenger_thread_themes) throw new Error("getTheme: No theme data in response");
|
|
152
|
+
|
|
153
|
+
const rawList = resData.data.messenger_thread_themes;
|
|
154
|
+
const baseThemes = rawList.map(normalizeThemeEntry).filter(Boolean);
|
|
155
|
+
|
|
156
|
+
let themes = baseThemes;
|
|
157
|
+
|
|
158
|
+
// Parallel detail resolution
|
|
159
|
+
if (options.resolveDetails !== false && api.fetchThemeData) {
|
|
160
|
+
themes = await Promise.all(
|
|
161
|
+
baseThemes.map(async base => {
|
|
162
|
+
try {
|
|
163
|
+
const detail = await api.fetchThemeData(base.id, { useCache: true });
|
|
164
|
+
return { ...base, ...detail };
|
|
165
|
+
} catch {
|
|
166
|
+
return base;
|
|
167
|
+
}
|
|
168
|
+
})
|
|
169
|
+
);
|
|
170
|
+
}
|
|
171
|
+
|
|
172
|
+
// Cache
|
|
173
|
+
if (options.useCache !== false) {
|
|
174
|
+
_catalogueCache.set(cacheKey, { data: themes, ts: Date.now() });
|
|
175
|
+
}
|
|
176
|
+
|
|
177
|
+
utils.log("getTheme", `Loaded ${themes.length} theme(s) for thread ${threadID}`);
|
|
178
|
+
done(null, applyFilters(themes, options));
|
|
179
|
+
} catch (err) {
|
|
180
|
+
utils.error("getTheme", err.message || err);
|
|
181
|
+
done(err);
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
return promise;
|
|
185
|
+
};
|
|
186
|
+
|
|
187
|
+
function applyFilters(themes, options) {
|
|
188
|
+
let out = themes;
|
|
189
|
+
if (options.search) {
|
|
190
|
+
const q = options.search.toLowerCase();
|
|
191
|
+
out = out.filter(t => (t.name || '').toLowerCase().includes(q) || (t.description || '').toLowerCase().includes(q));
|
|
192
|
+
}
|
|
193
|
+
if (options.searchColor) {
|
|
194
|
+
const col = options.searchColor.toLowerCase().replace(/^#/, '');
|
|
195
|
+
out = out.filter(t => (t.gradient_colors || []).some(c => c.toLowerCase().includes(col)));
|
|
196
|
+
}
|
|
197
|
+
return out;
|
|
198
|
+
}
|
|
199
|
+
};
|
|
@@ -0,0 +1,160 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* getThemeInfo — Advanced Thread / Theme Info Fetcher for Phantom SDK
|
|
5
|
+
*
|
|
6
|
+
* Resolves either a theme ID or a thread ID:
|
|
7
|
+
* - Theme ID → calls fetchThemeData for full theme metadata
|
|
8
|
+
* - Thread ID → calls getThreadInfo + optionally getTheme for active theme details
|
|
9
|
+
*
|
|
10
|
+
* Features:
|
|
11
|
+
* - Smart ID detection (theme vs thread)
|
|
12
|
+
* - Combined thread + active-theme metadata
|
|
13
|
+
* - In-memory cache (6-minute TTL)
|
|
14
|
+
* - Graceful degradation with sensible defaults on partial failures
|
|
15
|
+
* - Full callback + Promise dual API
|
|
16
|
+
*/
|
|
17
|
+
|
|
18
|
+
const utils = require('../datastore/models/matrix/tools');
|
|
19
|
+
|
|
20
|
+
const _cache = new Map();
|
|
21
|
+
const CACHE_TTL = 6 * 60 * 1000; // 6 minutes
|
|
22
|
+
|
|
23
|
+
const THEME_ID_RE = /^\d{10,}$/;
|
|
24
|
+
const THREAD_ID_RE = /^\d{7,}$/;
|
|
25
|
+
|
|
26
|
+
function defaultThemeInfo(identifier, extras) {
|
|
27
|
+
return Object.assign({
|
|
28
|
+
threadID: identifier,
|
|
29
|
+
threadName: '',
|
|
30
|
+
participantCount: 0,
|
|
31
|
+
isGroup: null,
|
|
32
|
+
color: null,
|
|
33
|
+
emoji: '👍',
|
|
34
|
+
theme_id: null,
|
|
35
|
+
theme_color: null,
|
|
36
|
+
gradient_colors: null,
|
|
37
|
+
backgroundImage: null,
|
|
38
|
+
is_dark: null,
|
|
39
|
+
is_default: true,
|
|
40
|
+
}, extras || {});
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
module.exports = function (defaultFuncs, api, ctx) {
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Get combined theme + thread info for a thread ID, or full theme data for a theme ID.
|
|
47
|
+
*
|
|
48
|
+
* @param {string} identifier Thread ID or Theme ID
|
|
49
|
+
* @param {object} [options]
|
|
50
|
+
* @param {boolean} [options.useCache=true] Use 6-minute cache
|
|
51
|
+
* @param {boolean} [options.fetchActiveTheme=true] Also resolve active theme details when threadID is given
|
|
52
|
+
* @param {Function} [callback] Node-style (err, info) callback
|
|
53
|
+
* @returns {Promise<object>} Resolved info object
|
|
54
|
+
*/
|
|
55
|
+
return async function getThemeInfo(identifier, options, callback) {
|
|
56
|
+
if (typeof options === 'function') { callback = options; options = {}; }
|
|
57
|
+
options = options || {};
|
|
58
|
+
|
|
59
|
+
if (!identifier) {
|
|
60
|
+
const err = new Error("getThemeInfo: identifier is required (threadID or themeID)");
|
|
61
|
+
if (callback) return callback(err);
|
|
62
|
+
throw err;
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
let resolveFunc, rejectFunc;
|
|
66
|
+
const promise = new Promise((res, rej) => { resolveFunc = res; rejectFunc = rej; });
|
|
67
|
+
|
|
68
|
+
function done(err, data) {
|
|
69
|
+
if (callback) return err ? callback(err) : callback(null, data);
|
|
70
|
+
if (err) rejectFunc(err); else resolveFunc(data);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
const id = identifier.toString().trim();
|
|
74
|
+
const cacheKey = `themeinfo::${id}`;
|
|
75
|
+
|
|
76
|
+
if (options.useCache !== false) {
|
|
77
|
+
const hit = _cache.get(cacheKey);
|
|
78
|
+
if (hit && Date.now() - hit.ts < CACHE_TTL) {
|
|
79
|
+
done(null, hit.data);
|
|
80
|
+
return promise;
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
// ── Detect if this is a pure theme ID ───────────────────────────────────
|
|
85
|
+
if (THEME_ID_RE.test(id) && api.fetchThemeData) {
|
|
86
|
+
try {
|
|
87
|
+
const themeData = await api.fetchThemeData(id, { useCache: options.useCache !== false });
|
|
88
|
+
const result = Object.assign({ _type: 'theme' }, themeData);
|
|
89
|
+
if (options.useCache !== false) _cache.set(cacheKey, { data: result, ts: Date.now() });
|
|
90
|
+
done(null, result);
|
|
91
|
+
} catch (themeErr) {
|
|
92
|
+
// Could still be a thread ID — fall through
|
|
93
|
+
utils.warn("getThemeInfo", `Theme fetch failed for ${id}: ${themeErr.message} — trying as thread`);
|
|
94
|
+
await resolveAsThread(id, options, done, cacheKey);
|
|
95
|
+
}
|
|
96
|
+
return promise;
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
// ── Treat as thread ID ───────────────────────────────────────────────────
|
|
100
|
+
await resolveAsThread(id, options, done, cacheKey);
|
|
101
|
+
return promise;
|
|
102
|
+
|
|
103
|
+
async function resolveAsThread(tid, opts, finish, ck) {
|
|
104
|
+
let threadInfo = null;
|
|
105
|
+
try {
|
|
106
|
+
threadInfo = await api.getThreadInfo(tid);
|
|
107
|
+
} catch (tiErr) {
|
|
108
|
+
utils.warn("getThemeInfo", `getThreadInfo failed: ${tiErr.message}`);
|
|
109
|
+
const fallback = defaultThemeInfo(tid, { error: tiErr.message || 'Could not retrieve thread info' });
|
|
110
|
+
if (opts.useCache !== false) _cache.set(ck, { data: fallback, ts: Date.now() });
|
|
111
|
+
finish(null, fallback);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (!threadInfo) {
|
|
116
|
+
const fallback = defaultThemeInfo(tid);
|
|
117
|
+
if (opts.useCache !== false) _cache.set(ck, { data: fallback, ts: Date.now() });
|
|
118
|
+
finish(null, fallback);
|
|
119
|
+
return;
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
const info = Array.isArray(threadInfo) ? threadInfo[0] : threadInfo;
|
|
123
|
+
|
|
124
|
+
const base = {
|
|
125
|
+
_type: 'thread',
|
|
126
|
+
threadID: tid,
|
|
127
|
+
threadName: info.threadName || info.name || '',
|
|
128
|
+
participantCount: info.participantIDs?.length || 0,
|
|
129
|
+
isGroup: info.isGroup || false,
|
|
130
|
+
color: info.color || null,
|
|
131
|
+
emoji: info.emoji || '👍',
|
|
132
|
+
theme_id: info.theme_id || info.themeID || null,
|
|
133
|
+
theme_color: info.theme_color || info.color || null,
|
|
134
|
+
gradient_colors: info.gradient_colors || null,
|
|
135
|
+
backgroundImage: info.backgroundImage || null,
|
|
136
|
+
is_dark: null,
|
|
137
|
+
is_default: !info.color && !info.theme_id,
|
|
138
|
+
muteUntil: info.muteUntil || null,
|
|
139
|
+
unreadCount: info.unreadCount || 0,
|
|
140
|
+
};
|
|
141
|
+
|
|
142
|
+
// Try to resolve the active theme's full details
|
|
143
|
+
if (opts.fetchActiveTheme !== false && base.theme_id && api.fetchThemeData) {
|
|
144
|
+
try {
|
|
145
|
+
const activeTheme = await api.fetchThemeData(base.theme_id, { useCache: true });
|
|
146
|
+
Object.assign(base, {
|
|
147
|
+
active_theme: activeTheme,
|
|
148
|
+
gradient_colors: activeTheme.gradient_colors || base.gradient_colors,
|
|
149
|
+
backgroundImage: activeTheme.backgroundImage || base.backgroundImage,
|
|
150
|
+
is_dark: activeTheme.is_dark,
|
|
151
|
+
theme_color: activeTheme.primary_color || base.theme_color,
|
|
152
|
+
});
|
|
153
|
+
} catch (_) {}
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
if (opts.useCache !== false) _cache.set(ck, { data: base, ts: Date.now() });
|
|
157
|
+
finish(null, base);
|
|
158
|
+
}
|
|
159
|
+
};
|
|
160
|
+
};
|