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.
Files changed (121) hide show
  1. package/LICENSE +58 -0
  2. package/README.md +534 -0
  3. package/index.js +35 -0
  4. package/package.json +101 -0
  5. package/phantom/core/builder/bootstrap.js +334 -0
  6. package/phantom/core/builder/config.js +78 -0
  7. package/phantom/core/builder/forge.js +113 -0
  8. package/phantom/core/builder/ignite.js +386 -0
  9. package/phantom/core/builder/options.js +61 -0
  10. package/phantom/core/engine.js +71 -0
  11. package/phantom/core/reactor.js +2 -0
  12. package/phantom/datastore/appState.js +2 -0
  13. package/phantom/datastore/appStateBackup.js +34 -0
  14. package/phantom/datastore/models/cipher/e2ee.js +48 -0
  15. package/phantom/datastore/models/cipher/vault.js +153 -0
  16. package/phantom/datastore/models/index.js +3 -0
  17. package/phantom/datastore/models/matrix/auth.js +151 -0
  18. package/phantom/datastore/models/matrix/cache.js +3 -0
  19. package/phantom/datastore/models/matrix/checker.js +2 -0
  20. package/phantom/datastore/models/matrix/clients.js +2 -0
  21. package/phantom/datastore/models/matrix/constants.js +2 -0
  22. package/phantom/datastore/models/matrix/credentials.js +2 -0
  23. package/phantom/datastore/models/matrix/cycle.js +2 -0
  24. package/phantom/datastore/models/matrix/gate.js +282 -0
  25. package/phantom/datastore/models/matrix/ghost.js +332 -0
  26. package/phantom/datastore/models/matrix/headers.js +193 -0
  27. package/phantom/datastore/models/matrix/heartbeat.js +298 -0
  28. package/phantom/datastore/models/matrix/identity.js +235 -0
  29. package/phantom/datastore/models/matrix/logger.js +271 -0
  30. package/phantom/datastore/models/matrix/monitor.js +2 -0
  31. package/phantom/datastore/models/matrix/net.js +316 -0
  32. package/phantom/datastore/models/matrix/response.js +193 -0
  33. package/phantom/datastore/models/matrix/revive.js +255 -0
  34. package/phantom/datastore/models/matrix/signals.js +2 -0
  35. package/phantom/datastore/models/matrix/store.js +263 -0
  36. package/phantom/datastore/models/matrix/telemetry.js +272 -0
  37. package/phantom/datastore/models/matrix/tools.js +93 -0
  38. package/phantom/datastore/models/matrix/transform/cookieParser.js +2 -0
  39. package/phantom/datastore/models/matrix/transform/cookies.js +114 -0
  40. package/phantom/datastore/models/matrix/transform/index.js +203 -0
  41. package/phantom/datastore/models/matrix/validator.js +157 -0
  42. package/phantom/datastore/models/types/index.d.ts +498 -0
  43. package/phantom/datastore/schema.js +167 -0
  44. package/phantom/datastore/session.js +129 -0
  45. package/phantom/datastore/threads.js +22 -0
  46. package/phantom/datastore/users.js +26 -0
  47. package/phantom/dispatch/addExternalModule.js +239 -0
  48. package/phantom/dispatch/addUserToGroup.js +161 -0
  49. package/phantom/dispatch/changeAdminStatus.js +142 -0
  50. package/phantom/dispatch/changeArchivedStatus.js +135 -0
  51. package/phantom/dispatch/changeAvatar.js +123 -0
  52. package/phantom/dispatch/changeBio.js +86 -0
  53. package/phantom/dispatch/changeBlockedStatus.js +86 -0
  54. package/phantom/dispatch/changeGroupImage.js +145 -0
  55. package/phantom/dispatch/changeThreadColor.js +172 -0
  56. package/phantom/dispatch/changeThreadEmoji.js +130 -0
  57. package/phantom/dispatch/comment.js +136 -0
  58. package/phantom/dispatch/createAITheme.js +333 -0
  59. package/phantom/dispatch/createNewGroup.js +99 -0
  60. package/phantom/dispatch/createPoll.js +148 -0
  61. package/phantom/dispatch/deleteMessage.js +131 -0
  62. package/phantom/dispatch/deleteThread.js +155 -0
  63. package/phantom/dispatch/e2ee.js +101 -0
  64. package/phantom/dispatch/editMessage.js +158 -0
  65. package/phantom/dispatch/emoji.js +143 -0
  66. package/phantom/dispatch/fetchThemeData.js +233 -0
  67. package/phantom/dispatch/follow.js +111 -0
  68. package/phantom/dispatch/forwardMessage.js +110 -0
  69. package/phantom/dispatch/friend.js +189 -0
  70. package/phantom/dispatch/gcmember.js +138 -0
  71. package/phantom/dispatch/gcname.js +131 -0
  72. package/phantom/dispatch/gcrule.js +111 -0
  73. package/phantom/dispatch/getAccess.js +109 -0
  74. package/phantom/dispatch/getBotInfo.js +81 -0
  75. package/phantom/dispatch/getBotInitialData.js +110 -0
  76. package/phantom/dispatch/getFriendsList.js +118 -0
  77. package/phantom/dispatch/getMessage.js +199 -0
  78. package/phantom/dispatch/getTheme.js +199 -0
  79. package/phantom/dispatch/getThemeInfo.js +160 -0
  80. package/phantom/dispatch/getThreadHistory.js +139 -0
  81. package/phantom/dispatch/getThreadInfo.js +153 -0
  82. package/phantom/dispatch/getThreadList.js +132 -0
  83. package/phantom/dispatch/getThreadPictures.js +93 -0
  84. package/phantom/dispatch/getUserID.js +147 -0
  85. package/phantom/dispatch/getUserInfo.js +513 -0
  86. package/phantom/dispatch/getUserInfoV2.js +146 -0
  87. package/phantom/dispatch/handleMessageRequest.js +50 -0
  88. package/phantom/dispatch/httpGet.js +63 -0
  89. package/phantom/dispatch/httpPost.js +89 -0
  90. package/phantom/dispatch/httpPostFormData.js +69 -0
  91. package/phantom/dispatch/listenMqtt.js +1236 -0
  92. package/phantom/dispatch/listenSpeed.js +179 -0
  93. package/phantom/dispatch/logout.js +93 -0
  94. package/phantom/dispatch/markAsDelivered.js +92 -0
  95. package/phantom/dispatch/markAsRead.js +119 -0
  96. package/phantom/dispatch/markAsReadAll.js +215 -0
  97. package/phantom/dispatch/markAsSeen.js +70 -0
  98. package/phantom/dispatch/mqttDeltaValue.js +278 -0
  99. package/phantom/dispatch/muteThread.js +253 -0
  100. package/phantom/dispatch/nickname.js +132 -0
  101. package/phantom/dispatch/notes.js +263 -0
  102. package/phantom/dispatch/pinMessage.js +238 -0
  103. package/phantom/dispatch/produceMetaTheme.js +335 -0
  104. package/phantom/dispatch/realtime.js +291 -0
  105. package/phantom/dispatch/removeUserFromGroup.js +248 -0
  106. package/phantom/dispatch/resolvePhotoUrl.js +217 -0
  107. package/phantom/dispatch/searchForThread.js +258 -0
  108. package/phantom/dispatch/sendMessage.js +354 -0
  109. package/phantom/dispatch/sendMessageMqtt.js +249 -0
  110. package/phantom/dispatch/sendTypingIndicator.js +206 -0
  111. package/phantom/dispatch/setMessageReaction.js +188 -0
  112. package/phantom/dispatch/setMessageReactionMqtt.js +248 -0
  113. package/phantom/dispatch/setThreadTheme.js +330 -0
  114. package/phantom/dispatch/setThreadThemeMqtt.js +207 -0
  115. package/phantom/dispatch/share.js +200 -0
  116. package/phantom/dispatch/shareContact.js +216 -0
  117. package/phantom/dispatch/stickers.js +395 -0
  118. package/phantom/dispatch/story.js +240 -0
  119. package/phantom/dispatch/theme.js +296 -0
  120. package/phantom/dispatch/unfriend.js +199 -0
  121. 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 };