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,114 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const { CookieJar, Cookie } = require("tough-cookie");
|
|
4
|
+
|
|
5
|
+
const FB_DOMAINS = ["https://www.facebook.com", "https://www.messenger.com"];
|
|
6
|
+
|
|
7
|
+
function normalizeCookieHeaderString(raw) {
|
|
8
|
+
if (!raw || typeof raw !== "string") return [];
|
|
9
|
+
return raw
|
|
10
|
+
.split(/\s*;\s*/)
|
|
11
|
+
.filter(p => {
|
|
12
|
+
const eq = p.indexOf("=");
|
|
13
|
+
const key = eq !== -1 ? p.slice(0, eq).trim() : p.trim();
|
|
14
|
+
return key && eq !== -1;
|
|
15
|
+
});
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
function setJarFromPairs(jar, pairs, domains = FB_DOMAINS) {
|
|
19
|
+
if (!pairs) return;
|
|
20
|
+
const list = Array.isArray(pairs) ? pairs : [pairs];
|
|
21
|
+
for (const pair of list) {
|
|
22
|
+
const raw = typeof pair === "string" ? pair.trim() : null;
|
|
23
|
+
if (!raw) continue;
|
|
24
|
+
const eq = raw.indexOf("=");
|
|
25
|
+
if (eq === -1) continue;
|
|
26
|
+
const name = raw.slice(0, eq).trim();
|
|
27
|
+
const value = raw.slice(eq + 1).trim();
|
|
28
|
+
if (!name) continue;
|
|
29
|
+
for (const domain of domains) {
|
|
30
|
+
try {
|
|
31
|
+
const cookie = new Cookie({
|
|
32
|
+
key: name,
|
|
33
|
+
value,
|
|
34
|
+
domain: "." + new URL(domain).hostname,
|
|
35
|
+
path: "/",
|
|
36
|
+
secure: true,
|
|
37
|
+
httpOnly: false,
|
|
38
|
+
});
|
|
39
|
+
jar.setCookieSync(cookie, domain);
|
|
40
|
+
} catch (_) {}
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
function fromStatePairs(statePairs) {
|
|
46
|
+
const jar = new CookieJar();
|
|
47
|
+
if (!Array.isArray(statePairs)) return jar;
|
|
48
|
+
for (const c of statePairs) {
|
|
49
|
+
if (!c || typeof c !== "object") continue;
|
|
50
|
+
try {
|
|
51
|
+
const domain = c.domain || ".facebook.com";
|
|
52
|
+
const domainUrl = `https://${domain.replace(/^\./, "")}`;
|
|
53
|
+
const cookie = new Cookie({
|
|
54
|
+
key: c.key || c.name || "",
|
|
55
|
+
value: String(c.value ?? ""),
|
|
56
|
+
domain,
|
|
57
|
+
path: c.path || "/",
|
|
58
|
+
secure: c.secure ?? true,
|
|
59
|
+
httpOnly: c.httpOnly ?? false,
|
|
60
|
+
expires: c.expires ? new Date(c.expires) : undefined,
|
|
61
|
+
});
|
|
62
|
+
jar.setCookieSync(cookie, domainUrl);
|
|
63
|
+
} catch (_) {}
|
|
64
|
+
}
|
|
65
|
+
return jar;
|
|
66
|
+
}
|
|
67
|
+
|
|
68
|
+
function jarToArray(jar, domains = FB_DOMAINS) {
|
|
69
|
+
const seen = new Set();
|
|
70
|
+
const out = [];
|
|
71
|
+
for (const domain of domains) {
|
|
72
|
+
const cookies = jar.getCookiesSync(domain);
|
|
73
|
+
for (const c of cookies) {
|
|
74
|
+
if (seen.has(c.key)) continue;
|
|
75
|
+
seen.add(c.key);
|
|
76
|
+
out.push({
|
|
77
|
+
key: c.key,
|
|
78
|
+
value: c.value,
|
|
79
|
+
domain: c.domain,
|
|
80
|
+
path: c.path,
|
|
81
|
+
secure: c.secure,
|
|
82
|
+
httpOnly: c.httpOnly,
|
|
83
|
+
expires: c.expires,
|
|
84
|
+
});
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
return out;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
function mergeCookieArrays(...arrays) {
|
|
91
|
+
const map = new Map();
|
|
92
|
+
for (const arr of arrays) {
|
|
93
|
+
if (!Array.isArray(arr)) continue;
|
|
94
|
+
for (const c of arr) {
|
|
95
|
+
const k = c.key || c.name;
|
|
96
|
+
if (k) map.set(k, c);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
return [...map.values()];
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
function hasCriticalCookies(cookieArray) {
|
|
103
|
+
const names = (cookieArray || []).map(c => c.key || c.name).filter(Boolean);
|
|
104
|
+
return names.includes("c_user") && names.includes("xs");
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
module.exports = {
|
|
108
|
+
normalizeCookieHeaderString,
|
|
109
|
+
setJarFromPairs,
|
|
110
|
+
fromStatePairs,
|
|
111
|
+
jarToArray,
|
|
112
|
+
mergeCookieArrays,
|
|
113
|
+
hasCriticalCookies,
|
|
114
|
+
};
|
|
@@ -0,0 +1,203 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const cookieParser = require("./cookies");
|
|
4
|
+
const { getType, padZeros, NUM_TO_MONTH, NUM_TO_DAY, isReadableStream } = require("../logger");
|
|
5
|
+
const url = require("url");
|
|
6
|
+
const qs = require("querystring");
|
|
7
|
+
const stream = require("stream");
|
|
8
|
+
|
|
9
|
+
function getExtension(ext, fileName = "") {
|
|
10
|
+
if (ext) return ext;
|
|
11
|
+
const parts = fileName.split(".");
|
|
12
|
+
return parts.length > 1 ? parts.pop().toLowerCase() : "";
|
|
13
|
+
}
|
|
14
|
+
|
|
15
|
+
const ATTACH_HANDLERS = {
|
|
16
|
+
StickerAttachment: (blob) => ({
|
|
17
|
+
type: "sticker",
|
|
18
|
+
ID: blob.id,
|
|
19
|
+
url: blob.thread_image?.uri || blob.url,
|
|
20
|
+
packID: blob.pack?.id,
|
|
21
|
+
width: blob.frame_size?.width,
|
|
22
|
+
height: blob.frame_size?.height,
|
|
23
|
+
frameCount: blob.frame_count,
|
|
24
|
+
frameRate: blob.frame_rate,
|
|
25
|
+
framesPerRow: blob.frames_per_row,
|
|
26
|
+
framesPerCol: blob.frames_per_column,
|
|
27
|
+
label: blob.label,
|
|
28
|
+
}),
|
|
29
|
+
MessageImage: (blob, a2) => ({
|
|
30
|
+
type: "photo",
|
|
31
|
+
ID: blob.legacy_attachment_id || blob.id || a2?.id || "",
|
|
32
|
+
filename: blob.filename || (blob.legacy_attachment_id + ".jpg"),
|
|
33
|
+
fileSize: Number(blob.large_image?.size || blob.preview?.size || 0),
|
|
34
|
+
width: blob.large_image?.width || blob.preview?.width || 0,
|
|
35
|
+
height: blob.large_image?.height || blob.preview?.height || 0,
|
|
36
|
+
url: blob.large_image?.uri || blob.preview?.uri || null,
|
|
37
|
+
thumbnailUrl: blob.thumbnail?.uri || null,
|
|
38
|
+
previewUrl: blob.preview?.uri || null,
|
|
39
|
+
largePreviewUrl:blob.large_preview?.uri || null,
|
|
40
|
+
mimeType: blob.original_extension ? `image/${blob.original_extension}` : "image/jpeg",
|
|
41
|
+
}),
|
|
42
|
+
MessageVideo: (blob, a2) => ({
|
|
43
|
+
type: "video",
|
|
44
|
+
ID: blob.legacy_attachment_id || blob.id || a2?.id || "",
|
|
45
|
+
filename: blob.filename || "video.mp4",
|
|
46
|
+
fileSize: Number(blob.attachment_video_data?.video_file_size || 0),
|
|
47
|
+
duration: blob.attachment_video_data?.video_length || 0,
|
|
48
|
+
url: blob.attachment_video_data?.video_hd_url ||
|
|
49
|
+
blob.attachment_video_data?.video_sd_url || null,
|
|
50
|
+
thumbnailUrl: blob.attachment_video_data?.video_image?.uri || null,
|
|
51
|
+
mimeType: "video/mp4",
|
|
52
|
+
}),
|
|
53
|
+
MessageFile: (blob, a2) => ({
|
|
54
|
+
type: "file",
|
|
55
|
+
ID: blob.legacy_attachment_id || blob.id || a2?.id || "",
|
|
56
|
+
filename: blob.filename || "file",
|
|
57
|
+
fileSize: Number(blob.filesize || 0),
|
|
58
|
+
url: blob.url || null,
|
|
59
|
+
mimeType: blob.content_type || "application/octet-stream",
|
|
60
|
+
}),
|
|
61
|
+
MessageAudio: (blob, a2) => ({
|
|
62
|
+
type: "audio",
|
|
63
|
+
ID: blob.legacy_attachment_id || blob.id || a2?.id || "",
|
|
64
|
+
filename: blob.filename || "audio",
|
|
65
|
+
duration: Number(blob.playable_duration_in_ms || 0),
|
|
66
|
+
url: blob.playable_url || null,
|
|
67
|
+
mimeType: blob.content_type || "audio/mpeg",
|
|
68
|
+
isVoiceMail: !!blob.is_voicemail,
|
|
69
|
+
}),
|
|
70
|
+
ExternalUrl: (blob) => ({
|
|
71
|
+
type: "share",
|
|
72
|
+
ID: blob.url,
|
|
73
|
+
url: blob.url,
|
|
74
|
+
title: blob.title || "",
|
|
75
|
+
description: blob.description?.text || "",
|
|
76
|
+
source: blob.source,
|
|
77
|
+
favicon: blob.icon?.uri || null,
|
|
78
|
+
}),
|
|
79
|
+
Photo: (blob, a2) => ({
|
|
80
|
+
type: "photo",
|
|
81
|
+
ID: blob.legacy_attachment_id || blob.id || a2?.id || "",
|
|
82
|
+
url: blob.large_image?.uri || blob.preview?.uri || null,
|
|
83
|
+
mimeType: "image/jpeg",
|
|
84
|
+
}),
|
|
85
|
+
};
|
|
86
|
+
|
|
87
|
+
function formatAttachment(a1, a2) {
|
|
88
|
+
const blob = a1.mercury || a1.blob_attachment || a1.sticker_attachment;
|
|
89
|
+
const tp = blob?.__typename || a1.attach_type;
|
|
90
|
+
|
|
91
|
+
if (!tp && a1.id != null && !a1.extensible_attachment) {
|
|
92
|
+
return { type: "share", ID: a1.id, url: a1.href || null, title: "Shared Content", isUnrecognized: true };
|
|
93
|
+
}
|
|
94
|
+
|
|
95
|
+
if (!a1.attach_type && a1.imageMetadata) {
|
|
96
|
+
return {
|
|
97
|
+
type: "photo",
|
|
98
|
+
ID: a1.fbid,
|
|
99
|
+
filename: a1.filename,
|
|
100
|
+
fileSize: Number(a1.fileSize || 0),
|
|
101
|
+
mimeType: a1.mimeType,
|
|
102
|
+
width: a1.imageMetadata.width,
|
|
103
|
+
height: a1.imageMetadata.height,
|
|
104
|
+
};
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (ATTACH_HANDLERS[tp]) return ATTACH_HANDLERS[tp](blob || a1, a2);
|
|
108
|
+
|
|
109
|
+
if (a1.extensible_attachment?.story_attachment?.target?.__typename === "ExternalUrl") {
|
|
110
|
+
const ext = a1.extensible_attachment.story_attachment;
|
|
111
|
+
return {
|
|
112
|
+
type: "share",
|
|
113
|
+
ID: ext.url_share_data?.share_id || "",
|
|
114
|
+
url: ext.url_share_data?.share_url || ext.target?.url || "",
|
|
115
|
+
title: ext.title_with_entities?.text || "",
|
|
116
|
+
description: ext.description?.text || "",
|
|
117
|
+
source: ext.source?.text || null,
|
|
118
|
+
media: ext.media?.image?.uri || null,
|
|
119
|
+
};
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
return { type: tp || "unknown", ID: blob?.id || a1.id || "", raw: a1 };
|
|
123
|
+
}
|
|
124
|
+
|
|
125
|
+
function formatDeltaMessage(d) {
|
|
126
|
+
const msg = d.message;
|
|
127
|
+
const body = msg?.text || "";
|
|
128
|
+
|
|
129
|
+
const rawAtts = [];
|
|
130
|
+
if (msg?.sticker_attachment) rawAtts.push(msg.sticker_attachment);
|
|
131
|
+
if (Array.isArray(msg?.blob_attachments)) rawAtts.push(...msg.blob_attachments);
|
|
132
|
+
if (Array.isArray(d.attachments)) rawAtts.push(...d.attachments);
|
|
133
|
+
const atts = rawAtts.filter(Boolean).map(a => formatAttachment(a));
|
|
134
|
+
|
|
135
|
+
const participants = d.participants || [];
|
|
136
|
+
const senderID = participants.find(p => p !== d.threadKey?.otherUserFbId)
|
|
137
|
+
|| d.actorFbId || d.actor_fbid || "";
|
|
138
|
+
const threadID = d.threadKey?.threadFbId || d.threadKey?.otherUserFbId || "";
|
|
139
|
+
const messageID = d.messageId || d.message_id || "";
|
|
140
|
+
|
|
141
|
+
const mentions = {};
|
|
142
|
+
if (msg?.text && Array.isArray(d.mentionOffsets)) {
|
|
143
|
+
d.mentionOffsets.forEach((offset, i) => {
|
|
144
|
+
const len = d.mentionLengths?.[i];
|
|
145
|
+
const uid = d.mentionUIDs?.[i];
|
|
146
|
+
if (uid && len != null) mentions[uid] = msg.text.substr(offset, len);
|
|
147
|
+
});
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
type: "message",
|
|
152
|
+
senderID: String(senderID),
|
|
153
|
+
body,
|
|
154
|
+
threadID: String(threadID),
|
|
155
|
+
messageID: String(messageID),
|
|
156
|
+
attachments: atts,
|
|
157
|
+
mentions,
|
|
158
|
+
isGroup: !!d.threadKey?.threadFbId,
|
|
159
|
+
isUnread: false,
|
|
160
|
+
isRead: false,
|
|
161
|
+
timestamp: msg?.timestamp_precise ? Number(msg.timestamp_precise) : Date.now(),
|
|
162
|
+
};
|
|
163
|
+
}
|
|
164
|
+
|
|
165
|
+
function formatID(n) { return String(n || ""); }
|
|
166
|
+
|
|
167
|
+
function formatDuration(ms) {
|
|
168
|
+
ms = Number(ms);
|
|
169
|
+
if (!isFinite(ms) || ms < 0) return "0s";
|
|
170
|
+
const s = Math.floor(ms / 1000);
|
|
171
|
+
const m = Math.floor(s / 60);
|
|
172
|
+
const h = Math.floor(m / 60);
|
|
173
|
+
const d = Math.floor(h / 24);
|
|
174
|
+
if (d > 0) return `${d}d ${h % 24}h`;
|
|
175
|
+
if (h > 0) return `${h}h ${m % 60}m`;
|
|
176
|
+
if (m > 0) return `${m}m ${s % 60}s`;
|
|
177
|
+
return `${s}s`;
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
function formatFileSize(bytes) {
|
|
181
|
+
if (bytes < 1024) return `${bytes}B`;
|
|
182
|
+
if (bytes < 1024 ** 2) return `${(bytes / 1024).toFixed(1)}KB`;
|
|
183
|
+
if (bytes < 1024 ** 3) return `${(bytes / 1024 ** 2).toFixed(1)}MB`;
|
|
184
|
+
return `${(bytes / 1024 ** 3).toFixed(2)}GB`;
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
function formatTimestamp(ms) {
|
|
188
|
+
const d = new Date(ms || Date.now());
|
|
189
|
+
const pad = (n) => String(n).padStart(2, "0");
|
|
190
|
+
return `${d.getFullYear()}-${pad(d.getMonth() + 1)}-${pad(d.getDate())} ` +
|
|
191
|
+
`${pad(d.getHours())}:${pad(d.getMinutes())}:${pad(d.getSeconds())}`;
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
module.exports = {
|
|
195
|
+
...cookieParser,
|
|
196
|
+
getExtension,
|
|
197
|
+
formatAttachment,
|
|
198
|
+
formatDeltaMessage,
|
|
199
|
+
formatID,
|
|
200
|
+
formatDuration,
|
|
201
|
+
formatFileSize,
|
|
202
|
+
formatTimestamp,
|
|
203
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const RULES = {
|
|
4
|
+
required: (v) => v != null && v !== "",
|
|
5
|
+
string: (v) => typeof v === "string",
|
|
6
|
+
number: (v) => typeof v === "number" && isFinite(v),
|
|
7
|
+
boolean: (v) => typeof v === "boolean",
|
|
8
|
+
array: (v) => Array.isArray(v),
|
|
9
|
+
object: (v) => v !== null && typeof v === "object" && !Array.isArray(v),
|
|
10
|
+
positive: (v) => typeof v === "number" && v > 0,
|
|
11
|
+
nonEmpty: (v) => (typeof v === "string" && v.trim().length > 0) || (Array.isArray(v) && v.length > 0),
|
|
12
|
+
url: (v) => { try { new URL(String(v)); return true; } catch (_) { return false; } },
|
|
13
|
+
email: (v) => /^[^\s@]+@[^\s@]+\.[^\s@]{2,}$/.test(String(v || "")),
|
|
14
|
+
numericID: (v) => !!v && /^\d{1,20}$/.test(String(v).trim()),
|
|
15
|
+
};
|
|
16
|
+
|
|
17
|
+
class ValidationError extends Error {
|
|
18
|
+
constructor(field, rule, value, message) {
|
|
19
|
+
super(message || `Validation failed for '${field}' (rule: ${rule})`);
|
|
20
|
+
this.name = "ValidationError";
|
|
21
|
+
this.field = field;
|
|
22
|
+
this.rule = rule;
|
|
23
|
+
this.value = value;
|
|
24
|
+
}
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
class Validator {
|
|
28
|
+
constructor() {
|
|
29
|
+
this.maxMessageLength = 20_000;
|
|
30
|
+
this.maxThreadName = 255;
|
|
31
|
+
this.maxNickname = 50;
|
|
32
|
+
this.maxReactionLen = 8;
|
|
33
|
+
this._customRules = new Map();
|
|
34
|
+
}
|
|
35
|
+
|
|
36
|
+
addRule(name, fn) {
|
|
37
|
+
if (typeof fn !== "function") throw new TypeError("Rule must be a function");
|
|
38
|
+
this._customRules.set(name, fn);
|
|
39
|
+
return this;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
check(value, ruleName, field = "value") {
|
|
43
|
+
const fn = this._customRules.get(ruleName) || RULES[ruleName];
|
|
44
|
+
if (!fn) throw new Error(`Unknown rule: ${ruleName}`);
|
|
45
|
+
return fn(value);
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
validate(data, schema) {
|
|
49
|
+
const errors = [];
|
|
50
|
+
for (const [field, rules] of Object.entries(schema)) {
|
|
51
|
+
const value = data?.[field];
|
|
52
|
+
for (const rule of (Array.isArray(rules) ? rules : [rules])) {
|
|
53
|
+
const name = typeof rule === "string" ? rule : rule.rule;
|
|
54
|
+
const fn = this._customRules.get(name) || RULES[name];
|
|
55
|
+
if (!fn) continue;
|
|
56
|
+
const opts = typeof rule === "object" ? rule : {};
|
|
57
|
+
if (!fn(value, opts)) {
|
|
58
|
+
errors.push(new ValidationError(field, name, value, opts.message));
|
|
59
|
+
break;
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
}
|
|
63
|
+
return { valid: errors.length === 0, errors };
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
isValidID(id) { return !!id && /^\d{1,20}$/.test(String(id).trim()); }
|
|
67
|
+
isValidThreadID(id) { return this.isValidID(id); }
|
|
68
|
+
isValidUserID(id) { return this.isValidID(id); }
|
|
69
|
+
isValidGroupID(id) { return this.isValidID(id); }
|
|
70
|
+
isValidMessageID(id){ return typeof id === "string" && id.length > 0 && id.length <= 50; }
|
|
71
|
+
|
|
72
|
+
isValidMessage(msg) {
|
|
73
|
+
if (typeof msg === "string") return msg.length > 0 && msg.length <= this.maxMessageLength;
|
|
74
|
+
if (msg && typeof msg === "object") {
|
|
75
|
+
return !!(msg.body || msg.sticker || msg.attachment || msg.emoji ||
|
|
76
|
+
msg.url || msg.mentions || msg.location);
|
|
77
|
+
}
|
|
78
|
+
return false;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
sanitizeMessage(msg) {
|
|
82
|
+
if (typeof msg !== "string") return msg;
|
|
83
|
+
return msg
|
|
84
|
+
.replace(/[\x00-\x08\x0B\x0C\x0E-\x1F\x7F]/g, "")
|
|
85
|
+
.trim()
|
|
86
|
+
.slice(0, this.maxMessageLength);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
isValidURL(url) {
|
|
90
|
+
try { const u = new URL(url); return ["http:", "https:"].includes(u.protocol); }
|
|
91
|
+
catch (_) { return false; }
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
isValidImageURL(url) {
|
|
95
|
+
return this.isValidURL(url) && /\.(jpg|jpeg|png|gif|webp|bmp|svg|avif|heic)(\?.*)?$/i.test(url);
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
isValidVideoURL(url) {
|
|
99
|
+
return this.isValidURL(url) && /\.(mp4|webm|mov|avi|mkv|3gp)(\?.*)?$/i.test(url);
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
isValidEmail(email) { return RULES.email(email); }
|
|
103
|
+
|
|
104
|
+
isValidReaction(reaction) {
|
|
105
|
+
if (!reaction || typeof reaction !== "string") return false;
|
|
106
|
+
const r = reaction.trim();
|
|
107
|
+
return r.length > 0 && r.length <= this.maxReactionLen;
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
isValidThreadName(name) {
|
|
111
|
+
return typeof name === "string" && name.trim().length > 0 && name.length <= this.maxThreadName;
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
isValidNickname(nick) {
|
|
115
|
+
return typeof nick === "string" && nick.length <= this.maxNickname;
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
isValidColor(color) {
|
|
119
|
+
return typeof color === "string" && /^(#[0-9A-Fa-f]{6}|#[0-9A-Fa-f]{3}|[a-zA-Z]+)$/.test(color.trim());
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
isValidAttachment(att) {
|
|
123
|
+
if (!att || typeof att !== "object") return false;
|
|
124
|
+
return !!(att.type && (att.url || att.path || att.data));
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
sanitizeID(id) {
|
|
128
|
+
const s = String(id || "").trim().replace(/\D/g, "");
|
|
129
|
+
return s.length > 0 && s.length <= 20 ? s : null;
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
sanitizeOptions(opts, defaults = {}) {
|
|
133
|
+
if (!opts || typeof opts !== "object") return { ...defaults };
|
|
134
|
+
const clean = { ...defaults };
|
|
135
|
+
for (const [k, v] of Object.entries(opts)) {
|
|
136
|
+
if (Object.prototype.hasOwnProperty.call(defaults, k)) {
|
|
137
|
+
clean[k] = typeof defaults[k] === typeof v ? v : defaults[k];
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
return clean;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
coerce(value, type) {
|
|
144
|
+
switch (type) {
|
|
145
|
+
case "string": return String(value ?? "");
|
|
146
|
+
case "number": return Number(value) || 0;
|
|
147
|
+
case "boolean": return Boolean(value);
|
|
148
|
+
case "array": return Array.isArray(value) ? value : [value].filter(v => v != null);
|
|
149
|
+
case "id": return this.sanitizeID(value);
|
|
150
|
+
default: return value;
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
const globalValidator = new Validator();
|
|
156
|
+
|
|
157
|
+
module.exports = { Validator, ValidationError, globalValidator, RULES };
|