@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.
Files changed (123) hide show
  1. package/LICENSE +3 -0
  2. package/README.md +199 -0
  3. package/examples/login-with-cookies.js +102 -0
  4. package/examples/verify.js +70 -0
  5. package/index.js +2 -0
  6. package/package.json +84 -0
  7. package/src/apis/addExternalModule.js +24 -0
  8. package/src/apis/addUserToGroup.js +108 -0
  9. package/src/apis/changeAdminStatus.js +148 -0
  10. package/src/apis/changeArchivedStatus.js +61 -0
  11. package/src/apis/changeAvatar.js +103 -0
  12. package/src/apis/changeBio.js +69 -0
  13. package/src/apis/changeBlockedStatus.js +54 -0
  14. package/src/apis/changeGroupImage.js +136 -0
  15. package/src/apis/changeThreadColor.js +116 -0
  16. package/src/apis/changeThreadEmoji.js +53 -0
  17. package/src/apis/comment.js +207 -0
  18. package/src/apis/createAITheme.js +129 -0
  19. package/src/apis/createNewGroup.js +79 -0
  20. package/src/apis/createPoll.js +73 -0
  21. package/src/apis/deleteMessage.js +44 -0
  22. package/src/apis/deleteThread.js +52 -0
  23. package/src/apis/editMessage.js +70 -0
  24. package/src/apis/emoji.js +124 -0
  25. package/src/apis/enableAutoSaveAppState.js +69 -0
  26. package/src/apis/fetchThemeData.js +113 -0
  27. package/src/apis/follow.js +81 -0
  28. package/src/apis/forwardAttachment.js +178 -0
  29. package/src/apis/forwardMessage.js +52 -0
  30. package/src/apis/friend.js +243 -0
  31. package/src/apis/gcmember.js +122 -0
  32. package/src/apis/gcname.js +123 -0
  33. package/src/apis/gcrule.js +119 -0
  34. package/src/apis/getAccess.js +111 -0
  35. package/src/apis/getBotInfo.js +88 -0
  36. package/src/apis/getBotInitialData.js +43 -0
  37. package/src/apis/getEmojiUrl.js +40 -0
  38. package/src/apis/getFriendsList.js +79 -0
  39. package/src/apis/getMessage.js +423 -0
  40. package/src/apis/getTheme.js +123 -0
  41. package/src/apis/getThemeInfo.js +116 -0
  42. package/src/apis/getThemePictures.js +87 -0
  43. package/src/apis/getThreadColors.js +119 -0
  44. package/src/apis/getThreadHistory.js +239 -0
  45. package/src/apis/getThreadInfo.js +267 -0
  46. package/src/apis/getThreadList.js +232 -0
  47. package/src/apis/getThreadPictures.js +58 -0
  48. package/src/apis/getUserID.js +117 -0
  49. package/src/apis/getUserInfo.js +513 -0
  50. package/src/apis/getUserInfoV2.js +146 -0
  51. package/src/apis/handleFriendRequest.js +66 -0
  52. package/src/apis/handleMessageRequest.js +50 -0
  53. package/src/apis/httpGet.js +63 -0
  54. package/src/apis/httpPost.js +89 -0
  55. package/src/apis/httpPostFormData.js +69 -0
  56. package/src/apis/listenMqtt.js +924 -0
  57. package/src/apis/listenSpeed.js +178 -0
  58. package/src/apis/logout.js +63 -0
  59. package/src/apis/markAsDelivered.js +47 -0
  60. package/src/apis/markAsRead.js +95 -0
  61. package/src/apis/markAsReadAll.js +40 -0
  62. package/src/apis/markAsSeen.js +70 -0
  63. package/src/apis/mqttDeltaValue.js +252 -0
  64. package/src/apis/muteThread.js +45 -0
  65. package/src/apis/nickname.js +132 -0
  66. package/src/apis/notes.js +163 -0
  67. package/src/apis/pinMessage.js +150 -0
  68. package/src/apis/produceMetaTheme.js +160 -0
  69. package/src/apis/realtime.js +182 -0
  70. package/src/apis/refreshFb_dtsg.js +94 -0
  71. package/src/apis/removeUserFromGroup.js +117 -0
  72. package/src/apis/resolvePhotoUrl.js +58 -0
  73. package/src/apis/searchForThread.js +154 -0
  74. package/src/apis/sendEffect.js +306 -0
  75. package/src/apis/sendMessage.js +353 -0
  76. package/src/apis/sendMessageMqtt.js +255 -0
  77. package/src/apis/sendTypingIndicator.js +40 -0
  78. package/src/apis/setMessageReaction.js +27 -0
  79. package/src/apis/setMessageReactionMqtt.js +61 -0
  80. package/src/apis/setPostReaction.js +118 -0
  81. package/src/apis/setThreadTheme.js +210 -0
  82. package/src/apis/setThreadThemeMqtt.js +94 -0
  83. package/src/apis/setTitle.js +26 -0
  84. package/src/apis/share.js +106 -0
  85. package/src/apis/shareContact.js +66 -0
  86. package/src/apis/stickers.js +257 -0
  87. package/src/apis/story.js +181 -0
  88. package/src/apis/theme.js +233 -0
  89. package/src/apis/unfriend.js +47 -0
  90. package/src/apis/unsendMessage.js +17 -0
  91. package/src/apis/uploadAttachment.js +87 -0
  92. package/src/database/appStateBackup.js +189 -0
  93. package/src/database/models/index.js +56 -0
  94. package/src/database/models/thread.js +31 -0
  95. package/src/database/models/user.js +32 -0
  96. package/src/database/threadData.js +101 -0
  97. package/src/database/userData.js +90 -0
  98. package/src/engine/client.js +92 -0
  99. package/src/engine/models/buildAPI.js +118 -0
  100. package/src/engine/models/loginHelper.js +492 -0
  101. package/src/engine/models/setOptions.js +88 -0
  102. package/src/types/index.d.ts +498 -0
  103. package/src/utils/antiSuspension.js +516 -0
  104. package/src/utils/auth-helpers.js +149 -0
  105. package/src/utils/autoReLogin.js +237 -0
  106. package/src/utils/axios.js +368 -0
  107. package/src/utils/cache.js +54 -0
  108. package/src/utils/clients.js +279 -0
  109. package/src/utils/constants.js +525 -0
  110. package/src/utils/formatters/data/formatAttachment.js +370 -0
  111. package/src/utils/formatters/data/formatDelta.js +109 -0
  112. package/src/utils/formatters/index.js +159 -0
  113. package/src/utils/formatters/value/formatCookie.js +91 -0
  114. package/src/utils/formatters/value/formatDate.js +36 -0
  115. package/src/utils/formatters/value/formatID.js +16 -0
  116. package/src/utils/formatters.js +1369 -0
  117. package/src/utils/headers.js +235 -0
  118. package/src/utils/index.js +152 -0
  119. package/src/utils/monitoring.js +333 -0
  120. package/src/utils/rateLimiter.js +251 -0
  121. package/src/utils/tokenRefresh.js +285 -0
  122. package/src/utils/user-agents.js +238 -0
  123. package/src/utils/validation.js +157 -0
