@lazyneoaz/nkxchat 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 +3 -0
- package/README.md +199 -0
- package/examples/login-with-cookies.js +102 -0
- package/examples/verify.js +70 -0
- package/index.js +2 -0
- package/package.json +84 -0
- package/src/apis/addExternalModule.js +24 -0
- package/src/apis/addUserToGroup.js +108 -0
- package/src/apis/changeAdminStatus.js +148 -0
- package/src/apis/changeArchivedStatus.js +61 -0
- package/src/apis/changeAvatar.js +103 -0
- package/src/apis/changeBio.js +69 -0
- package/src/apis/changeBlockedStatus.js +54 -0
- package/src/apis/changeGroupImage.js +136 -0
- package/src/apis/changeThreadColor.js +116 -0
- package/src/apis/changeThreadEmoji.js +53 -0
- package/src/apis/comment.js +207 -0
- package/src/apis/createAITheme.js +129 -0
- package/src/apis/createNewGroup.js +79 -0
- package/src/apis/createPoll.js +73 -0
- package/src/apis/deleteMessage.js +44 -0
- package/src/apis/deleteThread.js +52 -0
- package/src/apis/editMessage.js +70 -0
- package/src/apis/emoji.js +124 -0
- package/src/apis/enableAutoSaveAppState.js +69 -0
- package/src/apis/fetchThemeData.js +113 -0
- package/src/apis/follow.js +81 -0
- package/src/apis/forwardAttachment.js +178 -0
- package/src/apis/forwardMessage.js +52 -0
- package/src/apis/friend.js +243 -0
- package/src/apis/gcmember.js +122 -0
- package/src/apis/gcname.js +123 -0
- package/src/apis/gcrule.js +119 -0
- package/src/apis/getAccess.js +111 -0
- package/src/apis/getBotInfo.js +88 -0
- package/src/apis/getBotInitialData.js +43 -0
- package/src/apis/getEmojiUrl.js +40 -0
- package/src/apis/getFriendsList.js +79 -0
- package/src/apis/getMessage.js +423 -0
- package/src/apis/getTheme.js +123 -0
- package/src/apis/getThemeInfo.js +116 -0
- package/src/apis/getThemePictures.js +87 -0
- package/src/apis/getThreadColors.js +119 -0
- package/src/apis/getThreadHistory.js +239 -0
- package/src/apis/getThreadInfo.js +267 -0
- package/src/apis/getThreadList.js +232 -0
- package/src/apis/getThreadPictures.js +58 -0
- package/src/apis/getUserID.js +117 -0
- package/src/apis/getUserInfo.js +513 -0
- package/src/apis/getUserInfoV2.js +146 -0
- package/src/apis/handleFriendRequest.js +66 -0
- package/src/apis/handleMessageRequest.js +50 -0
- package/src/apis/httpGet.js +63 -0
- package/src/apis/httpPost.js +89 -0
- package/src/apis/httpPostFormData.js +69 -0
- package/src/apis/listenMqtt.js +924 -0
- package/src/apis/listenSpeed.js +178 -0
- package/src/apis/logout.js +63 -0
- package/src/apis/markAsDelivered.js +47 -0
- package/src/apis/markAsRead.js +95 -0
- package/src/apis/markAsReadAll.js +40 -0
- package/src/apis/markAsSeen.js +70 -0
- package/src/apis/mqttDeltaValue.js +252 -0
- package/src/apis/muteThread.js +45 -0
- package/src/apis/nickname.js +132 -0
- package/src/apis/notes.js +163 -0
- package/src/apis/pinMessage.js +150 -0
- package/src/apis/produceMetaTheme.js +160 -0
- package/src/apis/realtime.js +182 -0
- package/src/apis/refreshFb_dtsg.js +94 -0
- package/src/apis/removeUserFromGroup.js +117 -0
- package/src/apis/resolvePhotoUrl.js +58 -0
- package/src/apis/searchForThread.js +154 -0
- package/src/apis/sendEffect.js +306 -0
- package/src/apis/sendMessage.js +353 -0
- package/src/apis/sendMessageMqtt.js +255 -0
- package/src/apis/sendTypingIndicator.js +40 -0
- package/src/apis/setMessageReaction.js +27 -0
- package/src/apis/setMessageReactionMqtt.js +61 -0
- package/src/apis/setPostReaction.js +118 -0
- package/src/apis/setThreadTheme.js +210 -0
- package/src/apis/setThreadThemeMqtt.js +94 -0
- package/src/apis/setTitle.js +26 -0
- package/src/apis/share.js +106 -0
- package/src/apis/shareContact.js +66 -0
- package/src/apis/stickers.js +257 -0
- package/src/apis/story.js +181 -0
- package/src/apis/theme.js +233 -0
- package/src/apis/unfriend.js +47 -0
- package/src/apis/unsendMessage.js +17 -0
- package/src/apis/uploadAttachment.js +87 -0
- package/src/database/appStateBackup.js +189 -0
- package/src/database/models/index.js +56 -0
- package/src/database/models/thread.js +31 -0
- package/src/database/models/user.js +32 -0
- package/src/database/threadData.js +101 -0
- package/src/database/userData.js +90 -0
- package/src/engine/client.js +92 -0
- package/src/engine/models/buildAPI.js +118 -0
- package/src/engine/models/loginHelper.js +492 -0
- package/src/engine/models/setOptions.js +88 -0
- package/src/types/index.d.ts +498 -0
- package/src/utils/antiSuspension.js +516 -0
- package/src/utils/auth-helpers.js +149 -0
- package/src/utils/autoReLogin.js +237 -0
- package/src/utils/axios.js +368 -0
- package/src/utils/cache.js +54 -0
- package/src/utils/clients.js +279 -0
- package/src/utils/constants.js +525 -0
- package/src/utils/formatters/data/formatAttachment.js +370 -0
- package/src/utils/formatters/data/formatDelta.js +109 -0
- package/src/utils/formatters/index.js +159 -0
- package/src/utils/formatters/value/formatCookie.js +91 -0
- package/src/utils/formatters/value/formatDate.js +36 -0
- package/src/utils/formatters/value/formatID.js +16 -0
- package/src/utils/formatters.js +1369 -0
- package/src/utils/headers.js +235 -0
- package/src/utils/index.js +152 -0
- package/src/utils/monitoring.js +333 -0
- package/src/utils/rateLimiter.js +251 -0
- package/src/utils/tokenRefresh.js +285 -0
- package/src/utils/user-agents.js +238 -0
- package/src/utils/validation.js +157 -0
|
@@ -0,0 +1,285 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('./index');
|
|
4
|
+
const { globalAutoReLoginManager } = require('./autoReLogin');
|
|
5
|
+
|
|
6
|
+
/**
|
|
7
|
+
* Token Refresh Manager
|
|
8
|
+
* Automatically refreshes fb_dtsg, lsd, and other tokens to prevent expiration
|
|
9
|
+
*/
|
|
10
|
+
|
|
11
|
+
class TokenRefreshManager {
|
|
12
|
+
constructor() {
|
|
13
|
+
this.refreshInterval = null;
|
|
14
|
+
this.REFRESH_INTERVAL_MS = 10 * 60 * 60 * 1000; // 10 hours
|
|
15
|
+
this.SESSION_CHECK_INTERVAL_MS = 2 * 60 * 60 * 1000; // 2 hours - reduced frequency to avoid bot detection
|
|
16
|
+
this.lastRefresh = Date.now();
|
|
17
|
+
this.lastSessionCheck = Date.now();
|
|
18
|
+
this.failureCount = 0;
|
|
19
|
+
this.MAX_FAILURES = 3;
|
|
20
|
+
this.onSessionExpiry = null;
|
|
21
|
+
this.sessionHealthCheckInterval = null;
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* Start automatic token refresh
|
|
26
|
+
* @param {Object} ctx - Application context
|
|
27
|
+
* @param {Object} defaultFuncs - Default functions
|
|
28
|
+
* @param {string} fbLink - Facebook link
|
|
29
|
+
*/
|
|
30
|
+
startAutoRefresh(ctx, defaultFuncs, fbLink) {
|
|
31
|
+
if (this.refreshInterval) {
|
|
32
|
+
clearInterval(this.refreshInterval);
|
|
33
|
+
this.refreshInterval = null;
|
|
34
|
+
}
|
|
35
|
+
if (this.sessionHealthCheckInterval) {
|
|
36
|
+
clearInterval(this.sessionHealthCheckInterval);
|
|
37
|
+
this.sessionHealthCheckInterval = null;
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
const scheduleNext = () => {
|
|
41
|
+
const base = this.REFRESH_INTERVAL_MS;
|
|
42
|
+
const jitter = Math.floor(base * (0.2 + Math.random() * 0.4)); // 20%–60% jitter
|
|
43
|
+
const interval = base + (Math.random() > 0.5 ? jitter : -jitter);
|
|
44
|
+
this.refreshInterval = setTimeout(async () => {
|
|
45
|
+
try {
|
|
46
|
+
await this.refreshTokens(ctx, defaultFuncs, fbLink);
|
|
47
|
+
utils.log("TokenRefresh", "Tokens refreshed successfully");
|
|
48
|
+
} catch (error) {
|
|
49
|
+
utils.error("TokenRefresh", "Failed to refresh tokens:", error.message);
|
|
50
|
+
} finally {
|
|
51
|
+
scheduleNext();
|
|
52
|
+
}
|
|
53
|
+
}, interval);
|
|
54
|
+
utils.log("TokenRefresh", `Auto-refresh scheduled in ${interval}ms`);
|
|
55
|
+
};
|
|
56
|
+
|
|
57
|
+
// Start session health checks
|
|
58
|
+
this.sessionHealthCheckInterval = setInterval(async () => {
|
|
59
|
+
try {
|
|
60
|
+
const isHealthy = await this.checkSessionHealth(ctx, defaultFuncs, fbLink);
|
|
61
|
+
if (!isHealthy) {
|
|
62
|
+
utils.warn("TokenRefresh", "Session health check failed, triggering refresh");
|
|
63
|
+
await this.refreshTokens(ctx, defaultFuncs, fbLink);
|
|
64
|
+
}
|
|
65
|
+
} catch (error) {
|
|
66
|
+
utils.error("TokenRefresh", "Session health check error:", error.message);
|
|
67
|
+
}
|
|
68
|
+
}, this.SESSION_CHECK_INTERVAL_MS);
|
|
69
|
+
|
|
70
|
+
scheduleNext();
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Check if session is healthy using a lightweight AJAX ping instead of a
|
|
75
|
+
* full homepage load. Fetching the full homepage every 2 hours is a clear
|
|
76
|
+
* automation fingerprint; a small presence/ping endpoint is far less
|
|
77
|
+
* conspicuous and produces a much smaller response.
|
|
78
|
+
*
|
|
79
|
+
* @param {Object} ctx - Application context
|
|
80
|
+
* @param {Object} defaultFuncs - Default functions
|
|
81
|
+
* @param {string} fbLink - Facebook link
|
|
82
|
+
* @returns {Promise<boolean>}
|
|
83
|
+
*/
|
|
84
|
+
async checkSessionHealth(ctx, defaultFuncs, fbLink) {
|
|
85
|
+
try {
|
|
86
|
+
// Use a lightweight AJAX endpoint that only returns a small JSON
|
|
87
|
+
// payload — not the full multi-megabyte homepage.
|
|
88
|
+
const probeUrl = 'https://www.facebook.com/ajax/presence/reconnect.php';
|
|
89
|
+
const probeCtx = { ...ctx, _skipSessionInspect: true };
|
|
90
|
+
const resp = await utils.get(
|
|
91
|
+
probeUrl,
|
|
92
|
+
ctx.jar,
|
|
93
|
+
{ reason: 'reconnect', __a: 1, __req: 'probe' },
|
|
94
|
+
ctx.globalOptions,
|
|
95
|
+
probeCtx
|
|
96
|
+
);
|
|
97
|
+
|
|
98
|
+
const body = resp.body;
|
|
99
|
+
if (!body) return false;
|
|
100
|
+
|
|
101
|
+
const bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
|
|
102
|
+
|
|
103
|
+
// If we get a login-page redirect in the probe response the session is gone
|
|
104
|
+
const isLoginPage =
|
|
105
|
+
bodyStr.includes('<form id="login_form"') ||
|
|
106
|
+
bodyStr.includes('"login_page"') ||
|
|
107
|
+
bodyStr.includes('id="loginbutton"');
|
|
108
|
+
|
|
109
|
+
if (isLoginPage) return false;
|
|
110
|
+
|
|
111
|
+
const isCheckpoint =
|
|
112
|
+
bodyStr.includes('"checkpoint"') && bodyStr.includes('"flow_type"');
|
|
113
|
+
if (isCheckpoint) return false;
|
|
114
|
+
|
|
115
|
+
this.lastSessionCheck = Date.now();
|
|
116
|
+
return true;
|
|
117
|
+
} catch (error) {
|
|
118
|
+
// A network error doesn't mean the session is gone — be conservative
|
|
119
|
+
// and only return false for clear auth failures.
|
|
120
|
+
const msg = error.message || '';
|
|
121
|
+
if (msg.includes('Not logged in') || msg.includes('Session has expired')) {
|
|
122
|
+
utils.error("TokenRefresh", "Session health check: session expired:", msg);
|
|
123
|
+
return false;
|
|
124
|
+
}
|
|
125
|
+
utils.warn("TokenRefresh", "Session health check network error (ignoring):", msg);
|
|
126
|
+
return true;
|
|
127
|
+
}
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
/**
|
|
131
|
+
* Manually refresh tokens with retry logic
|
|
132
|
+
* @param {Object} ctx - Application context
|
|
133
|
+
* @param {Object} defaultFuncs - Default functions
|
|
134
|
+
* @param {string} fbLink - Facebook link
|
|
135
|
+
* @param {number} retryCount - Current retry attempt (internal use)
|
|
136
|
+
* @returns {Promise<boolean>}
|
|
137
|
+
*/
|
|
138
|
+
async refreshTokens(ctx, defaultFuncs, fbLink, retryCount = 0) {
|
|
139
|
+
const MAX_RETRIES = 3;
|
|
140
|
+
const RETRY_DELAYS = [2000, 5000, 10000];
|
|
141
|
+
|
|
142
|
+
try {
|
|
143
|
+
const resp = await utils.get(fbLink, ctx.jar, null, ctx.globalOptions, { noRef: true });
|
|
144
|
+
|
|
145
|
+
const html = resp.body;
|
|
146
|
+
if (!html) {
|
|
147
|
+
throw new Error("Empty response from Facebook");
|
|
148
|
+
}
|
|
149
|
+
|
|
150
|
+
// Precise check - broad html.includes("login") is a false positive because
|
|
151
|
+
// Facebook includes the word "login" all over authenticated pages too.
|
|
152
|
+
const isLoginPage = html.includes('<form id="login_form"') ||
|
|
153
|
+
html.includes('id="loginbutton"') ||
|
|
154
|
+
html.includes('"login_page"') ||
|
|
155
|
+
html.includes('id="email" name="email"');
|
|
156
|
+
const isCheckpoint = html.includes('"checkpoint"') && html.includes('"flow_type"');
|
|
157
|
+
|
|
158
|
+
if (isLoginPage || isCheckpoint) {
|
|
159
|
+
if (isCheckpoint) {
|
|
160
|
+
try {
|
|
161
|
+
const { globalAntiSuspension } = require('./antiSuspension');
|
|
162
|
+
globalAntiSuspension.tripCircuitBreaker('checkpoint_on_token_refresh', 60 * 60 * 1000);
|
|
163
|
+
} catch (_) {}
|
|
164
|
+
}
|
|
165
|
+
throw new Error("Session expired or checkpoint required");
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
const dtsgMatch = html.match(/"DTSGInitialData",\[],{"token":"([^"]+)"/);
|
|
169
|
+
if (dtsgMatch) {
|
|
170
|
+
ctx.fb_dtsg = dtsgMatch[1];
|
|
171
|
+
ctx.ttstamp = "2";
|
|
172
|
+
for (let i = 0; i < ctx.fb_dtsg.length; i++) {
|
|
173
|
+
ctx.ttstamp += ctx.fb_dtsg.charCodeAt(i);
|
|
174
|
+
}
|
|
175
|
+
} else {
|
|
176
|
+
throw new Error("Failed to extract fb_dtsg token");
|
|
177
|
+
}
|
|
178
|
+
|
|
179
|
+
const lsdMatch = html.match(/"LSD",\[],{"token":"([^"]+)"/);
|
|
180
|
+
if (lsdMatch) {
|
|
181
|
+
ctx.lsd = lsdMatch[1];
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
const jazoestMatch = html.match(/jazoest=(\d+)/);
|
|
185
|
+
if (jazoestMatch) {
|
|
186
|
+
ctx.jazoest = jazoestMatch[1];
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
const revisionMatch = html.match(/"client_revision":(\d+)/);
|
|
190
|
+
if (revisionMatch) {
|
|
191
|
+
ctx.__rev = revisionMatch[1];
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
this.lastRefresh = Date.now();
|
|
195
|
+
this.failureCount = 0;
|
|
196
|
+
try {
|
|
197
|
+
if (globalAutoReLoginManager && globalAutoReLoginManager.isEnabled && globalAutoReLoginManager.isEnabled()) {
|
|
198
|
+
const appState = utils.getAppState(ctx.jar);
|
|
199
|
+
globalAutoReLoginManager.updateAppState(appState);
|
|
200
|
+
}
|
|
201
|
+
} catch (_) {}
|
|
202
|
+
return true;
|
|
203
|
+
} catch (error) {
|
|
204
|
+
this.failureCount++;
|
|
205
|
+
utils.error("TokenRefresh", `Refresh failed (attempt ${retryCount + 1}/${MAX_RETRIES + 1}):`, error.message);
|
|
206
|
+
|
|
207
|
+
if (this.failureCount >= this.MAX_FAILURES) {
|
|
208
|
+
utils.error("TokenRefresh", `Maximum failures (${this.MAX_FAILURES}) reached. Session may be expired.`);
|
|
209
|
+
if (this.onSessionExpiry && typeof this.onSessionExpiry === 'function') {
|
|
210
|
+
this.onSessionExpiry(error);
|
|
211
|
+
}
|
|
212
|
+
return false;
|
|
213
|
+
}
|
|
214
|
+
|
|
215
|
+
if (retryCount < MAX_RETRIES) {
|
|
216
|
+
const delay = RETRY_DELAYS[retryCount];
|
|
217
|
+
utils.log("TokenRefresh", `Retrying in ${delay}ms...`);
|
|
218
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
219
|
+
return await this.refreshTokens(ctx, defaultFuncs, fbLink, retryCount + 1);
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
return false;
|
|
223
|
+
}
|
|
224
|
+
}
|
|
225
|
+
|
|
226
|
+
/**
|
|
227
|
+
* Stop automatic token refresh
|
|
228
|
+
*/
|
|
229
|
+
stopAutoRefresh() {
|
|
230
|
+
if (this.refreshInterval) {
|
|
231
|
+
clearTimeout(this.refreshInterval);
|
|
232
|
+
this.refreshInterval = null;
|
|
233
|
+
utils.log("TokenRefresh", "Auto-refresh disabled");
|
|
234
|
+
}
|
|
235
|
+
if (this.sessionHealthCheckInterval) {
|
|
236
|
+
clearInterval(this.sessionHealthCheckInterval);
|
|
237
|
+
this.sessionHealthCheckInterval = null;
|
|
238
|
+
utils.log("TokenRefresh", "Session health checks disabled");
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
/**
|
|
243
|
+
* Get time until next refresh
|
|
244
|
+
* @returns {number} Milliseconds until next refresh
|
|
245
|
+
*/
|
|
246
|
+
getTimeUntilNextRefresh() {
|
|
247
|
+
if (!this.refreshInterval) return -1;
|
|
248
|
+
return Math.max(0, this.REFRESH_INTERVAL_MS - (Date.now() - this.lastRefresh));
|
|
249
|
+
}
|
|
250
|
+
|
|
251
|
+
/**
|
|
252
|
+
* Check if tokens need immediate refresh
|
|
253
|
+
* @returns {boolean}
|
|
254
|
+
*/
|
|
255
|
+
needsImmediateRefresh() {
|
|
256
|
+
return (Date.now() - this.lastRefresh) >= this.REFRESH_INTERVAL_MS;
|
|
257
|
+
}
|
|
258
|
+
|
|
259
|
+
/**
|
|
260
|
+
* Set callback for session expiry detection
|
|
261
|
+
* @param {Function} callback - Callback function to trigger on session expiry
|
|
262
|
+
*/
|
|
263
|
+
setSessionExpiryCallback(callback) {
|
|
264
|
+
this.onSessionExpiry = callback;
|
|
265
|
+
}
|
|
266
|
+
|
|
267
|
+
/**
|
|
268
|
+
* Reset failure count (useful after successful re-login)
|
|
269
|
+
*/
|
|
270
|
+
resetFailureCount() {
|
|
271
|
+
this.failureCount = 0;
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* Get current failure count
|
|
276
|
+
* @returns {number}
|
|
277
|
+
*/
|
|
278
|
+
getFailureCount() {
|
|
279
|
+
return this.failureCount;
|
|
280
|
+
}
|
|
281
|
+
}
|
|
282
|
+
|
|
283
|
+
module.exports = {
|
|
284
|
+
TokenRefreshManager
|
|
285
|
+
};
|
|
@@ -0,0 +1,238 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
const { getRandom } = require("./constants");
|
|
3
|
+
|
|
4
|
+
const BROWSER_DATA = {
|
|
5
|
+
windows: {
|
|
6
|
+
platform: "Windows NT 10.0; Win64; x64",
|
|
7
|
+
chromeVersions: ["139.0.0.0", "131.0.6778.86", "130.0.6723.92", "129.0.6668.101", "128.0.6613.120", "127.0.6533.120"],
|
|
8
|
+
edgeVersions: ["139.0.0.0", "131.0.2903.51", "130.0.2849.68", "129.0.2792.89"],
|
|
9
|
+
platformVersion: '"15.0.0"'
|
|
10
|
+
},
|
|
11
|
+
mac: {
|
|
12
|
+
platform: "Macintosh; Intel Mac OS X 10_15_7",
|
|
13
|
+
chromeVersions: ["139.0.0.0", "131.0.6778.86", "130.0.6723.92", "129.0.6668.101", "128.0.6613.120", "127.0.6533.120"],
|
|
14
|
+
edgeVersions: ["139.0.0.0", "131.0.2903.51", "130.0.2849.68", "129.0.2792.89"],
|
|
15
|
+
platformVersion: '"14.7.0"'
|
|
16
|
+
},
|
|
17
|
+
linux: {
|
|
18
|
+
platform: "X11; Linux x86_64",
|
|
19
|
+
chromeVersions: ["139.0.0.0", "131.0.6778.86", "130.0.6723.92", "129.0.6668.101", "128.0.6613.120"],
|
|
20
|
+
edgeVersions: ["139.0.0.0", "131.0.2903.51", "130.0.2849.68"],
|
|
21
|
+
platformVersion: '""'
|
|
22
|
+
}
|
|
23
|
+
};
|
|
24
|
+
|
|
25
|
+
const defaultUserAgent = "Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/139.0.0.0 Safari/537.36";
|
|
26
|
+
|
|
27
|
+
/**
|
|
28
|
+
* Generates a realistic, randomized User-Agent string and related Sec-CH headers.
|
|
29
|
+
* Supports Chrome and Edge browsers across Windows, macOS, and Linux.
|
|
30
|
+
* @returns {{userAgent: string, secChUa: string, secChUaFullVersionList: string, secChUaPlatform: string, secChUaPlatformVersion: string, browser: string}}
|
|
31
|
+
*/
|
|
32
|
+
function randomUserAgent() {
|
|
33
|
+
const os = getRandom(Object.keys(BROWSER_DATA));
|
|
34
|
+
const data = BROWSER_DATA[os];
|
|
35
|
+
|
|
36
|
+
const useEdge = Math.random() > 0.7 && data.edgeVersions;
|
|
37
|
+
const versions = useEdge ? data.edgeVersions : data.chromeVersions;
|
|
38
|
+
const version = getRandom(versions);
|
|
39
|
+
const majorVersion = version.split('.')[0];
|
|
40
|
+
const browserName = useEdge ? 'Microsoft Edge' : 'Google Chrome';
|
|
41
|
+
const browserIdentifier = useEdge ? 'Edg' : 'Chrome';
|
|
42
|
+
|
|
43
|
+
const userAgent = useEdge
|
|
44
|
+
? `Mozilla/5.0 (${data.platform}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${version} Safari/537.36 Edg/${version}`
|
|
45
|
+
: `Mozilla/5.0 (${data.platform}) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/${version} Safari/537.36`;
|
|
46
|
+
|
|
47
|
+
const greeseValue = Math.random() > 0.5 ? '99' : '8';
|
|
48
|
+
const brands = useEdge ? [
|
|
49
|
+
`"Chromium";v="${majorVersion}"`,
|
|
50
|
+
`"Not(A:Brand";v="${greeseValue}"`,
|
|
51
|
+
`"${browserName}";v="${majorVersion}"`
|
|
52
|
+
] : [
|
|
53
|
+
`"${browserName}";v="${majorVersion}"`,
|
|
54
|
+
`"Not;A=Brand";v="${greeseValue}"`,
|
|
55
|
+
`"Chromium";v="${majorVersion}"`
|
|
56
|
+
];
|
|
57
|
+
|
|
58
|
+
const secChUa = brands.join(', ');
|
|
59
|
+
const secChUaFullVersionList = brands.map(b => {
|
|
60
|
+
const match = b.match(/v="(\d+)"/);
|
|
61
|
+
if (match && match[1] === majorVersion) {
|
|
62
|
+
return b.replace(`v="${majorVersion}"`, `v="${version}"`);
|
|
63
|
+
}
|
|
64
|
+
return b;
|
|
65
|
+
}).join(', ');
|
|
66
|
+
|
|
67
|
+
const platformName = os === 'windows' ? 'Windows' : os === 'mac' ? 'macOS' : 'Linux';
|
|
68
|
+
|
|
69
|
+
return {
|
|
70
|
+
userAgent,
|
|
71
|
+
secChUa,
|
|
72
|
+
secChUaFullVersionList,
|
|
73
|
+
secChUaPlatform: `"${platformName}"`,
|
|
74
|
+
secChUaPlatformVersion: data.platformVersion,
|
|
75
|
+
browser: browserName
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
function randomInt(min, max) {
|
|
80
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
function randomChoice(arr) {
|
|
84
|
+
return arr[Math.floor(Math.random() * arr.length)];
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
function randomBuildId() {
|
|
88
|
+
const prefixes = ["QP1A", "RP1A", "SP1A", "TP1A", "UP1A", "AP4A"];
|
|
89
|
+
return `${randomChoice(prefixes)}.${randomInt(180000, 250000)}.${randomInt(10, 99)}`;
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
function randomResolution() {
|
|
93
|
+
const presets = [
|
|
94
|
+
{ width: 720, height: 1280, density: 2.0 },
|
|
95
|
+
{ width: 1080, height: 1920, density: 2.625 },
|
|
96
|
+
{ width: 1080, height: 2400, density: 3.0 },
|
|
97
|
+
{ width: 1440, height: 3040, density: 3.5 },
|
|
98
|
+
{ width: 1440, height: 3200, density: 4.0 }
|
|
99
|
+
];
|
|
100
|
+
return randomChoice(presets);
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
function randomFbav() {
|
|
104
|
+
return `${randomInt(390, 499)}.${randomInt(0, 3)}.${randomInt(0, 2)}.${randomInt(10, 60)}.${randomInt(100, 999)}`;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
function randomOrcaUA() {
|
|
108
|
+
const androidVersions = ["8.1.0", "9", "10", "11", "12", "13", "14"];
|
|
109
|
+
const devices = [
|
|
110
|
+
{ brand: "samsung", model: "SM-G996B" },
|
|
111
|
+
{ brand: "samsung", model: "SM-S908E" },
|
|
112
|
+
{ brand: "Xiaomi", model: "M2101K9AG" },
|
|
113
|
+
{ brand: "OPPO", model: "CPH2219" },
|
|
114
|
+
{ brand: "vivo", model: "V2109" },
|
|
115
|
+
{ brand: "HUAWEI", model: "VOG-L29" },
|
|
116
|
+
{ brand: "asus", model: "ASUS_I001DA" },
|
|
117
|
+
{ brand: "Google", model: "Pixel 6" },
|
|
118
|
+
{ brand: "realme", model: "RMX2170" }
|
|
119
|
+
];
|
|
120
|
+
const carriers = [
|
|
121
|
+
"Viettel Telecom", "Mobifone", "Vinaphone",
|
|
122
|
+
"T-Mobile", "Verizon", "AT&T",
|
|
123
|
+
"Telkomsel", "Jio", "NTT DOCOMO",
|
|
124
|
+
"Vodafone", "Orange"
|
|
125
|
+
];
|
|
126
|
+
const locales = [
|
|
127
|
+
"vi_VN", "en_US", "en_GB", "id_ID",
|
|
128
|
+
"th_TH", "fr_FR", "de_DE", "es_ES", "pt_BR"
|
|
129
|
+
];
|
|
130
|
+
const archs = ["arm64-v8a", "armeabi-v7a"];
|
|
131
|
+
|
|
132
|
+
const androidVersion = randomChoice(androidVersions);
|
|
133
|
+
const device = randomChoice(devices);
|
|
134
|
+
const buildId = randomBuildId();
|
|
135
|
+
const resolution = randomResolution();
|
|
136
|
+
const fbav = randomFbav();
|
|
137
|
+
const fbbv = randomInt(320000000, 520000000);
|
|
138
|
+
const arch = `${randomChoice(archs)}:${randomChoice(archs)}`;
|
|
139
|
+
const selectedLocale = randomChoice(locales);
|
|
140
|
+
const selectedCarrier = randomChoice(carriers);
|
|
141
|
+
|
|
142
|
+
const userAgent = `Dalvik/2.1.0 (Linux; U; Android ${androidVersion}; ${device.model} Build/${buildId}) ` +
|
|
143
|
+
`[FBAN/Orca-Android;FBAV/${fbav};FBPN/com.facebook.orca;` +
|
|
144
|
+
`FBLC/${selectedLocale};FBBV/${fbbv};FBCR/${selectedCarrier};` +
|
|
145
|
+
`FBMF/${device.brand};FBBD/${device.brand};FBDV/${device.model};` +
|
|
146
|
+
`FBSV/${androidVersion};FBCA/${arch};` +
|
|
147
|
+
`FBDM/{density=${resolution.density.toFixed(1)},width=${resolution.width},height=${resolution.height}};` +
|
|
148
|
+
`FB_FW/1;]`;
|
|
149
|
+
|
|
150
|
+
return {
|
|
151
|
+
userAgent,
|
|
152
|
+
androidVersion,
|
|
153
|
+
device,
|
|
154
|
+
buildId,
|
|
155
|
+
resolution,
|
|
156
|
+
fbav,
|
|
157
|
+
fbbv,
|
|
158
|
+
locale: selectedLocale,
|
|
159
|
+
carrier: selectedCarrier
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
function generateUserAgentByPersona(persona = 'desktop', options = {}) {
|
|
164
|
+
if (persona === 'android' || persona === 'mobile') {
|
|
165
|
+
if (options.cachedAndroidUA && options.cachedAndroidDevice) {
|
|
166
|
+
return {
|
|
167
|
+
userAgent: options.cachedAndroidUA,
|
|
168
|
+
androidVersion: options.cachedAndroidVersion,
|
|
169
|
+
device: options.cachedAndroidDevice,
|
|
170
|
+
buildId: options.cachedAndroidBuildId,
|
|
171
|
+
resolution: options.cachedAndroidResolution,
|
|
172
|
+
fbav: options.cachedAndroidFbav,
|
|
173
|
+
fbbv: options.cachedAndroidFbbv,
|
|
174
|
+
locale: options.cachedAndroidLocale,
|
|
175
|
+
carrier: options.cachedAndroidCarrier,
|
|
176
|
+
persona: 'android'
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
const androidData = randomOrcaUA();
|
|
181
|
+
return {
|
|
182
|
+
...androidData,
|
|
183
|
+
persona: 'android'
|
|
184
|
+
};
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
if (options.cachedUserAgent && options.cachedSecChUa) {
|
|
188
|
+
return {
|
|
189
|
+
userAgent: options.cachedUserAgent,
|
|
190
|
+
secChUa: options.cachedSecChUa,
|
|
191
|
+
secChUaFullVersionList: options.cachedSecChUaFullVersionList,
|
|
192
|
+
secChUaPlatform: options.cachedSecChUaPlatform,
|
|
193
|
+
secChUaPlatformVersion: options.cachedSecChUaPlatformVersion,
|
|
194
|
+
browser: options.cachedBrowser || 'Google Chrome',
|
|
195
|
+
persona: 'desktop'
|
|
196
|
+
};
|
|
197
|
+
}
|
|
198
|
+
|
|
199
|
+
const desktopData = randomUserAgent();
|
|
200
|
+
return {
|
|
201
|
+
...desktopData,
|
|
202
|
+
persona: 'desktop'
|
|
203
|
+
};
|
|
204
|
+
}
|
|
205
|
+
|
|
206
|
+
function cachePersonaData(options, personaData) {
|
|
207
|
+
if (personaData.persona === 'android') {
|
|
208
|
+
options.cachedAndroidUA = personaData.userAgent;
|
|
209
|
+
options.cachedAndroidVersion = personaData.androidVersion;
|
|
210
|
+
options.cachedAndroidDevice = personaData.device;
|
|
211
|
+
options.cachedAndroidBuildId = personaData.buildId;
|
|
212
|
+
options.cachedAndroidResolution = personaData.resolution;
|
|
213
|
+
options.cachedAndroidFbav = personaData.fbav;
|
|
214
|
+
options.cachedAndroidFbbv = personaData.fbbv;
|
|
215
|
+
options.cachedAndroidLocale = personaData.locale;
|
|
216
|
+
options.cachedAndroidCarrier = personaData.carrier;
|
|
217
|
+
} else {
|
|
218
|
+
options.cachedUserAgent = personaData.userAgent;
|
|
219
|
+
options.cachedSecChUa = personaData.secChUa;
|
|
220
|
+
options.cachedSecChUaFullVersionList = personaData.secChUaFullVersionList;
|
|
221
|
+
options.cachedSecChUaPlatform = personaData.secChUaPlatform;
|
|
222
|
+
options.cachedSecChUaPlatformVersion = personaData.secChUaPlatformVersion;
|
|
223
|
+
options.cachedBrowser = personaData.browser;
|
|
224
|
+
}
|
|
225
|
+
return options;
|
|
226
|
+
}
|
|
227
|
+
|
|
228
|
+
module.exports = {
|
|
229
|
+
defaultUserAgent,
|
|
230
|
+
windowsUserAgent: defaultUserAgent,
|
|
231
|
+
randomUserAgent,
|
|
232
|
+
randomBuildId,
|
|
233
|
+
randomResolution,
|
|
234
|
+
randomFbav,
|
|
235
|
+
randomOrcaUA,
|
|
236
|
+
generateUserAgentByPersona,
|
|
237
|
+
cachePersonaData,
|
|
238
|
+
};
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Input validation and sanitization utilities
|
|
5
|
+
*/
|
|
6
|
+
|
|
7
|
+
class InputValidator {
|
|
8
|
+
constructor() {
|
|
9
|
+
this.maxMessageLength = 10000;
|
|
10
|
+
this.maxThreadNameLength = 255;
|
|
11
|
+
this.maxNicknameLength = 50;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Validate thread ID
|
|
16
|
+
* @param {string|number} threadID
|
|
17
|
+
* @returns {boolean}
|
|
18
|
+
*/
|
|
19
|
+
isValidThreadID(threadID) {
|
|
20
|
+
if (!threadID) return false;
|
|
21
|
+
const id = String(threadID).trim();
|
|
22
|
+
// Thread IDs are numeric, can be up to 20 digits
|
|
23
|
+
return /^\d{1,20}$/.test(id);
|
|
24
|
+
}
|
|
25
|
+
|
|
26
|
+
/**
|
|
27
|
+
* Validate user ID
|
|
28
|
+
* @param {string|number} userID
|
|
29
|
+
* @returns {boolean}
|
|
30
|
+
*/
|
|
31
|
+
isValidUserID(userID) {
|
|
32
|
+
return this.isValidThreadID(userID); // Same format
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Validate message content
|
|
37
|
+
* @param {string|object} message
|
|
38
|
+
* @returns {boolean}
|
|
39
|
+
*/
|
|
40
|
+
isValidMessage(message) {
|
|
41
|
+
if (typeof message === 'string') {
|
|
42
|
+
return message.length > 0 && message.length <= this.maxMessageLength;
|
|
43
|
+
}
|
|
44
|
+
if (typeof message === 'object' && message !== null) {
|
|
45
|
+
return message.body || message.sticker || message.attachment || message.emoji;
|
|
46
|
+
}
|
|
47
|
+
return false;
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
/**
|
|
51
|
+
* Sanitize message content
|
|
52
|
+
* @param {string} message
|
|
53
|
+
* @returns {string}
|
|
54
|
+
*/
|
|
55
|
+
sanitizeMessage(message) {
|
|
56
|
+
if (typeof message !== 'string') return message;
|
|
57
|
+
// Remove null bytes and control characters
|
|
58
|
+
return message.replace(/[\x00-\x1F\x7F]/g, '').trim();
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
/**
|
|
62
|
+
* Validate and sanitize thread name
|
|
63
|
+
* @param {string} name
|
|
64
|
+
* @returns {string|null}
|
|
65
|
+
*/
|
|
66
|
+
sanitizeThreadName(name) {
|
|
67
|
+
if (typeof name !== 'string') return null;
|
|
68
|
+
const sanitized = name.trim();
|
|
69
|
+
return sanitized.length > 0 && sanitized.length <= this.maxThreadNameLength ? sanitized : null;
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
/**
|
|
73
|
+
* Validate and sanitize nickname
|
|
74
|
+
* @param {string} nickname
|
|
75
|
+
* @returns {string|null}
|
|
76
|
+
*/
|
|
77
|
+
sanitizeNickname(nickname) {
|
|
78
|
+
if (typeof nickname !== 'string') return null;
|
|
79
|
+
const sanitized = nickname.trim();
|
|
80
|
+
return sanitized.length > 0 && sanitized.length <= this.maxNicknameLength ? sanitized : null;
|
|
81
|
+
}
|
|
82
|
+
|
|
83
|
+
/**
|
|
84
|
+
* Validate array of IDs
|
|
85
|
+
* @param {Array} ids
|
|
86
|
+
* @param {function} validator
|
|
87
|
+
* @returns {boolean}
|
|
88
|
+
*/
|
|
89
|
+
validateIDArray(ids, validator = this.isValidThreadID.bind(this)) {
|
|
90
|
+
if (!Array.isArray(ids)) return false;
|
|
91
|
+
return ids.length > 0 && ids.length <= 100 && ids.every(validator);
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
/**
|
|
95
|
+
* Validate email
|
|
96
|
+
* @param {string} email
|
|
97
|
+
* @returns {boolean}
|
|
98
|
+
*/
|
|
99
|
+
isValidEmail(email) {
|
|
100
|
+
const emailRegex = /^[^\s@]+@[^\s@]+\.[^\s@]+$/;
|
|
101
|
+
return emailRegex.test(email);
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Validate URL
|
|
106
|
+
* @param {string} url
|
|
107
|
+
* @returns {boolean}
|
|
108
|
+
*/
|
|
109
|
+
isValidUrl(url) {
|
|
110
|
+
try {
|
|
111
|
+
new URL(url);
|
|
112
|
+
return true;
|
|
113
|
+
} catch {
|
|
114
|
+
return false;
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Validate file path (basic check)
|
|
120
|
+
* @param {string} path
|
|
121
|
+
* @returns {boolean}
|
|
122
|
+
*/
|
|
123
|
+
isValidFilePath(path) {
|
|
124
|
+
if (typeof path !== 'string') return false;
|
|
125
|
+
// Basic check - no null bytes, reasonable length
|
|
126
|
+
return path.length > 0 && path.length <= 255 && !path.includes('\x00');
|
|
127
|
+
}
|
|
128
|
+
|
|
129
|
+
/**
|
|
130
|
+
* Validate emoji
|
|
131
|
+
* @param {string} emoji
|
|
132
|
+
* @returns {boolean}
|
|
133
|
+
*/
|
|
134
|
+
isValidEmoji(emoji) {
|
|
135
|
+
if (typeof emoji !== 'string') return false;
|
|
136
|
+
// Check if it's an emoji (basic check)
|
|
137
|
+
return /\p{Emoji}/u.test(emoji) || emoji.length === 1;
|
|
138
|
+
}
|
|
139
|
+
}
|
|
140
|
+
|
|
141
|
+
const globalValidator = new InputValidator();
|
|
142
|
+
|
|
143
|
+
module.exports = {
|
|
144
|
+
InputValidator,
|
|
145
|
+
globalValidator,
|
|
146
|
+
validateThreadID: (id) => globalValidator.isValidThreadID(id),
|
|
147
|
+
validateUserID: (id) => globalValidator.isValidUserID(id),
|
|
148
|
+
validateMessage: (msg) => globalValidator.isValidMessage(msg),
|
|
149
|
+
sanitizeMessage: (msg) => globalValidator.sanitizeMessage(msg),
|
|
150
|
+
sanitizeThreadName: (name) => globalValidator.sanitizeThreadName(name),
|
|
151
|
+
sanitizeNickname: (nick) => globalValidator.sanitizeNickname(nick),
|
|
152
|
+
validateIDArray: (ids, validator) => globalValidator.validateIDArray(ids, validator),
|
|
153
|
+
isValidEmail: (email) => globalValidator.isValidEmail(email),
|
|
154
|
+
isValidUrl: (url) => globalValidator.isValidUrl(url),
|
|
155
|
+
isValidFilePath: (path) => globalValidator.isValidFilePath(path),
|
|
156
|
+
isValidEmoji: (emoji) => globalValidator.isValidEmoji(emoji)
|
|
157
|
+
};
|