@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,237 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
const utils = require('./index');
|
|
4
|
+
|
|
5
|
+
class AutoReLoginManager {
|
|
6
|
+
constructor() {
|
|
7
|
+
this.credentials = null;
|
|
8
|
+
this.loginOptions = null;
|
|
9
|
+
this.loginCallback = null;
|
|
10
|
+
this.isReLoggingIn = false;
|
|
11
|
+
this.pendingRequests = [];
|
|
12
|
+
this.maxRetries = 3; // Reduced: too many re-logins look highly suspicious to Facebook
|
|
13
|
+
this.retryCount = 0;
|
|
14
|
+
this.onReLoginSuccess = null;
|
|
15
|
+
this.onReLoginFailure = null;
|
|
16
|
+
this.enabled = false;
|
|
17
|
+
this.reLoginInterval = 1000 * 60 * 60 * 24; // 24 hours
|
|
18
|
+
this.sessionMonitorInterval = null;
|
|
19
|
+
// Removed separate 15-minute session polling - tokenRefresh already handles this.
|
|
20
|
+
// Duplicate polling from two systems every 15-30 min looked like bot traffic to Facebook.
|
|
21
|
+
this.sessionCheckInterval = 1000 * 60 * 120; // 2 hours
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
setCredentials(credentials, options, callback) {
|
|
25
|
+
this.credentials = credentials;
|
|
26
|
+
this.loginOptions = options || {};
|
|
27
|
+
this.loginCallback = callback;
|
|
28
|
+
this.enabled = true;
|
|
29
|
+
// Reset retry counter on fresh credential set so old failures
|
|
30
|
+
// from a previous session do not permanently lock re-login.
|
|
31
|
+
this.retryCount = 0;
|
|
32
|
+
// Do NOT call startSessionMonitoring() here — the api object is not
|
|
33
|
+
// available yet. loginHelper calls startSessionMonitoring(api) once
|
|
34
|
+
// all api methods are registered.
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
startSessionMonitoring(api) {
|
|
38
|
+
if (this.sessionMonitorInterval) {
|
|
39
|
+
clearInterval(this.sessionMonitorInterval);
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
if (!this.enabled || !api) return;
|
|
43
|
+
|
|
44
|
+
this.sessionMonitorInterval = setInterval(async () => {
|
|
45
|
+
if (this.isReLoggingIn) return; // Skip if already re-logging in
|
|
46
|
+
|
|
47
|
+
try {
|
|
48
|
+
const isValid = await api.isSessionValid();
|
|
49
|
+
if (!isValid) {
|
|
50
|
+
utils.warn("AutoReLogin", "Session health check failed, triggering automatic re-login");
|
|
51
|
+
await this.handleSessionExpiry(api, 'https://www.facebook.com', "Session expired during monitoring");
|
|
52
|
+
}
|
|
53
|
+
} catch (error) {
|
|
54
|
+
utils.error("AutoReLogin", "Session monitoring error:", error.message);
|
|
55
|
+
}
|
|
56
|
+
}, this.sessionCheckInterval);
|
|
57
|
+
|
|
58
|
+
utils.log("AutoReLogin", `Session monitoring started (interval: ${this.sessionCheckInterval}ms)`);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
stopSessionMonitoring() {
|
|
62
|
+
if (this.sessionMonitorInterval) {
|
|
63
|
+
clearInterval(this.sessionMonitorInterval);
|
|
64
|
+
this.sessionMonitorInterval = null;
|
|
65
|
+
utils.log("AutoReLogin", "Session monitoring stopped");
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
isEnabled() {
|
|
70
|
+
return this.enabled && this.credentials !== null;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
async handleSessionExpiry(api, fbLink, ERROR_RETRIEVING) {
|
|
74
|
+
if (!this.isEnabled()) {
|
|
75
|
+
utils.warn("AutoReLogin", "Auto re-login not enabled. Credentials not stored.");
|
|
76
|
+
return false;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (this.isReLoggingIn) {
|
|
80
|
+
utils.log("AutoReLogin", "Re-login already in progress. Queuing request...");
|
|
81
|
+
return new Promise((resolve, reject) => {
|
|
82
|
+
this.pendingRequests.push({ resolve, reject });
|
|
83
|
+
});
|
|
84
|
+
}
|
|
85
|
+
|
|
86
|
+
if (this.retryCount >= this.maxRetries) {
|
|
87
|
+
utils.error("AutoReLogin", `Maximum re-login attempts (${this.maxRetries}) exceeded`);
|
|
88
|
+
if (this.onReLoginFailure) {
|
|
89
|
+
this.onReLoginFailure(new Error("Max re-login retries exceeded"));
|
|
90
|
+
}
|
|
91
|
+
return false;
|
|
92
|
+
}
|
|
93
|
+
|
|
94
|
+
this.isReLoggingIn = true;
|
|
95
|
+
this.retryCount++;
|
|
96
|
+
utils.log("AutoReLogin", `Starting automatic re-login (attempt ${this.retryCount}/${this.maxRetries})...`);
|
|
97
|
+
|
|
98
|
+
try {
|
|
99
|
+
await this.pauseAPIRequests();
|
|
100
|
+
|
|
101
|
+
const loginHelperModel = require('../engine/models/loginHelper');
|
|
102
|
+
const setOptionsModel = require('../engine/models/setOptions');
|
|
103
|
+
const buildAPIModel = require('../engine/models/buildAPI');
|
|
104
|
+
|
|
105
|
+
await new Promise((resolve, reject) => {
|
|
106
|
+
loginHelperModel(
|
|
107
|
+
this.credentials,
|
|
108
|
+
this.loginOptions,
|
|
109
|
+
(loginError, newApi) => {
|
|
110
|
+
if (loginError) {
|
|
111
|
+
reject(loginError);
|
|
112
|
+
return;
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (api) {
|
|
116
|
+
api.ctx = newApi.ctx;
|
|
117
|
+
api.defaultFuncs = newApi.defaultFuncs;
|
|
118
|
+
|
|
119
|
+
if (api.tokenRefreshManager) {
|
|
120
|
+
api.tokenRefreshManager.resetFailureCount();
|
|
121
|
+
}
|
|
122
|
+
}
|
|
123
|
+
|
|
124
|
+
resolve(newApi);
|
|
125
|
+
},
|
|
126
|
+
setOptionsModel,
|
|
127
|
+
buildAPIModel,
|
|
128
|
+
api,
|
|
129
|
+
fbLink,
|
|
130
|
+
ERROR_RETRIEVING
|
|
131
|
+
);
|
|
132
|
+
});
|
|
133
|
+
|
|
134
|
+
utils.log("AutoReLogin", "Re-login successful! Session restored.");
|
|
135
|
+
this.retryCount = 0;
|
|
136
|
+
this.isReLoggingIn = false;
|
|
137
|
+
|
|
138
|
+
this.resolvePendingRequests(true);
|
|
139
|
+
|
|
140
|
+
if (this.onReLoginSuccess) {
|
|
141
|
+
this.onReLoginSuccess();
|
|
142
|
+
}
|
|
143
|
+
|
|
144
|
+
try {
|
|
145
|
+
if (api && api.listenMqtt && api.ctx && api.ctx._listeningActive) {
|
|
146
|
+
try {
|
|
147
|
+
if (typeof api.stopListening === 'function') {
|
|
148
|
+
try { api.stopListening(); } catch (_) {}
|
|
149
|
+
}
|
|
150
|
+
const cb = api.ctx._lastListenCallback || null;
|
|
151
|
+
if (cb) {
|
|
152
|
+
api.listenMqtt(cb);
|
|
153
|
+
} else {
|
|
154
|
+
api.listenMqtt();
|
|
155
|
+
}
|
|
156
|
+
} catch (_) {}
|
|
157
|
+
}
|
|
158
|
+
} catch (_) {}
|
|
159
|
+
|
|
160
|
+
return true;
|
|
161
|
+
} catch (error) {
|
|
162
|
+
utils.error("AutoReLogin", `Re-login failed:`, error.message);
|
|
163
|
+
this.isReLoggingIn = false;
|
|
164
|
+
|
|
165
|
+
if (this.retryCount >= this.maxRetries) {
|
|
166
|
+
this.resolvePendingRequests(false);
|
|
167
|
+
if (this.onReLoginFailure) {
|
|
168
|
+
this.onReLoginFailure(error);
|
|
169
|
+
}
|
|
170
|
+
return false;
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
const backoffDelay = Math.min(30000, Math.pow(2, this.retryCount) * 1000);
|
|
174
|
+
utils.log("AutoReLogin", `Retrying re-login in ${backoffDelay}ms...`);
|
|
175
|
+
await new Promise(resolve => setTimeout(resolve, backoffDelay));
|
|
176
|
+
|
|
177
|
+
return await this.handleSessionExpiry(api, fbLink, ERROR_RETRIEVING);
|
|
178
|
+
}
|
|
179
|
+
}
|
|
180
|
+
|
|
181
|
+
async pauseAPIRequests() {
|
|
182
|
+
utils.log("AutoReLogin", "Pausing API requests during re-login...");
|
|
183
|
+
await new Promise(resolve => setTimeout(resolve, 1000));
|
|
184
|
+
}
|
|
185
|
+
|
|
186
|
+
resolvePendingRequests(success) {
|
|
187
|
+
utils.log("AutoReLogin", `Resolving ${this.pendingRequests.length} pending requests (success: ${success})`);
|
|
188
|
+
|
|
189
|
+
this.pendingRequests.forEach(({ resolve, reject }) => {
|
|
190
|
+
if (success) {
|
|
191
|
+
resolve(true);
|
|
192
|
+
} else {
|
|
193
|
+
reject(new Error("Re-login failed"));
|
|
194
|
+
}
|
|
195
|
+
});
|
|
196
|
+
|
|
197
|
+
this.pendingRequests = [];
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
setReLoginSuccessCallback(callback) {
|
|
201
|
+
this.onReLoginSuccess = callback;
|
|
202
|
+
}
|
|
203
|
+
|
|
204
|
+
setReLoginFailureCallback(callback) {
|
|
205
|
+
this.onReLoginFailure = callback;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
updateAppState(appState) {
|
|
209
|
+
if (!this.credentials) return;
|
|
210
|
+
if (!Array.isArray(appState) || appState.length === 0) return;
|
|
211
|
+
if (!this.credentials.appState || Array.isArray(this.credentials.appState) || typeof this.credentials.appState === "string") {
|
|
212
|
+
this.credentials.appState = appState;
|
|
213
|
+
}
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
disable() {
|
|
217
|
+
this.enabled = false;
|
|
218
|
+
this.stopSessionMonitoring();
|
|
219
|
+
this.credentials = null;
|
|
220
|
+
this.loginOptions = null;
|
|
221
|
+
this.loginCallback = null;
|
|
222
|
+
utils.log("AutoReLogin", "Auto re-login disabled and credentials cleared");
|
|
223
|
+
}
|
|
224
|
+
|
|
225
|
+
reset() {
|
|
226
|
+
this.retryCount = 0;
|
|
227
|
+
this.isReLoggingIn = false;
|
|
228
|
+
this.pendingRequests = [];
|
|
229
|
+
}
|
|
230
|
+
}
|
|
231
|
+
|
|
232
|
+
const globalAutoReLoginManager = new AutoReLoginManager();
|
|
233
|
+
|
|
234
|
+
module.exports = {
|
|
235
|
+
AutoReLoginManager,
|
|
236
|
+
globalAutoReLoginManager
|
|
237
|
+
};
|
|
@@ -0,0 +1,368 @@
|
|
|
1
|
+
/* eslint-disable no-prototype-builtins */
|
|
2
|
+
"use strict";
|
|
3
|
+
|
|
4
|
+
const axios = require("axios");
|
|
5
|
+
const { CookieJar } = require("tough-cookie");
|
|
6
|
+
const { wrapper } = require("axios-cookiejar-support");
|
|
7
|
+
const FormData = require("form-data");
|
|
8
|
+
const { getHeaders } = require("./headers");
|
|
9
|
+
const { getType } = require("./constants");
|
|
10
|
+
const { globalRateLimiter } = require("./rateLimiter");
|
|
11
|
+
|
|
12
|
+
const jar = new CookieJar();
|
|
13
|
+
const client = wrapper(axios.create({ jar }));
|
|
14
|
+
|
|
15
|
+
let proxyConfig = {};
|
|
16
|
+
|
|
17
|
+
const delay = (ms) => new Promise(resolve => setTimeout(resolve, ms));
|
|
18
|
+
|
|
19
|
+
function adaptResponse(res) {
|
|
20
|
+
const response = res.response || res;
|
|
21
|
+
return {
|
|
22
|
+
...response,
|
|
23
|
+
body: response.data,
|
|
24
|
+
statusCode: response.status,
|
|
25
|
+
request: {
|
|
26
|
+
uri: new URL(response.config.url),
|
|
27
|
+
headers: response.config.headers,
|
|
28
|
+
method: response.config.method.toUpperCase(),
|
|
29
|
+
form: response.config.data,
|
|
30
|
+
formData: response.config.data
|
|
31
|
+
},
|
|
32
|
+
};
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
/**
|
|
36
|
+
* Inspects an API response body for signs of session expiry or Facebook
|
|
37
|
+
* bot-detection checkpoints and emits the appropriate signals on ctx.
|
|
38
|
+
*
|
|
39
|
+
* Returns true if the response looks like a valid authenticated response,
|
|
40
|
+
* false if it signals logout / checkpoint.
|
|
41
|
+
*
|
|
42
|
+
* When a logout is detected and ctx.performAutoLogin is available the
|
|
43
|
+
* function fires it (non-blocking) and throws so the caller knows the
|
|
44
|
+
* original response is unusable.
|
|
45
|
+
*/
|
|
46
|
+
async function inspectResponseForSessionIssues(adapted, ctx) {
|
|
47
|
+
if (!ctx || ctx._skipSessionInspect) return;
|
|
48
|
+
|
|
49
|
+
const body = adapted.body;
|
|
50
|
+
if (!body) return;
|
|
51
|
+
|
|
52
|
+
const bodyStr = typeof body === 'string' ? body : JSON.stringify(body);
|
|
53
|
+
|
|
54
|
+
// Facebook bot-detection checkpoint IDs
|
|
55
|
+
const isCheckpoint282 = bodyStr.includes('1501092823525282');
|
|
56
|
+
const isCheckpoint956 = bodyStr.includes('828281030927956');
|
|
57
|
+
const isScrapingWarning = bodyStr.includes('XCheckpointFBScrapingWarningController');
|
|
58
|
+
|
|
59
|
+
if (isCheckpoint282) {
|
|
60
|
+
const err = new Error('Bot checkpoint 282 detected. Please verify the account.');
|
|
61
|
+
err.error = 'checkpoint_282';
|
|
62
|
+
err.res = body;
|
|
63
|
+
if (ctx._emitter && typeof ctx._emitter.emit === 'function') {
|
|
64
|
+
ctx._emitter.emit('checkpoint_282', { res: body });
|
|
65
|
+
}
|
|
66
|
+
throw err;
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
if (isCheckpoint956) {
|
|
70
|
+
const err = new Error('Bot checkpoint 956 detected. Please verify the account.');
|
|
71
|
+
err.error = 'checkpoint_956';
|
|
72
|
+
err.res = body;
|
|
73
|
+
if (ctx._emitter && typeof ctx._emitter.emit === 'function') {
|
|
74
|
+
ctx._emitter.emit('checkpoint_956', { res: body });
|
|
75
|
+
}
|
|
76
|
+
throw err;
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
if (isScrapingWarning) {
|
|
80
|
+
const err = new Error('Facebook scraping warning checkpoint detected.');
|
|
81
|
+
err.error = 'checkpoint_scraping';
|
|
82
|
+
err.res = body;
|
|
83
|
+
if (ctx._emitter && typeof ctx._emitter.emit === 'function') {
|
|
84
|
+
ctx._emitter.emit('checkpoint', { type: 'scraping_warning', res: body });
|
|
85
|
+
}
|
|
86
|
+
throw err;
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
// Detect session expiry / forced logout
|
|
90
|
+
const isLoginRedirect =
|
|
91
|
+
bodyStr.includes('"login.php?') ||
|
|
92
|
+
bodyStr.includes('https://www.facebook.com/login.php') ||
|
|
93
|
+
bodyStr.includes('<form id="login_form"') ||
|
|
94
|
+
bodyStr.includes('id="loginbutton"') ||
|
|
95
|
+
bodyStr.includes('"login_page"');
|
|
96
|
+
|
|
97
|
+
const isLoginBlocked =
|
|
98
|
+
typeof body === 'object' && body !== null && body.error === 1357001;
|
|
99
|
+
|
|
100
|
+
if (isLoginBlocked) {
|
|
101
|
+
const err = new Error('Facebook blocked the login.');
|
|
102
|
+
err.error = 'login_blocked';
|
|
103
|
+
err.res = body;
|
|
104
|
+
throw err;
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
if (isLoginRedirect) {
|
|
108
|
+
if (ctx._emitter && typeof ctx._emitter.emit === 'function') {
|
|
109
|
+
ctx._emitter.emit('sessionExpired', { res: body });
|
|
110
|
+
}
|
|
111
|
+
|
|
112
|
+
if (!ctx.auto_login && typeof ctx.performAutoLogin === 'function') {
|
|
113
|
+
ctx.auto_login = true;
|
|
114
|
+
try {
|
|
115
|
+
const ok = await ctx.performAutoLogin();
|
|
116
|
+
ctx.auto_login = false;
|
|
117
|
+
if (!ok) {
|
|
118
|
+
const err = new Error('Not logged in. Auto re-login failed.');
|
|
119
|
+
err.error = 'Not logged in.';
|
|
120
|
+
err.res = body;
|
|
121
|
+
throw err;
|
|
122
|
+
}
|
|
123
|
+
} catch (autoErr) {
|
|
124
|
+
ctx.auto_login = false;
|
|
125
|
+
throw autoErr;
|
|
126
|
+
}
|
|
127
|
+
} else {
|
|
128
|
+
const err = new Error('Not logged in. Session has expired.');
|
|
129
|
+
err.error = 'Not logged in.';
|
|
130
|
+
err.res = body;
|
|
131
|
+
throw err;
|
|
132
|
+
}
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
async function requestWithRetry(requestFunction, retries = 5, endpoint = '', threadID = '', ctx = null) {
|
|
137
|
+
await globalRateLimiter.checkRateLimit();
|
|
138
|
+
|
|
139
|
+
if (globalRateLimiter.isEndpointOnCooldown("__GLOBAL__")) {
|
|
140
|
+
const cooldown = globalRateLimiter.getEndpointCooldownRemaining("__GLOBAL__");
|
|
141
|
+
console.warn(`Global cooldown active. Waiting ${cooldown}ms...`);
|
|
142
|
+
await delay(cooldown);
|
|
143
|
+
}
|
|
144
|
+
|
|
145
|
+
if (endpoint && globalRateLimiter.isEndpointOnCooldown(endpoint)) {
|
|
146
|
+
const cooldown = globalRateLimiter.getEndpointCooldownRemaining(endpoint);
|
|
147
|
+
console.warn(`Endpoint ${endpoint} on cooldown. Waiting ${cooldown}ms...`);
|
|
148
|
+
await delay(cooldown);
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
if (threadID && globalRateLimiter.isThreadOnCooldown(threadID)) {
|
|
152
|
+
const cooldown = globalRateLimiter.getCooldownRemaining(threadID);
|
|
153
|
+
console.warn(`Thread ${threadID} on cooldown. Waiting ${cooldown}ms...`);
|
|
154
|
+
await delay(cooldown);
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
const checkAndApplyRateLimitCooldowns = (responseBody) => {
|
|
158
|
+
const ERROR_COOLDOWNS = {
|
|
159
|
+
1545012: 60000,
|
|
160
|
+
1675004: 30000,
|
|
161
|
+
368: 120000,
|
|
162
|
+
404: 5000,
|
|
163
|
+
500: 10000,
|
|
164
|
+
503: 30000
|
|
165
|
+
};
|
|
166
|
+
|
|
167
|
+
const applyCooldown = (errorCode) => {
|
|
168
|
+
if (errorCode && ERROR_COOLDOWNS[errorCode]) {
|
|
169
|
+
if (threadID) {
|
|
170
|
+
globalRateLimiter.setThreadCooldown(threadID, ERROR_COOLDOWNS[errorCode]);
|
|
171
|
+
}
|
|
172
|
+
if (endpoint) {
|
|
173
|
+
globalRateLimiter.setEndpointCooldown(endpoint, ERROR_COOLDOWNS[errorCode]);
|
|
174
|
+
}
|
|
175
|
+
console.warn(`Rate limit detected (error ${errorCode}). Applied cooldown.`);
|
|
176
|
+
return true;
|
|
177
|
+
}
|
|
178
|
+
return false;
|
|
179
|
+
};
|
|
180
|
+
|
|
181
|
+
if (!responseBody || typeof responseBody !== 'object') {
|
|
182
|
+
return false;
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
if (applyCooldown(responseBody.error)) {
|
|
186
|
+
return true;
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (Array.isArray(responseBody)) {
|
|
190
|
+
for (const item of responseBody) {
|
|
191
|
+
if (item && typeof item === 'object') {
|
|
192
|
+
if (applyCooldown(item.error)) return true;
|
|
193
|
+
if (item.errors && Array.isArray(item.errors)) {
|
|
194
|
+
for (const err of item.errors) {
|
|
195
|
+
const code = err.code || err.extensions?.code;
|
|
196
|
+
if (applyCooldown(code)) return true;
|
|
197
|
+
}
|
|
198
|
+
}
|
|
199
|
+
}
|
|
200
|
+
}
|
|
201
|
+
}
|
|
202
|
+
|
|
203
|
+
if (responseBody.errors && Array.isArray(responseBody.errors)) {
|
|
204
|
+
for (const err of responseBody.errors) {
|
|
205
|
+
const code = err.code || err.extensions?.code;
|
|
206
|
+
if (applyCooldown(code)) return true;
|
|
207
|
+
}
|
|
208
|
+
}
|
|
209
|
+
|
|
210
|
+
return false;
|
|
211
|
+
};
|
|
212
|
+
|
|
213
|
+
for (let i = 0; i < retries; i++) {
|
|
214
|
+
try {
|
|
215
|
+
const res = await requestFunction();
|
|
216
|
+
const adapted = adaptResponse(res);
|
|
217
|
+
|
|
218
|
+
checkAndApplyRateLimitCooldowns(adapted.body);
|
|
219
|
+
|
|
220
|
+
// Inspect for session expiry / bot-detection checkpoints
|
|
221
|
+
await inspectResponseForSessionIssues(adapted, ctx);
|
|
222
|
+
|
|
223
|
+
return adapted;
|
|
224
|
+
} catch (error) {
|
|
225
|
+
// If this is a session/checkpoint error we already raised, propagate immediately
|
|
226
|
+
if (error.error === 'Not logged in.' ||
|
|
227
|
+
error.error === 'checkpoint_282' ||
|
|
228
|
+
error.error === 'checkpoint_956' ||
|
|
229
|
+
error.error === 'checkpoint_scraping' ||
|
|
230
|
+
error.error === 'login_blocked') {
|
|
231
|
+
throw error;
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// Abort immediately on invalid header characters - retrying won't help
|
|
235
|
+
if (error.code === 'ERR_INVALID_CHAR' ||
|
|
236
|
+
(error.message && error.message.includes('Invalid character in header'))) {
|
|
237
|
+
const e = new Error('Invalid header content detected. Request aborted.');
|
|
238
|
+
e.error = 'invalid_header';
|
|
239
|
+
e.code = 'ERR_INVALID_CHAR';
|
|
240
|
+
e.originalError = error;
|
|
241
|
+
throw e;
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
if (error.response) {
|
|
245
|
+
const adapted = adaptResponse(error.response);
|
|
246
|
+
checkAndApplyRateLimitCooldowns(adapted.body);
|
|
247
|
+
}
|
|
248
|
+
|
|
249
|
+
if (i === retries - 1) {
|
|
250
|
+
console.error(`Request failed after ${retries} attempts:`, error.message);
|
|
251
|
+
if (error.response) {
|
|
252
|
+
return adaptResponse(error.response);
|
|
253
|
+
}
|
|
254
|
+
throw error;
|
|
255
|
+
}
|
|
256
|
+
const backoffTime = (Math.pow(2, i)) * 1000 + Math.floor(Math.random() * 1000);
|
|
257
|
+
console.warn(`Request attempt ${i + 1} failed. Retrying in ${backoffTime}ms...`);
|
|
258
|
+
await delay(backoffTime);
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
function setProxy(proxyUrl) {
|
|
264
|
+
if (proxyUrl) {
|
|
265
|
+
try {
|
|
266
|
+
const parsedProxy = new URL(proxyUrl);
|
|
267
|
+
proxyConfig = {
|
|
268
|
+
proxy: {
|
|
269
|
+
host: parsedProxy.hostname,
|
|
270
|
+
port: parsedProxy.port,
|
|
271
|
+
protocol: parsedProxy.protocol.replace(":", ""),
|
|
272
|
+
auth: parsedProxy.username && parsedProxy.password ? {
|
|
273
|
+
username: parsedProxy.username,
|
|
274
|
+
password: parsedProxy.password,
|
|
275
|
+
} : undefined,
|
|
276
|
+
},
|
|
277
|
+
};
|
|
278
|
+
} catch (e) {
|
|
279
|
+
console.error("Invalid proxy URL. Please use a full URL format (e.g., http://user:pass@host:port).");
|
|
280
|
+
proxyConfig = {};
|
|
281
|
+
}
|
|
282
|
+
} else {
|
|
283
|
+
proxyConfig = {};
|
|
284
|
+
}
|
|
285
|
+
}
|
|
286
|
+
|
|
287
|
+
function cleanGet(url) {
|
|
288
|
+
const fn = () => client.get(url, { timeout: 60000, ...proxyConfig });
|
|
289
|
+
return requestWithRetry(fn);
|
|
290
|
+
}
|
|
291
|
+
|
|
292
|
+
async function get(url, reqJar, qs, options, ctx, customHeader) {
|
|
293
|
+
const config = {
|
|
294
|
+
headers: getHeaders(url, options, ctx, customHeader),
|
|
295
|
+
timeout: 60000,
|
|
296
|
+
params: qs,
|
|
297
|
+
...proxyConfig,
|
|
298
|
+
validateStatus: (status) => status >= 200 && status < 600,
|
|
299
|
+
};
|
|
300
|
+
const endpoint = new URL(url).pathname;
|
|
301
|
+
const threadHint = ctx && ctx.requestThreadID ? String(ctx.requestThreadID) : '';
|
|
302
|
+
return requestWithRetry(async () => await client.get(url, config), 3, endpoint, threadHint, ctx);
|
|
303
|
+
}
|
|
304
|
+
|
|
305
|
+
async function post(url, reqJar, form, options, ctx, customHeader) {
|
|
306
|
+
const headers = getHeaders(url, options, ctx, customHeader, 'xhr');
|
|
307
|
+
let data = form;
|
|
308
|
+
let contentType = headers['Content-Type'] || 'application/x-www-form-urlencoded';
|
|
309
|
+
|
|
310
|
+
if (contentType.includes('json')) {
|
|
311
|
+
data = JSON.stringify(form);
|
|
312
|
+
} else {
|
|
313
|
+
const transformedForm = new URLSearchParams();
|
|
314
|
+
for (const key in form) {
|
|
315
|
+
if (form.hasOwnProperty(key)) {
|
|
316
|
+
let value = form[key];
|
|
317
|
+
if (getType(value) === "Object") {
|
|
318
|
+
value = JSON.stringify(value);
|
|
319
|
+
}
|
|
320
|
+
transformedForm.append(key, value);
|
|
321
|
+
}
|
|
322
|
+
}
|
|
323
|
+
data = transformedForm.toString();
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
headers['Content-Type'] = contentType;
|
|
327
|
+
|
|
328
|
+
const config = {
|
|
329
|
+
headers,
|
|
330
|
+
timeout: 60000,
|
|
331
|
+
...proxyConfig,
|
|
332
|
+
validateStatus: (status) => status >= 200 && status < 600,
|
|
333
|
+
};
|
|
334
|
+
const endpoint = new URL(url).pathname;
|
|
335
|
+
const threadHint = ctx && ctx.requestThreadID ? String(ctx.requestThreadID) : '';
|
|
336
|
+
return requestWithRetry(async () => await client.post(url, data, config), 3, endpoint, threadHint, ctx);
|
|
337
|
+
}
|
|
338
|
+
|
|
339
|
+
async function postFormData(url, reqJar, form, qs, options, ctx) {
|
|
340
|
+
const formData = new FormData();
|
|
341
|
+
for (const key in form) {
|
|
342
|
+
if (form.hasOwnProperty(key)) {
|
|
343
|
+
formData.append(key, form[key]);
|
|
344
|
+
}
|
|
345
|
+
}
|
|
346
|
+
|
|
347
|
+
const customHeader = { "Content-Type": `multipart/form-data; boundary=${formData.getBoundary()}` };
|
|
348
|
+
|
|
349
|
+
const config = {
|
|
350
|
+
headers: getHeaders(url, options, ctx, customHeader, 'xhr'),
|
|
351
|
+
timeout: 60000,
|
|
352
|
+
params: qs,
|
|
353
|
+
...proxyConfig,
|
|
354
|
+
validateStatus: (status) => status >= 200 && status < 600,
|
|
355
|
+
};
|
|
356
|
+
const endpoint = new URL(url).pathname;
|
|
357
|
+
const threadHint = ctx && ctx.requestThreadID ? String(ctx.requestThreadID) : '';
|
|
358
|
+
return requestWithRetry(async () => await client.post(url, formData, config), 3, endpoint, threadHint, ctx);
|
|
359
|
+
}
|
|
360
|
+
|
|
361
|
+
module.exports = {
|
|
362
|
+
cleanGet,
|
|
363
|
+
get,
|
|
364
|
+
post,
|
|
365
|
+
postFormData,
|
|
366
|
+
getJar: () => jar,
|
|
367
|
+
setProxy,
|
|
368
|
+
};
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
|
|
3
|
+
class SimpleCache {
|
|
4
|
+
constructor(defaultTTL = 300000) { // 5 minutes default
|
|
5
|
+
this.cache = new Map();
|
|
6
|
+
this.defaultTTL = defaultTTL;
|
|
7
|
+
}
|
|
8
|
+
|
|
9
|
+
set(key, value, ttl = this.defaultTTL) {
|
|
10
|
+
const expiresAt = Date.now() + ttl;
|
|
11
|
+
this.cache.set(key, { value, expiresAt });
|
|
12
|
+
|
|
13
|
+
// Clean up expired entries occasionally
|
|
14
|
+
if (Math.random() < 0.01) { // 1% chance
|
|
15
|
+
this.cleanup();
|
|
16
|
+
}
|
|
17
|
+
}
|
|
18
|
+
|
|
19
|
+
get(key) {
|
|
20
|
+
const item = this.cache.get(key);
|
|
21
|
+
if (!item) return null;
|
|
22
|
+
|
|
23
|
+
if (Date.now() > item.expiresAt) {
|
|
24
|
+
this.cache.delete(key);
|
|
25
|
+
return null;
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
return item.value;
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
delete(key) {
|
|
32
|
+
return this.cache.delete(key);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
clear() {
|
|
36
|
+
this.cache.clear();
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
cleanup() {
|
|
40
|
+
const now = Date.now();
|
|
41
|
+
for (const [key, item] of this.cache.entries()) {
|
|
42
|
+
if (now > item.expiresAt) {
|
|
43
|
+
this.cache.delete(key);
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
size() {
|
|
49
|
+
this.cleanup();
|
|
50
|
+
return this.cache.size;
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
module.exports = SimpleCache;
|