@@ -0,0 +1,333 @@
1
+ "use strict";
2
+
3
+ const utils = require('./index');
4
+
5
+ class ProductionMonitor {
6
+ constructor() {
7
+ this.metrics = {
8
+ requests: {
9
+ total: 0,
10
+ success: 0,
11
+ failed: 0,
12
+ byEndpoint: new Map()
13
+ },
14
+ errors: {
15
+ total: 0,
16
+ byType: new Map(),
17
+ byCode: new Map(),
18
+ recent: []
19
+ },
20
+ performance: {
21
+ avgResponseTime: 0,
22
+ slowRequests: [],
23
+ requestTimes: []
24
+ },
25
+ session: {
26
+ loginTime: null,
27
+ lastActivity: null,
28
+ tokenRefreshCount: 0,
29
+ reconnectCount: 0
30
+ },
31
+ rateLimiting: {
32
+ hitCount: 0,
33
+ cooldowns: 0,
34
+ delayedRequests: 0
35
+ }
36
+ };
37
+
38
+ this.config = {
39
+ logLevel: 'info',
40
+ enableMetrics: true,
41
+ enableErrorTracking: true,
42
+ performanceThreshold: 5000,
43
+ errorRetentionCount: 100,
44
+ metricsInterval: 60000
45
+ };
46
+
47
+ this.startTime = Date.now();
48
+ this.metricsInterval = null;
49
+ }
50
+
51
+ setConfig(options) {
52
+ Object.assign(this.config, options);
53
+ }
54
+
55
+ trackRequest(endpoint, success, responseTime, error = null) {
56
+ if (!this.config.enableMetrics) return;
57
+
58
+ this.metrics.requests.total++;
59
+ if (success) {
60
+ this.metrics.requests.success++;
61
+ } else {
62
+ this.metrics.requests.failed++;
63
+ }
64
+
65
+ if (!this.metrics.requests.byEndpoint.has(endpoint)) {
66
+ this.metrics.requests.byEndpoint.set(endpoint, {
67
+ total: 0,
68
+ success: 0,
69
+ failed: 0,
70
+ avgTime: 0
71
+ });
72
+ }
73
+
74
+ const endpointStats = this.metrics.requests.byEndpoint.get(endpoint);
75
+ endpointStats.total++;
76
+ if (success) {
77
+ endpointStats.success++;
78
+ } else {
79
+ endpointStats.failed++;
80
+ }
81
+
82
+ endpointStats.avgTime =
83
+ (endpointStats.avgTime * (endpointStats.total - 1) + responseTime) / endpointStats.total;
84
+
85
+ this.trackPerformance(endpoint, responseTime);
86
+
87
+ if (error) {
88
+ this.trackError(error, endpoint);
89
+ }
90
+
91
+ this.metrics.session.lastActivity = Date.now();
92
+ }
93
+
94
+ trackPerformance(endpoint, responseTime) {
95
+ this.metrics.performance.requestTimes.push(responseTime);
96
+
97
+ if (this.metrics.performance.requestTimes.length > 1000) {
98
+ this.metrics.performance.requestTimes.shift();
99
+ }
100
+
101
+ const sum = this.metrics.performance.requestTimes.reduce((a, b) => a + b, 0);
102
+ this.metrics.performance.avgResponseTime =
103
+ sum / this.metrics.performance.requestTimes.length;
104
+
105
+ if (responseTime > this.config.performanceThreshold) {
106
+ this.metrics.performance.slowRequests.push({
107
+ endpoint,
108
+ responseTime,
109
+ timestamp: Date.now()
110
+ });
111
+
112
+ if (this.metrics.performance.slowRequests.length > 50) {
113
+ this.metrics.performance.slowRequests.shift();
114
+ }
115
+
116
+ utils.warn("Performance", `Slow request: ${endpoint} took ${responseTime}ms`);
117
+ }
118
+ }
119
+
120
+ trackError(error, context = '') {
121
+ if (!this.config.enableErrorTracking) return;
122
+
123
+ this.metrics.errors.total++;
124
+
125
+ const errorType = error.errorType || error.name || 'UnknownError';
126
+ const errorCode = error.errorCode || error.code || 'N/A';
127
+
128
+ this.metrics.errors.byType.set(
129
+ errorType,
130
+ (this.metrics.errors.byType.get(errorType) || 0) + 1
131
+ );
132
+
133
+ this.metrics.errors.byCode.set(
134
+ errorCode,
135
+ (this.metrics.errors.byCode.get(errorCode) || 0) + 1
136
+ );
137
+
138
+ this.metrics.errors.recent.push({
139
+ type: errorType,
140
+ code: errorCode,
141
+ message: error.message,
142
+ context,
143
+ timestamp: Date.now(),
144
+ stack: error.stack
145
+ });
146
+
147
+ if (this.metrics.errors.recent.length > this.config.errorRetentionCount) {
148
+ this.metrics.errors.recent.shift();
149
+ }
150
+ }
151
+
152
+ trackRateLimit(type, threadID = null) {
153
+ this.metrics.rateLimiting.hitCount++;
154
+
155
+ if (type === 'cooldown') {
156
+ this.metrics.rateLimiting.cooldowns++;
157
+ } else if (type === 'delayed') {
158
+ this.metrics.rateLimiting.delayedRequests++;
159
+ }
160
+ }
161
+
162
+ trackTokenRefresh() {
163
+ this.metrics.session.tokenRefreshCount++;
164
+ }
165
+
166
+ trackReconnect() {
167
+ this.metrics.session.reconnectCount++;
168
+ }
169
+
170
+ setLoginTime() {
171
+ this.metrics.session.loginTime = Date.now();
172
+ }
173
+
174
+ getMetrics() {
175
+ const uptime = Date.now() - this.startTime;
176
+ const sessionDuration = this.metrics.session.loginTime
177
+ ? Date.now() - this.metrics.session.loginTime
178
+ : 0;
179
+
180
+ return {
181
+ uptime,
182
+ sessionDuration,
183
+ requests: {
184
+ ...this.metrics.requests,
185
+ byEndpoint: Object.fromEntries(this.metrics.requests.byEndpoint),
186
+ successRate: this.metrics.requests.total > 0
187
+ ? (this.metrics.requests.success / this.metrics.requests.total * 100).toFixed(2) + '%'
188
+ : 'N/A'
189
+ },
190
+ errors: {
191
+ ...this.metrics.errors,
192
+ byType: Object.fromEntries(this.metrics.errors.byType),
193
+ byCode: Object.fromEntries(this.metrics.errors.byCode),
194
+ errorRate: this.metrics.requests.total > 0
195
+ ? (this.metrics.errors.total / this.metrics.requests.total * 100).toFixed(2) + '%'
196
+ : 'N/A'
197
+ },
198
+ performance: this.metrics.performance,
199
+ session: this.metrics.session,
200
+ rateLimiting: this.metrics.rateLimiting
201
+ };
202
+ }
203
+
204
+ getHealth() {
205
+ const metrics = this.getMetrics();
206
+ const health = {
207
+ status: 'healthy',
208
+ checks: {},
209
+ timestamp: Date.now()
210
+ };
211
+
212
+ const errorRate = this.metrics.requests.total > 0
213
+ ? (this.metrics.errors.total / this.metrics.requests.total) * 100
214
+ : 0;
215
+
216
+ health.checks.errorRate = {
217
+ status: errorRate < 5 ? 'pass' : errorRate < 15 ? 'warn' : 'fail',
218
+ value: errorRate.toFixed(2) + '%',
219
+ threshold: '5%'
220
+ };
221
+
222
+ health.checks.performance = {
223
+ status: this.metrics.performance.avgResponseTime < 2000 ? 'pass' :
224
+ this.metrics.performance.avgResponseTime < 5000 ? 'warn' : 'fail',
225
+ value: Math.round(this.metrics.performance.avgResponseTime) + 'ms',
226
+ threshold: '2000ms'
227
+ };
228
+
229
+ health.checks.session = {
230
+ status: this.metrics.session.loginTime ? 'pass' : 'fail',
231
+ value: this.metrics.session.loginTime ? 'active' : 'not logged in'
232
+ };
233
+
234
+ health.checks.rateLimiting = {
235
+ status: this.metrics.rateLimiting.hitCount < 100 ? 'pass' :
236
+ this.metrics.rateLimiting.hitCount < 500 ? 'warn' : 'fail',
237
+ value: this.metrics.rateLimiting.hitCount,
238
+ threshold: '100 hits'
239
+ };
240
+
241
+ const failedChecks = Object.values(health.checks).filter(c => c.status === 'fail').length;
242
+ const warnChecks = Object.values(health.checks).filter(c => c.status === 'warn').length;
243
+
244
+ if (failedChecks > 0) {
245
+ health.status = 'unhealthy';
246
+ } else if (warnChecks > 0) {
247
+ health.status = 'degraded';
248
+ }
249
+
250
+ return health;
251
+ }
252
+
253
+ reset() {
254
+ this.metrics.requests.total = 0;
255
+ this.metrics.requests.success = 0;
256
+ this.metrics.requests.failed = 0;
257
+ this.metrics.requests.byEndpoint.clear();
258
+ this.metrics.errors.total = 0;
259
+ this.metrics.errors.byType.clear();
260
+ this.metrics.errors.byCode.clear();
261
+ this.metrics.errors.recent = [];
262
+ this.metrics.performance.slowRequests = [];
263
+ this.metrics.performance.requestTimes = [];
264
+ this.metrics.rateLimiting.hitCount = 0;
265
+ this.metrics.rateLimiting.cooldowns = 0;
266
+ this.metrics.rateLimiting.delayedRequests = 0;
267
+
268
+ utils.success("Monitoring", "All metrics have been reset successfully");
269
+ }
270
+
271
+ displayHealthStatus() {
272
+ const health = this.getHealth();
273
+ console.log('\n' + '='.repeat(60));
274
+ if (health.status === 'healthy') {
275
+ console.log('SYSTEM HEALTH: HEALTHY');
276
+ } else if (health.status === 'degraded') {
277
+ console.log('SYSTEM HEALTH: DEGRADED');
278
+ } else {
279
+ console.log('SYSTEM HEALTH: UNHEALTHY');
280
+ }
281
+ console.log('='.repeat(60));
282
+ Object.entries(health.checks).forEach(([name, check]) => {
283
+ const nameFormatted = name.charAt(0).toUpperCase() + name.slice(1);
284
+ console.log(`\n${nameFormatted}:`);
285
+ console.log(` Status: ${check.status.toUpperCase()}`);
286
+ console.log(` Value: ${check.value}`);
287
+ if (check.threshold) {
288
+ console.log(` Threshold: ${check.threshold}`);
289
+ }
290
+ });
291
+ console.log('\n' + '='.repeat(60) + '\n');
292
+ return health;
293
+ }
294
+
295
+ startPeriodicReporting(interval = 60000) {
296
+ if (this.metricsInterval) {
297
+ clearInterval(this.metricsInterval);
298
+ }
299
+
300
+ this.metricsInterval = setInterval(() => {
301
+ const metrics = this.getMetrics();
302
+ console.log('\n' + '='.repeat(60));
303
+ console.log('PERFORMANCE METRICS REPORT');
304
+ console.log('='.repeat(60));
305
+ console.log('\nRequests:');
306
+ console.log(` Total: ${metrics.requests.total}`);
307
+ console.log(` Success Rate: ${metrics.requests.successRate}`);
308
+ console.log('\nErrors:');
309
+ console.log(` Total: ${metrics.errors.total}`);
310
+ console.log(` Error Rate: ${metrics.errors.errorRate}`);
311
+ console.log('\nPerformance:');
312
+ const avgTime = Math.round(metrics.performance.avgResponseTime);
313
+ console.log(` Avg Response Time: ${avgTime}ms`);
314
+ console.log('\nRate Limiting:');
315
+ console.log(` Total Hits: ${metrics.rateLimiting.hitCount}`);
316
+ console.log('='.repeat(60) + '\n');
317
+ }, interval);
318
+ }
319
+
320
+ stopPeriodicReporting() {
321
+ if (this.metricsInterval) {
322
+ clearInterval(this.metricsInterval);
323
+ this.metricsInterval = null;
324
+ }
325
+ }
326
+ }
327
+
328
+ const globalMonitor = new ProductionMonitor();
329
+
330
+ module.exports = {
331
+ ProductionMonitor,
332
+ globalMonitor
333
+ };
@@ -0,0 +1,251 @@
1
+ "use strict";
2
+
3
+ /**
4
+ * Adaptive Rate Limiting Manager
5
+ * Sliding-window per-minute and per-second rate limiting to prevent
6
+ * Facebook from flagging automated behaviour.
7
+ */
8
+
9
+ class RateLimiter {
10
+ constructor() {
11
+ this.threadCooldowns = new Map();
12
+ this.endpointCooldowns = new Map();
13
+ this.errorCache = new Map();
14
+
15
+ this.ERROR_CACHE_TTL = 300000;
16
+ this.COOLDOWN_DURATION = 60000;
17
+ this.MAX_REQUESTS_PER_MINUTE = 50;
18
+ this.MAX_CONCURRENT_REQUESTS = 5;
19
+
20
+ this.activeRequests = 0;
21
+
22
+ // Sliding window: store timestamps of recent requests
23
+ this._requestWindow = [];
24
+ this._WINDOW_MS = 60000;
25
+
26
+ // Per-endpoint sliding windows
27
+ this._endpointWindows = new Map();
28
+ this._MAX_PER_ENDPOINT_PER_MINUTE = 20;
29
+ }
30
+
31
+ configure(opts = {}) {
32
+ if (typeof opts.maxConcurrentRequests === 'number' && opts.maxConcurrentRequests > 0 && opts.maxConcurrentRequests <= 20) {
33
+ this.MAX_CONCURRENT_REQUESTS = Math.floor(opts.maxConcurrentRequests);
34
+ }
35
+ if (typeof opts.maxRequestsPerMinute === 'number' && opts.maxRequestsPerMinute > 0 && opts.maxRequestsPerMinute <= 1000) {
36
+ this.MAX_REQUESTS_PER_MINUTE = Math.floor(opts.maxRequestsPerMinute);
37
+ }
38
+ if (typeof opts.requestCooldownMs === 'number' && opts.requestCooldownMs >= 0 && opts.requestCooldownMs <= 10 * 60 * 1000) {
39
+ this.COOLDOWN_DURATION = Math.floor(opts.requestCooldownMs);
40
+ }
41
+ if (typeof opts.errorCacheTtlMs === 'number' && opts.errorCacheTtlMs >= 0 && opts.errorCacheTtlMs <= 24 * 60 * 60 * 1000) {
42
+ this.ERROR_CACHE_TTL = Math.floor(opts.errorCacheTtlMs);
43
+ }
44
+ }
45
+
46
+ // ─── Thread cooldowns ─────────────────────────────────────────────────────
47
+
48
+ isThreadOnCooldown(threadID) {
49
+ const cooldownEnd = this.threadCooldowns.get(threadID);
50
+ if (!cooldownEnd) return false;
51
+ if (Date.now() >= cooldownEnd) {
52
+ this.threadCooldowns.delete(threadID);
53
+ return false;
54
+ }
55
+ return true;
56
+ }
57
+
58
+ setThreadCooldown(threadID, duration = null) {
59
+ this.threadCooldowns.set(threadID, Date.now() + (duration || this.COOLDOWN_DURATION));
60
+ }
61
+
62
+ // ─── Endpoint cooldowns ───────────────────────────────────────────────────
63
+
64
+ isEndpointOnCooldown(endpoint) {
65
+ const cooldownEnd = this.endpointCooldowns.get(endpoint);
66
+ if (!cooldownEnd) return false;
67
+ if (Date.now() >= cooldownEnd) {
68
+ this.endpointCooldowns.delete(endpoint);
69
+ return false;
70
+ }
71
+ return true;
72
+ }
73
+
74
+ setEndpointCooldown(endpoint, duration = null) {
75
+ this.endpointCooldowns.set(endpoint, Date.now() + (duration || this.COOLDOWN_DURATION));
76
+ }
77
+
78
+ // ─── Error suppression ────────────────────────────────────────────────────
79
+
80
+ shouldSuppressError(key) {
81
+ const cachedTime = this.errorCache.get(key);
82
+ if (!cachedTime) {
83
+ this.errorCache.set(key, Date.now());
84
+ return false;
85
+ }
86
+ if (Date.now() - cachedTime > this.ERROR_CACHE_TTL) {
87
+ this.errorCache.set(key, Date.now());
88
+ return false;
89
+ }
90
+ return true;
91
+ }
92
+
93
+ // ─── Sliding-window rate checking ─────────────────────────────────────────
94
+
95
+ /**
96
+ * Prune timestamps older than the window and return the current count.
97
+ */
98
+ _pruneWindow(arr) {
99
+ const cutoff = Date.now() - this._WINDOW_MS;
100
+ let i = 0;
101
+ while (i < arr.length && arr[i] < cutoff) i++;
102
+ if (i > 0) arr.splice(0, i);
103
+ return arr.length;
104
+ }
105
+
106
+ isGloballyRateLimited() {
107
+ const count = this._pruneWindow(this._requestWindow);
108
+ return count >= this.MAX_REQUESTS_PER_MINUTE;
109
+ }
110
+
111
+ isEndpointRateLimited(endpoint) {
112
+ if (!this._endpointWindows.has(endpoint)) return false;
113
+ const count = this._pruneWindow(this._endpointWindows.get(endpoint));
114
+ return count >= this._MAX_PER_ENDPOINT_PER_MINUTE;
115
+ }
116
+
117
+ _recordRequest(endpoint) {
118
+ const now = Date.now();
119
+ this._requestWindow.push(now);
120
+ if (this._requestWindow.length > this.MAX_REQUESTS_PER_MINUTE * 2) {
121
+ this._pruneWindow(this._requestWindow);
122
+ }
123
+ if (endpoint) {
124
+ if (!this._endpointWindows.has(endpoint)) {
125
+ this._endpointWindows.set(endpoint, []);
126
+ }
127
+ const ew = this._endpointWindows.get(endpoint);
128
+ ew.push(now);
129
+ if (ew.length > this._MAX_PER_ENDPOINT_PER_MINUTE * 2) {
130
+ this._pruneWindow(ew);
131
+ }
132
+ }
133
+ }
134
+
135
+ // ─── Adaptive delay ───────────────────────────────────────────────────────
136
+
137
+ getAdaptiveDelay(retryCount, errorCode = null) {
138
+ const baseDelays = [2000, 5000, 10000, 20000];
139
+ const base = baseDelays[Math.min(retryCount, baseDelays.length - 1)];
140
+
141
+ if (errorCode === 1545012 || errorCode === 1675004) {
142
+ return base * 2;
143
+ }
144
+ if (errorCode === 368 || errorCode === 10) {
145
+ return base * 3;
146
+ }
147
+ return base;
148
+ }
149
+
150
+ async addHumanizedDelay(min = 150, max = 450) {
151
+ const delay = Math.floor(Math.random() * (max - min + 1)) + min;
152
+ await new Promise(resolve => setTimeout(resolve, delay));
153
+ }
154
+
155
+ /**
156
+ * Check global and concurrent rate limits.
157
+ * Will wait until below limit, then record the request.
158
+ */
159
+ async checkRateLimit(skipHumanDelay = false, endpoint = null) {
160
+ // Wait for concurrent slot
161
+ while (this.activeRequests >= this.MAX_CONCURRENT_REQUESTS) {
162
+ await new Promise(resolve => setTimeout(resolve, 100));
163
+ }
164
+
165
+ // Wait for per-minute global window to clear
166
+ let waitCycles = 0;
167
+ while (this.isGloballyRateLimited()) {
168
+ if (waitCycles++ > 60) break;
169
+ await new Promise(resolve => setTimeout(resolve, 1000));
170
+ }
171
+
172
+ // Wait for per-endpoint window to clear
173
+ if (endpoint) {
174
+ let epCycles = 0;
175
+ while (this.isEndpointRateLimited(endpoint)) {
176
+ if (epCycles++ > 30) break;
177
+ await new Promise(resolve => setTimeout(resolve, 1000));
178
+ }
179
+ }
180
+
181
+ if (!skipHumanDelay) {
182
+ await this.addHumanizedDelay();
183
+ }
184
+
185
+ this.activeRequests++;
186
+ this._recordRequest(endpoint);
187
+
188
+ setTimeout(() => {
189
+ this.activeRequests = Math.max(0, this.activeRequests - 1);
190
+ }, 1000);
191
+ }
192
+
193
+ // ─── Cleanup ──────────────────────────────────────────────────────────────
194
+
195
+ cleanup() {
196
+ const now = Date.now();
197
+
198
+ for (const [key, time] of this.errorCache.entries()) {
199
+ if (now - time > this.ERROR_CACHE_TTL) this.errorCache.delete(key);
200
+ }
201
+ for (const [key, time] of this.threadCooldowns.entries()) {
202
+ if (now >= time) this.threadCooldowns.delete(key);
203
+ }
204
+ for (const [key, time] of this.endpointCooldowns.entries()) {
205
+ if (now >= time) this.endpointCooldowns.delete(key);
206
+ }
207
+
208
+ // Prune all endpoint windows
209
+ for (const [key, arr] of this._endpointWindows.entries()) {
210
+ this._pruneWindow(arr);
211
+ if (arr.length === 0) this._endpointWindows.delete(key);
212
+ }
213
+ this._pruneWindow(this._requestWindow);
214
+ }
215
+
216
+ getStats() {
217
+ this._pruneWindow(this._requestWindow);
218
+ return {
219
+ activeRequests: this.activeRequests,
220
+ maxConcurrentRequests: this.MAX_CONCURRENT_REQUESTS,
221
+ maxRequestsPerMinute: this.MAX_REQUESTS_PER_MINUTE,
222
+ requestsInLastMinute: this._requestWindow.length,
223
+ threadCooldowns: this.threadCooldowns.size,
224
+ endpointCooldowns: this.endpointCooldowns.size,
225
+ errorCacheSize: this.errorCache.size
226
+ };
227
+ }
228
+
229
+ getCooldownRemaining(threadID) {
230
+ const cooldownEnd = this.threadCooldowns.get(threadID);
231
+ if (!cooldownEnd) return 0;
232
+ return Math.max(0, cooldownEnd - Date.now());
233
+ }
234
+
235
+ getEndpointCooldownRemaining(endpoint) {
236
+ const cooldownEnd = this.endpointCooldowns.get(endpoint);
237
+ if (!cooldownEnd) return 0;
238
+ return Math.max(0, cooldownEnd - Date.now());
239
+ }
240
+ }
241
+
242
+ const globalRateLimiter = new RateLimiter();
243
+
244
+ setInterval(() => globalRateLimiter.cleanup(), 60000);
245
+
246
+ module.exports = {
247
+ RateLimiter,
248
+ globalRateLimiter,
249
+ configureRateLimiter: (opts) => globalRateLimiter.configure(opts),
250
+ getRateLimiterStats: () => globalRateLimiter.getStats()
251
+ };