@ikyyjee/ikyysinggle 1.7.7
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/WAProto/GenerateStatics.sh +3 -0
- package/WAProto/WAProto.proto +8083 -0
- package/WAProto/fix-imports.js +85 -0
- package/WAProto/index.d.ts +29095 -0
- package/WAProto/index.js +172336 -0
- package/engine-requirements.js +10 -0
- package/lib/Defaults/index.js +194 -0
- package/lib/Signal/Group/ciphertext-message.js +12 -0
- package/lib/Signal/Group/group-session-builder.js +30 -0
- package/lib/Signal/Group/group_cipher.js +82 -0
- package/lib/Signal/Group/index.js +12 -0
- package/lib/Signal/Group/keyhelper.js +18 -0
- package/lib/Signal/Group/sender-chain-key.js +26 -0
- package/lib/Signal/Group/sender-key-distribution-message.js +63 -0
- package/lib/Signal/Group/sender-key-message.js +66 -0
- package/lib/Signal/Group/sender-key-name.js +48 -0
- package/lib/Signal/Group/sender-key-record.js +41 -0
- package/lib/Signal/Group/sender-key-state.js +84 -0
- package/lib/Signal/Group/sender-message-key.js +26 -0
- package/lib/Signal/libsignal.js +431 -0
- package/lib/Signal/lid-mapping.js +277 -0
- package/lib/Socket/Client/index.js +3 -0
- package/lib/Socket/Client/types.js +11 -0
- package/lib/Socket/Client/websocket.js +102 -0
- package/lib/Socket/aigroups.js +221 -0
- package/lib/Socket/business.js +379 -0
- package/lib/Socket/chats.js +1193 -0
- package/lib/Socket/communities.js +431 -0
- package/lib/Socket/graphql.js +524 -0
- package/lib/Socket/groups.js +408 -0
- package/lib/Socket/index.js +49 -0
- package/lib/Socket/interop.js +341 -0
- package/lib/Socket/luxu.js +510 -0
- package/lib/Socket/managed-account.js +99 -0
- package/lib/Socket/messages-recv.js +2009 -0
- package/lib/Socket/messages-send.js +1608 -0
- package/lib/Socket/mex.js +41 -0
- package/lib/Socket/newsletter.js +399 -0
- package/lib/Socket/privacy.js +128 -0
- package/lib/Socket/registration.js +238 -0
- package/lib/Socket/socket.js +1000 -0
- package/lib/Socket/text-router.js +67 -0
- package/lib/Socket/username.js +234 -0
- package/lib/Store/index.js +10 -0
- package/lib/Store/keyed-db.js +108 -0
- package/lib/Store/make-cache-manager-store.js +85 -0
- package/lib/Store/make-in-memory-store.js +198 -0
- package/lib/Store/make-ordered-dictionary.js +75 -0
- package/lib/Store/object-repository.js +32 -0
- package/lib/Types/Auth.js +2 -0
- package/lib/Types/Bussines.js +2 -0
- package/lib/Types/Call.js +2 -0
- package/lib/Types/Chat.js +8 -0
- package/lib/Types/Contact.js +2 -0
- package/lib/Types/Events.js +2 -0
- package/lib/Types/GroupMetadata.js +2 -0
- package/lib/Types/Label.js +25 -0
- package/lib/Types/LabelAssociation.js +7 -0
- package/lib/Types/Message.js +11 -0
- package/lib/Types/Mex.js +114 -0
- package/lib/Types/Product.js +2 -0
- package/lib/Types/Signal.js +2 -0
- package/lib/Types/Socket.js +3 -0
- package/lib/Types/State.js +56 -0
- package/lib/Types/USync.js +2 -0
- package/lib/Types/index.js +26 -0
- package/lib/Utils/adaptive-healing.js +53 -0
- package/lib/Utils/auth-utils.js +302 -0
- package/lib/Utils/browser-utils.js +50 -0
- package/lib/Utils/business.js +231 -0
- package/lib/Utils/chat-utils.js +872 -0
- package/lib/Utils/command-loader.js +108 -0
- package/lib/Utils/companion-reg-client-utils.js +35 -0
- package/lib/Utils/consumer-application.js +106 -0
- package/lib/Utils/crypto.js +137 -0
- package/lib/Utils/curve25519-js.js +262 -0
- package/lib/Utils/decode-wa-message.js +498 -0
- package/lib/Utils/event-buffer.js +622 -0
- package/lib/Utils/generics.js +403 -0
- package/lib/Utils/group-history.js +47 -0
- package/lib/Utils/history.js +134 -0
- package/lib/Utils/identity-change-handler.js +50 -0
- package/lib/Utils/index.js +38 -0
- package/lib/Utils/jid-display-normalization.js +198 -0
- package/lib/Utils/link-preview.js +85 -0
- package/lib/Utils/logger.js +3 -0
- package/lib/Utils/lt-hash.js +8 -0
- package/lib/Utils/make-mutex.js +33 -0
- package/lib/Utils/message-composer.js +273 -0
- package/lib/Utils/message-retry-manager.js +267 -0
- package/lib/Utils/messages-media.js +791 -0
- package/lib/Utils/messages.js +1260 -0
- package/lib/Utils/meta-ai-msmsg.js +271 -0
- package/lib/Utils/native-bridge.js +77 -0
- package/lib/Utils/noise-handler.js +201 -0
- package/lib/Utils/offline-node-processor.js +40 -0
- package/lib/Utils/optimizer.js +90 -0
- package/lib/Utils/pre-key-manager.js +106 -0
- package/lib/Utils/process-message.js +630 -0
- package/lib/Utils/reporting-utils.js +258 -0
- package/lib/Utils/session-pool.js +73 -0
- package/lib/Utils/signal.js +207 -0
- package/lib/Utils/stanza-ack.js +38 -0
- package/lib/Utils/sticker.js +139 -0
- package/lib/Utils/sync-action-utils.js +49 -0
- package/lib/Utils/tc-token-utils.js +163 -0
- package/lib/Utils/use-multi-file-auth-state.js +121 -0
- package/lib/Utils/use-sqlite-auth-state.js +168 -0
- package/lib/Utils/validate-connection.js +203 -0
- package/lib/Utils/view-once-cache.js +79 -0
- package/lib/Utils/voip-rekey.js +25 -0
- package/lib/Utils/warmup.js +117 -0
- package/lib/WABinary/constants.js +1301 -0
- package/lib/WABinary/decode.js +262 -0
- package/lib/WABinary/encode.js +220 -0
- package/lib/WABinary/generic-utils.js +204 -0
- package/lib/WABinary/index.js +6 -0
- package/lib/WABinary/jid-utils.js +98 -0
- package/lib/WABinary/types.js +2 -0
- package/lib/WAM/BinaryInfo.js +10 -0
- package/lib/WAM/constants.js +22853 -0
- package/lib/WAM/encode.js +150 -0
- package/lib/WAM/index.js +4 -0
- package/lib/WAUSync/Protocols/USyncBusinessProtocol.js +41 -0
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +52 -0
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +54 -0
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +27 -0
- package/lib/WAUSync/Protocols/USyncFeatureProtocol.js +52 -0
- package/lib/WAUSync/Protocols/USyncPictureProtocol.js +31 -0
- package/lib/WAUSync/Protocols/USyncSidelistProtocol.js +26 -0
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +38 -0
- package/lib/WAUSync/Protocols/USyncTextStatusProtocol.js +35 -0
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +25 -0
- package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +51 -0
- package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +29 -0
- package/lib/WAUSync/Protocols/index.js +13 -0
- package/lib/WAUSync/USyncQuery.js +127 -0
- package/lib/WAUSync/USyncUser.js +31 -0
- package/lib/WAUSync/index.js +4 -0
- package/lib/antiban.js +4083 -0
- package/lib/index.js +24 -0
- package/package.json +147 -0
package/lib/antiban.js
ADDED
|
@@ -0,0 +1,4083 @@
|
|
|
1
|
+
import { createRequire } from 'node:module';
|
|
2
|
+
import * as fs from 'node:fs';
|
|
3
|
+
import * as fs2 from 'node:fs';
|
|
4
|
+
import * as path from 'node:path';
|
|
5
|
+
import * as import_events from 'node:events';
|
|
6
|
+
import * as _fsPromises from 'node:fs/promises';
|
|
7
|
+
import * as _fsSync from 'node:fs';
|
|
8
|
+
import * as _path3 from 'node:path';
|
|
9
|
+
import * as import_fs from 'node:fs';
|
|
10
|
+
import * as path2 from 'node:path';
|
|
11
|
+
|
|
12
|
+
const require2 = createRequire(import.meta.url);
|
|
13
|
+
|
|
14
|
+
var TIME_CONSTANTS = {
|
|
15
|
+
MS_PER_SECOND: 1e3,
|
|
16
|
+
MS_PER_MINUTE: 6e4,
|
|
17
|
+
MS_PER_HOUR: 36e5,
|
|
18
|
+
MS_PER_DAY: 864e5,
|
|
19
|
+
BURST_RESET_MS: 3e4,
|
|
20
|
+
IDENTICAL_WINDOW_MS: 36e5
|
|
21
|
+
};
|
|
22
|
+
var DEFAULT_CONFIG = {
|
|
23
|
+
maxPerMinute: 8,
|
|
24
|
+
maxPerHour: 200,
|
|
25
|
+
maxPerDay: 1500,
|
|
26
|
+
minDelayMs: 1500,
|
|
27
|
+
maxDelayMs: 5e3,
|
|
28
|
+
newChatDelayMs: 3e3,
|
|
29
|
+
maxIdenticalMessages: 3,
|
|
30
|
+
burstAllowance: 3,
|
|
31
|
+
identicalMessageWindowMs: TIME_CONSTANTS.IDENTICAL_WINDOW_MS
|
|
32
|
+
};
|
|
33
|
+
var RateLimiter = class {
|
|
34
|
+
config;
|
|
35
|
+
messages = [];
|
|
36
|
+
identicalCount = new Map();
|
|
37
|
+
knownChats = new Set();
|
|
38
|
+
burstCount = 0;
|
|
39
|
+
lastMessageTime = 0;
|
|
40
|
+
constructor(config = {}) {
|
|
41
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
42
|
+
}
|
|
43
|
+
async getDelay(recipient, content) {
|
|
44
|
+
const now = Date.now();
|
|
45
|
+
this.cleanup(now);
|
|
46
|
+
const contentHash = this.hashContent(content);
|
|
47
|
+
const dayMessages = this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_DAY);
|
|
48
|
+
if (dayMessages.length >= this.config.maxPerDay) {
|
|
49
|
+
return -1;
|
|
50
|
+
}
|
|
51
|
+
const hourMessages = this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_HOUR);
|
|
52
|
+
if (hourMessages.length >= this.config.maxPerHour) {
|
|
53
|
+
hourMessages.sort((a, b) => a.timestamp - b.timestamp);
|
|
54
|
+
const oldestInHour = hourMessages[0];
|
|
55
|
+
const delay2 = oldestInHour
|
|
56
|
+
? oldestInHour.timestamp + TIME_CONSTANTS.MS_PER_HOUR - now
|
|
57
|
+
: TIME_CONSTANTS.MS_PER_HOUR;
|
|
58
|
+
return Math.max(delay2, TIME_CONSTANTS.MS_PER_MINUTE);
|
|
59
|
+
}
|
|
60
|
+
const minuteMessages = this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_MINUTE);
|
|
61
|
+
if (minuteMessages.length >= this.config.maxPerMinute) {
|
|
62
|
+
minuteMessages.sort((a, b) => a.timestamp - b.timestamp);
|
|
63
|
+
const oldestInMinute = minuteMessages[0];
|
|
64
|
+
const delay2 = oldestInMinute
|
|
65
|
+
? oldestInMinute.timestamp + TIME_CONSTANTS.MS_PER_MINUTE - now
|
|
66
|
+
: TIME_CONSTANTS.MS_PER_MINUTE;
|
|
67
|
+
return Math.max(delay2, TIME_CONSTANTS.MS_PER_SECOND);
|
|
68
|
+
}
|
|
69
|
+
const tracker = this.identicalCount.get(contentHash);
|
|
70
|
+
if (tracker) {
|
|
71
|
+
if (now - tracker.firstSeen < this.config.identicalMessageWindowMs) {
|
|
72
|
+
if (tracker.count >= this.config.maxIdenticalMessages) {
|
|
73
|
+
return -1;
|
|
74
|
+
}
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
let delay = 0;
|
|
78
|
+
if (this.burstCount < this.config.burstAllowance) {
|
|
79
|
+
this.burstCount++;
|
|
80
|
+
delay = this.jitter(this.config.minDelayMs * 0.5, this.config.minDelayMs);
|
|
81
|
+
}
|
|
82
|
+
else {
|
|
83
|
+
delay = this.jitter(this.config.minDelayMs, this.config.maxDelayMs);
|
|
84
|
+
}
|
|
85
|
+
const isInterop = recipient.endsWith('@interop');
|
|
86
|
+
if (!this.knownChats.has(recipient) || isInterop) {
|
|
87
|
+
delay += this.jitter(this.config.newChatDelayMs * 0.5, this.config.newChatDelayMs);
|
|
88
|
+
}
|
|
89
|
+
const timeSinceLast = now - this.lastMessageTime;
|
|
90
|
+
if (timeSinceLast < this.config.minDelayMs) {
|
|
91
|
+
delay = Math.max(delay, this.config.minDelayMs - timeSinceLast);
|
|
92
|
+
}
|
|
93
|
+
const typingDelay = Math.min(content.length * 30, 3e3);
|
|
94
|
+
delay += this.jitter(typingDelay * 0.5, typingDelay);
|
|
95
|
+
return Math.round(delay);
|
|
96
|
+
}
|
|
97
|
+
record(recipient, content) {
|
|
98
|
+
const now = Date.now();
|
|
99
|
+
const contentHash = this.hashContent(content);
|
|
100
|
+
const timeSinceLast = now - this.lastMessageTime;
|
|
101
|
+
if (timeSinceLast > TIME_CONSTANTS.BURST_RESET_MS) {
|
|
102
|
+
this.burstCount = 0;
|
|
103
|
+
}
|
|
104
|
+
this.messages.push({ timestamp: now, recipient, contentHash });
|
|
105
|
+
this.knownChats.add(recipient);
|
|
106
|
+
this.lastMessageTime = now;
|
|
107
|
+
const tracker = this.identicalCount.get(contentHash);
|
|
108
|
+
if (tracker) {
|
|
109
|
+
if (now - tracker.firstSeen < this.config.identicalMessageWindowMs) {
|
|
110
|
+
tracker.count++;
|
|
111
|
+
tracker.lastSeen = now;
|
|
112
|
+
}
|
|
113
|
+
else {
|
|
114
|
+
this.identicalCount.set(contentHash, { count: 1, firstSeen: now, lastSeen: now });
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
else {
|
|
118
|
+
this.identicalCount.set(contentHash, { count: 1, firstSeen: now, lastSeen: now });
|
|
119
|
+
}
|
|
120
|
+
}
|
|
121
|
+
getStats() {
|
|
122
|
+
const now = Date.now();
|
|
123
|
+
this.cleanup(now);
|
|
124
|
+
return {
|
|
125
|
+
lastMinute: this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_MINUTE).length,
|
|
126
|
+
lastHour: this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_HOUR).length,
|
|
127
|
+
lastDay: this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_DAY).length,
|
|
128
|
+
limits: {
|
|
129
|
+
perMinute: this.config.maxPerMinute,
|
|
130
|
+
perHour: this.config.maxPerHour,
|
|
131
|
+
perDay: this.config.maxPerDay
|
|
132
|
+
},
|
|
133
|
+
knownChats: this.knownChats.size
|
|
134
|
+
};
|
|
135
|
+
}
|
|
136
|
+
getKnownChats() {
|
|
137
|
+
return this.knownChats;
|
|
138
|
+
}
|
|
139
|
+
restoreKnownChats(chats) {
|
|
140
|
+
for (const jid of chats) {
|
|
141
|
+
this.knownChats.add(jid);
|
|
142
|
+
}
|
|
143
|
+
}
|
|
144
|
+
cleanup(now) {
|
|
145
|
+
this.messages = this.messages.filter(m => now - m.timestamp < TIME_CONSTANTS.MS_PER_DAY);
|
|
146
|
+
for (const [hash, tracker] of this.identicalCount.entries()) {
|
|
147
|
+
if (now - tracker.lastSeen > this.config.identicalMessageWindowMs) {
|
|
148
|
+
this.identicalCount.delete(hash);
|
|
149
|
+
}
|
|
150
|
+
}
|
|
151
|
+
}
|
|
152
|
+
jitter(min, max) {
|
|
153
|
+
const u1 = Math.random();
|
|
154
|
+
const u2 = Math.random();
|
|
155
|
+
const normal = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
|
156
|
+
const normalized = (normal + 3) / 6;
|
|
157
|
+
const clamped = Math.max(0, Math.min(1, normalized));
|
|
158
|
+
return Math.round(min + clamped * (max - min));
|
|
159
|
+
}
|
|
160
|
+
hashContent(content) {
|
|
161
|
+
let hash = 0;
|
|
162
|
+
for (let i = 0; i < content.length; i++) {
|
|
163
|
+
const char = content.charCodeAt(i);
|
|
164
|
+
hash = (hash << 5) - hash + char;
|
|
165
|
+
hash |= 0;
|
|
166
|
+
}
|
|
167
|
+
return hash.toString(36);
|
|
168
|
+
}
|
|
169
|
+
};
|
|
170
|
+
var DEFAULT_CONFIG2 = {
|
|
171
|
+
warmUpDays: 7,
|
|
172
|
+
day1Limit: 20,
|
|
173
|
+
growthFactor: 1.8,
|
|
174
|
+
inactivityThresholdHours: 72
|
|
175
|
+
};
|
|
176
|
+
var WarmUp = class {
|
|
177
|
+
config;
|
|
178
|
+
state;
|
|
179
|
+
constructor(config = {}, existingState) {
|
|
180
|
+
this.config = { ...DEFAULT_CONFIG2, ...config };
|
|
181
|
+
this.state = existingState || this.freshState();
|
|
182
|
+
}
|
|
183
|
+
getDailyLimit() {
|
|
184
|
+
if (this.state.graduated)
|
|
185
|
+
return Infinity;
|
|
186
|
+
const day = this.getCurrentDay();
|
|
187
|
+
if (day >= this.config.warmUpDays) {
|
|
188
|
+
this.state.graduated = true;
|
|
189
|
+
return Infinity;
|
|
190
|
+
}
|
|
191
|
+
return Math.round(this.config.day1Limit * Math.pow(this.config.growthFactor, day));
|
|
192
|
+
}
|
|
193
|
+
canSend() {
|
|
194
|
+
this.checkInactivity();
|
|
195
|
+
if (this.state.graduated)
|
|
196
|
+
return true;
|
|
197
|
+
const day = this.getCurrentDay();
|
|
198
|
+
const todayCount = this.state.dailyCounts[day] || 0;
|
|
199
|
+
return todayCount < this.getDailyLimit();
|
|
200
|
+
}
|
|
201
|
+
record() {
|
|
202
|
+
const now = Date.now();
|
|
203
|
+
const day = this.getCurrentDay();
|
|
204
|
+
while (this.state.dailyCounts.length <= day) {
|
|
205
|
+
this.state.dailyCounts.push(0);
|
|
206
|
+
}
|
|
207
|
+
this.state.dailyCounts[day]++;
|
|
208
|
+
this.state.lastActiveAt = now;
|
|
209
|
+
}
|
|
210
|
+
getStatus() {
|
|
211
|
+
const day = this.getCurrentDay();
|
|
212
|
+
const todaySent = this.state.dailyCounts[day] || 0;
|
|
213
|
+
const limit = this.getDailyLimit();
|
|
214
|
+
return {
|
|
215
|
+
phase: this.state.graduated ? 'graduated' : 'warming',
|
|
216
|
+
day: Math.min(day + 1, this.config.warmUpDays),
|
|
217
|
+
totalDays: this.config.warmUpDays,
|
|
218
|
+
todayLimit: limit === Infinity ? -1 : limit,
|
|
219
|
+
todaySent,
|
|
220
|
+
progress: this.state.graduated ? 100 : Math.round((day / this.config.warmUpDays) * 100)
|
|
221
|
+
};
|
|
222
|
+
}
|
|
223
|
+
exportState() {
|
|
224
|
+
return { ...this.state };
|
|
225
|
+
}
|
|
226
|
+
reset() {
|
|
227
|
+
this.state = this.freshState();
|
|
228
|
+
}
|
|
229
|
+
getCurrentDay() {
|
|
230
|
+
return Math.floor((Date.now() - this.state.startedAt) / 864e5);
|
|
231
|
+
}
|
|
232
|
+
checkInactivity() {
|
|
233
|
+
const hoursSinceActive = (Date.now() - this.state.lastActiveAt) / 36e5;
|
|
234
|
+
if (hoursSinceActive > this.config.inactivityThresholdHours && this.state.graduated) {
|
|
235
|
+
this.state = this.freshState();
|
|
236
|
+
this.state.graduated = false;
|
|
237
|
+
}
|
|
238
|
+
}
|
|
239
|
+
freshState() {
|
|
240
|
+
const now = Date.now();
|
|
241
|
+
return {
|
|
242
|
+
startedAt: now,
|
|
243
|
+
lastActiveAt: now,
|
|
244
|
+
dailyCounts: [],
|
|
245
|
+
graduated: false
|
|
246
|
+
};
|
|
247
|
+
}
|
|
248
|
+
};
|
|
249
|
+
var DEFAULT_CONFIG3 = {
|
|
250
|
+
disconnectWarningThreshold: 3,
|
|
251
|
+
disconnectCriticalThreshold: 5,
|
|
252
|
+
failedMessageThreshold: 5,
|
|
253
|
+
autoPauseAt: 'high'
|
|
254
|
+
};
|
|
255
|
+
var HealthMonitor = class {
|
|
256
|
+
config;
|
|
257
|
+
events = [];
|
|
258
|
+
startTime = Date.now();
|
|
259
|
+
paused = false;
|
|
260
|
+
lastRisk = 'low';
|
|
261
|
+
lastBadEventTime = Date.now();
|
|
262
|
+
lastEventWasSevere = false;
|
|
263
|
+
constructor(config = {}) {
|
|
264
|
+
this.config = { ...DEFAULT_CONFIG3, ...config };
|
|
265
|
+
}
|
|
266
|
+
recordDisconnect(reason) {
|
|
267
|
+
const reasonStr = String(reason);
|
|
268
|
+
if (reasonStr === '403' || reasonStr === 'forbidden') {
|
|
269
|
+
this.events.push({ type: 'forbidden', timestamp: Date.now(), detail: reasonStr });
|
|
270
|
+
this.lastBadEventTime = Date.now();
|
|
271
|
+
this.lastEventWasSevere = true;
|
|
272
|
+
}
|
|
273
|
+
else if (reasonStr === '401' || reasonStr === 'loggedOut') {
|
|
274
|
+
this.events.push({ type: 'loggedOut', timestamp: Date.now(), detail: reasonStr });
|
|
275
|
+
this.lastBadEventTime = Date.now();
|
|
276
|
+
this.lastEventWasSevere = true;
|
|
277
|
+
}
|
|
278
|
+
else if (reasonStr === '463') {
|
|
279
|
+
this.events.push({ type: 'reachoutTimelocked', timestamp: Date.now(), detail: reasonStr });
|
|
280
|
+
this.lastBadEventTime = Date.now();
|
|
281
|
+
this.lastEventWasSevere = false;
|
|
282
|
+
}
|
|
283
|
+
else {
|
|
284
|
+
this.events.push({ type: 'disconnect', timestamp: Date.now(), detail: reasonStr });
|
|
285
|
+
this.lastBadEventTime = Date.now();
|
|
286
|
+
this.lastEventWasSevere = false;
|
|
287
|
+
}
|
|
288
|
+
this.checkAndNotify();
|
|
289
|
+
}
|
|
290
|
+
recordReconnect() {
|
|
291
|
+
this.events.push({ type: 'reconnect', timestamp: Date.now() });
|
|
292
|
+
}
|
|
293
|
+
recordMessageFailed(error) {
|
|
294
|
+
this.events.push({ type: 'messageFailed', timestamp: Date.now(), detail: error });
|
|
295
|
+
this.lastBadEventTime = Date.now();
|
|
296
|
+
this.lastEventWasSevere = false;
|
|
297
|
+
this.checkAndNotify();
|
|
298
|
+
}
|
|
299
|
+
recordReachoutTimelock(detail) {
|
|
300
|
+
this.events.push({ type: 'reachoutTimelocked', timestamp: Date.now(), detail });
|
|
301
|
+
this.lastBadEventTime = Date.now();
|
|
302
|
+
this.lastEventWasSevere = false;
|
|
303
|
+
this.checkAndNotify();
|
|
304
|
+
}
|
|
305
|
+
getStatus() {
|
|
306
|
+
const now = Date.now();
|
|
307
|
+
this.cleanup(now);
|
|
308
|
+
const hourEvents = this.events.filter(e => now - e.timestamp < 36e5);
|
|
309
|
+
const disconnects = hourEvents.filter(e => e.type === 'disconnect').length;
|
|
310
|
+
const forbidden = hourEvents.filter(e => e.type === 'forbidden').length;
|
|
311
|
+
const loggedOut = hourEvents.filter(e => e.type === 'loggedOut').length;
|
|
312
|
+
const failedMessages = hourEvents.filter(e => e.type === 'messageFailed').length;
|
|
313
|
+
let score = 0;
|
|
314
|
+
const reasons = [];
|
|
315
|
+
if (forbidden > 0) {
|
|
316
|
+
score += 40 * forbidden;
|
|
317
|
+
reasons.push(`${forbidden} forbidden (403) error${forbidden > 1 ? 's' : ''} in last hour`);
|
|
318
|
+
}
|
|
319
|
+
if (loggedOut > 0) {
|
|
320
|
+
score += 60;
|
|
321
|
+
reasons.push('Logged out by WhatsApp \u2014 possible temporary ban');
|
|
322
|
+
}
|
|
323
|
+
const timelocked = hourEvents.filter(e => e.type === 'reachoutTimelocked').length;
|
|
324
|
+
if (timelocked > 0) {
|
|
325
|
+
score += 25;
|
|
326
|
+
reasons.push(`${timelocked} reachout timelock (463) error${timelocked > 1 ? 's' : ''} in last hour`);
|
|
327
|
+
}
|
|
328
|
+
if (disconnects >= this.config.disconnectCriticalThreshold) {
|
|
329
|
+
score += 30;
|
|
330
|
+
reasons.push(`${disconnects} disconnects in last hour (critical threshold)`);
|
|
331
|
+
}
|
|
332
|
+
else if (disconnects >= this.config.disconnectWarningThreshold) {
|
|
333
|
+
score += 30;
|
|
334
|
+
reasons.push(`${disconnects} disconnects in last hour`);
|
|
335
|
+
}
|
|
336
|
+
if (failedMessages >= this.config.failedMessageThreshold) {
|
|
337
|
+
score += 20;
|
|
338
|
+
reasons.push(`${failedMessages} failed messages in last hour`);
|
|
339
|
+
}
|
|
340
|
+
score = Math.min(100, score);
|
|
341
|
+
const minutesSinceLastBad = (now - this.lastBadEventTime) / 6e4;
|
|
342
|
+
const decayRate = this.lastEventWasSevere ? 2 : 5;
|
|
343
|
+
score = Math.max(0, score - Math.floor(minutesSinceLastBad * decayRate));
|
|
344
|
+
let risk;
|
|
345
|
+
if (score >= 80)
|
|
346
|
+
risk = 'critical';
|
|
347
|
+
else if (score >= 40)
|
|
348
|
+
risk = 'high';
|
|
349
|
+
else if (score >= 15)
|
|
350
|
+
risk = 'medium';
|
|
351
|
+
else
|
|
352
|
+
risk = 'low';
|
|
353
|
+
let recommendation;
|
|
354
|
+
switch (risk) {
|
|
355
|
+
case 'critical':
|
|
356
|
+
recommendation = 'STOP ALL MESSAGING IMMEDIATELY. Disconnect and wait 24-48 hours before reconnecting.';
|
|
357
|
+
break;
|
|
358
|
+
case 'high':
|
|
359
|
+
recommendation = 'Reduce messaging rate by 80%. Consider pausing for 1-2 hours.';
|
|
360
|
+
break;
|
|
361
|
+
case 'medium':
|
|
362
|
+
recommendation = 'Reduce messaging rate by 50%. Increase delays between messages.';
|
|
363
|
+
break;
|
|
364
|
+
default:
|
|
365
|
+
recommendation = 'Operating normally. Continue monitoring.';
|
|
366
|
+
}
|
|
367
|
+
const lastDisconnect = [...this.events]
|
|
368
|
+
.reverse()
|
|
369
|
+
.find(e => e.type === 'disconnect' || e.type === 'forbidden' || e.type === 'loggedOut');
|
|
370
|
+
return {
|
|
371
|
+
risk,
|
|
372
|
+
score,
|
|
373
|
+
reasons: reasons.length ? reasons : ['No issues detected'],
|
|
374
|
+
recommendation,
|
|
375
|
+
stats: {
|
|
376
|
+
disconnectsLastHour: disconnects,
|
|
377
|
+
failedMessagesLastHour: failedMessages,
|
|
378
|
+
forbiddenErrors: forbidden,
|
|
379
|
+
timelockErrors: timelocked,
|
|
380
|
+
uptimeMs: now - this.startTime,
|
|
381
|
+
lastDisconnectReason: lastDisconnect?.detail
|
|
382
|
+
}
|
|
383
|
+
};
|
|
384
|
+
}
|
|
385
|
+
isPaused() {
|
|
386
|
+
if (this.paused)
|
|
387
|
+
return true;
|
|
388
|
+
const status = this.getStatus();
|
|
389
|
+
const riskOrder = ['low', 'medium', 'high', 'critical'];
|
|
390
|
+
return riskOrder.indexOf(status.risk) >= riskOrder.indexOf(this.config.autoPauseAt);
|
|
391
|
+
}
|
|
392
|
+
setPaused(paused) {
|
|
393
|
+
this.paused = paused;
|
|
394
|
+
}
|
|
395
|
+
reset() {
|
|
396
|
+
this.events = [];
|
|
397
|
+
this.startTime = Date.now();
|
|
398
|
+
this.paused = false;
|
|
399
|
+
this.lastRisk = 'low';
|
|
400
|
+
this.lastBadEventTime = Date.now();
|
|
401
|
+
this.lastEventWasSevere = false;
|
|
402
|
+
}
|
|
403
|
+
cleanup(now) {
|
|
404
|
+
this.events = this.events.filter(e => now - e.timestamp < 216e5);
|
|
405
|
+
}
|
|
406
|
+
checkAndNotify() {
|
|
407
|
+
const status = this.getStatus();
|
|
408
|
+
if (status.risk !== this.lastRisk) {
|
|
409
|
+
this.lastRisk = status.risk;
|
|
410
|
+
this.config.onRiskChange?.(status);
|
|
411
|
+
}
|
|
412
|
+
}
|
|
413
|
+
};
|
|
414
|
+
var DEFAULT_CONFIG4 = {
|
|
415
|
+
resumeBufferMs: 1e4
|
|
416
|
+
};
|
|
417
|
+
var TimelockGuard = class {
|
|
418
|
+
config;
|
|
419
|
+
state = {
|
|
420
|
+
isActive: false,
|
|
421
|
+
errorCount: 0
|
|
422
|
+
};
|
|
423
|
+
knownChats = new Set();
|
|
424
|
+
resumeTimer = null;
|
|
425
|
+
timerGeneration = 0;
|
|
426
|
+
constructor(config = {}) {
|
|
427
|
+
this.config = { ...DEFAULT_CONFIG4, ...config };
|
|
428
|
+
}
|
|
429
|
+
onTimelockUpdate(data) {
|
|
430
|
+
const wasActive = this.state.isActive;
|
|
431
|
+
this.state.isActive = !!data.isActive;
|
|
432
|
+
this.state.enforcementType = data.enforcementType;
|
|
433
|
+
this.state.expiresAt = data.timeEnforcementEnds;
|
|
434
|
+
if (this.state.isActive && !wasActive) {
|
|
435
|
+
this.state.detectedAt = new Date();
|
|
436
|
+
this.state.errorCount = 0;
|
|
437
|
+
this.config.onTimelockDetected?.(this.getState());
|
|
438
|
+
this.scheduleResume();
|
|
439
|
+
}
|
|
440
|
+
else if (this.state.isActive && wasActive) {
|
|
441
|
+
this.scheduleResume();
|
|
442
|
+
}
|
|
443
|
+
if (!this.state.isActive && wasActive) {
|
|
444
|
+
this.clearResumeTimer();
|
|
445
|
+
this.config.onTimelockLifted?.(this.getState());
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
record463Error() {
|
|
449
|
+
this.state.errorCount++;
|
|
450
|
+
if (!this.state.isActive) {
|
|
451
|
+
this.state.isActive = true;
|
|
452
|
+
this.state.detectedAt = new Date();
|
|
453
|
+
this.state.expiresAt = new Date(Date.now() + 6e4);
|
|
454
|
+
this.config.onTimelockDetected?.(this.getState());
|
|
455
|
+
this.scheduleResume();
|
|
456
|
+
}
|
|
457
|
+
}
|
|
458
|
+
registerKnownChat(jid) {
|
|
459
|
+
this.knownChats.add(jid);
|
|
460
|
+
}
|
|
461
|
+
registerKnownChats(jids) {
|
|
462
|
+
for (const jid of jids) {
|
|
463
|
+
this.knownChats.add(jid);
|
|
464
|
+
}
|
|
465
|
+
}
|
|
466
|
+
canSend(jid) {
|
|
467
|
+
if (!this.state.isActive) {
|
|
468
|
+
return { allowed: true };
|
|
469
|
+
}
|
|
470
|
+
if (this.state.expiresAt) {
|
|
471
|
+
const expiryWithBuffer = this.state.expiresAt.getTime() + this.config.resumeBufferMs;
|
|
472
|
+
if (Date.now() >= expiryWithBuffer) {
|
|
473
|
+
this.lift();
|
|
474
|
+
return { allowed: true };
|
|
475
|
+
}
|
|
476
|
+
}
|
|
477
|
+
if (jid.endsWith('@g.us') || jid.endsWith('@newsletter')) {
|
|
478
|
+
return { allowed: true };
|
|
479
|
+
}
|
|
480
|
+
if (this.knownChats.has(jid)) {
|
|
481
|
+
return { allowed: true };
|
|
482
|
+
}
|
|
483
|
+
const expiresIn = this.state.expiresAt ? Math.max(0, this.state.expiresAt.getTime() - Date.now()) : 6e4;
|
|
484
|
+
return {
|
|
485
|
+
allowed: false,
|
|
486
|
+
reason: `Reachout timelocked (${this.state.enforcementType || 'unknown'}). New contacts blocked. Expires in ${Math.ceil(expiresIn / 1e3)}s.`
|
|
487
|
+
};
|
|
488
|
+
}
|
|
489
|
+
getState() {
|
|
490
|
+
return { ...this.state };
|
|
491
|
+
}
|
|
492
|
+
isTimelocked() {
|
|
493
|
+
if (!this.state.isActive)
|
|
494
|
+
return false;
|
|
495
|
+
if (this.state.expiresAt) {
|
|
496
|
+
const expiryWithBuffer = this.state.expiresAt.getTime() + this.config.resumeBufferMs;
|
|
497
|
+
if (Date.now() >= expiryWithBuffer) {
|
|
498
|
+
this.lift();
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
}
|
|
502
|
+
return true;
|
|
503
|
+
}
|
|
504
|
+
getKnownChats() {
|
|
505
|
+
return new Set(this.knownChats);
|
|
506
|
+
}
|
|
507
|
+
lift() {
|
|
508
|
+
if (this.state.isActive) {
|
|
509
|
+
this.state.isActive = false;
|
|
510
|
+
this.clearResumeTimer();
|
|
511
|
+
this.config.onTimelockLifted?.(this.getState());
|
|
512
|
+
}
|
|
513
|
+
}
|
|
514
|
+
reset() {
|
|
515
|
+
this.state = { isActive: false, errorCount: 0 };
|
|
516
|
+
this.knownChats.clear();
|
|
517
|
+
this.clearResumeTimer();
|
|
518
|
+
}
|
|
519
|
+
scheduleResume() {
|
|
520
|
+
this.clearResumeTimer();
|
|
521
|
+
if (this.state.expiresAt) {
|
|
522
|
+
const delay = this.state.expiresAt.getTime() - Date.now() + this.config.resumeBufferMs;
|
|
523
|
+
if (delay > 0) {
|
|
524
|
+
this.timerGeneration++;
|
|
525
|
+
const currentGeneration = this.timerGeneration;
|
|
526
|
+
this.resumeTimer = setTimeout(() => {
|
|
527
|
+
if (currentGeneration === this.timerGeneration) {
|
|
528
|
+
this.lift();
|
|
529
|
+
}
|
|
530
|
+
}, delay);
|
|
531
|
+
}
|
|
532
|
+
}
|
|
533
|
+
}
|
|
534
|
+
clearResumeTimer() {
|
|
535
|
+
if (this.resumeTimer) {
|
|
536
|
+
clearTimeout(this.resumeTimer);
|
|
537
|
+
this.resumeTimer = null;
|
|
538
|
+
this.timerGeneration++;
|
|
539
|
+
}
|
|
540
|
+
}
|
|
541
|
+
};
|
|
542
|
+
var DEFAULT_CONFIG5 = {
|
|
543
|
+
enabled: false,
|
|
544
|
+
minRatio: 0.1,
|
|
545
|
+
minMessagesBeforeEnforce: 5,
|
|
546
|
+
inboundAutoReplyProbability: 0.25,
|
|
547
|
+
autoReplyTemplates: ['\u{1F44D}', '\u{1F44C}', 'ok', 'noted', 'thanks', '\u{1F64F}', 'got it'],
|
|
548
|
+
cooldownHoursOnViolation: 24,
|
|
549
|
+
scope: 'individual'
|
|
550
|
+
};
|
|
551
|
+
var ReplyRatioGuard = class {
|
|
552
|
+
config;
|
|
553
|
+
contacts = new Map();
|
|
554
|
+
constructor(config = {}) {
|
|
555
|
+
this.config = { ...DEFAULT_CONFIG5, ...config };
|
|
556
|
+
}
|
|
557
|
+
beforeSend(jid) {
|
|
558
|
+
if (!this.config.enabled) {
|
|
559
|
+
return { allowed: true };
|
|
560
|
+
}
|
|
561
|
+
if (this.isGroup(jid) && this.config.scope === 'individual') {
|
|
562
|
+
return { allowed: true };
|
|
563
|
+
}
|
|
564
|
+
const record = this.contacts.get(jid);
|
|
565
|
+
if (!record) {
|
|
566
|
+
return { allowed: true };
|
|
567
|
+
}
|
|
568
|
+
if (record.cooledUntil && Date.now() < record.cooledUntil) {
|
|
569
|
+
const hoursLeft = Math.ceil((record.cooledUntil - Date.now()) / 36e5);
|
|
570
|
+
return {
|
|
571
|
+
allowed: false,
|
|
572
|
+
reason: `Reply ratio cooldown \u2014 ${record.sent} sent, ${record.received} received. Retry in ${hoursLeft}h`
|
|
573
|
+
};
|
|
574
|
+
}
|
|
575
|
+
if (record.sent >= this.config.minMessagesBeforeEnforce) {
|
|
576
|
+
const ratio = record.sent === 0 ? 1 : record.received / record.sent;
|
|
577
|
+
if (ratio < this.config.minRatio) {
|
|
578
|
+
record.cooledUntil = Date.now() + this.config.cooldownHoursOnViolation * 36e5;
|
|
579
|
+
return {
|
|
580
|
+
allowed: false,
|
|
581
|
+
reason: `Reply ratio too low (${(ratio * 100).toFixed(1)}% < ${(this.config.minRatio * 100).toFixed(1)}%). Cooldown ${this.config.cooldownHoursOnViolation}h`
|
|
582
|
+
};
|
|
583
|
+
}
|
|
584
|
+
}
|
|
585
|
+
return { allowed: true };
|
|
586
|
+
}
|
|
587
|
+
recordSent(jid) {
|
|
588
|
+
if (!this.config.enabled)
|
|
589
|
+
return;
|
|
590
|
+
const record = this.contacts.get(jid) || { sent: 0, received: 0 };
|
|
591
|
+
record.sent++;
|
|
592
|
+
this.contacts.set(jid, record);
|
|
593
|
+
}
|
|
594
|
+
recordReceived(jid) {
|
|
595
|
+
if (!this.config.enabled)
|
|
596
|
+
return;
|
|
597
|
+
const record = this.contacts.get(jid) || { sent: 0, received: 0 };
|
|
598
|
+
record.received++;
|
|
599
|
+
delete record.cooledUntil;
|
|
600
|
+
this.contacts.set(jid, record);
|
|
601
|
+
}
|
|
602
|
+
suggestReply(jid, _msgText) {
|
|
603
|
+
if (!this.config.enabled) {
|
|
604
|
+
return { shouldReply: false };
|
|
605
|
+
}
|
|
606
|
+
if (this.isGroup(jid) && this.config.scope === 'individual') {
|
|
607
|
+
return { shouldReply: false };
|
|
608
|
+
}
|
|
609
|
+
if (Math.random() < this.config.inboundAutoReplyProbability) {
|
|
610
|
+
const templates = this.config.autoReplyTemplates;
|
|
611
|
+
const suggestedText = templates[Math.floor(Math.random() * templates.length)];
|
|
612
|
+
return { shouldReply: true, suggestedText };
|
|
613
|
+
}
|
|
614
|
+
return { shouldReply: false };
|
|
615
|
+
}
|
|
616
|
+
getStats() {
|
|
617
|
+
const perContact = Array.from(this.contacts.entries()).map(([jid, record]) => ({
|
|
618
|
+
jid,
|
|
619
|
+
sent: record.sent,
|
|
620
|
+
received: record.received,
|
|
621
|
+
ratio: record.sent === 0 ? 0 : record.received / record.sent,
|
|
622
|
+
cooledUntil: record.cooledUntil
|
|
623
|
+
}));
|
|
624
|
+
const globalSent = perContact.reduce((sum, c) => sum + c.sent, 0);
|
|
625
|
+
const globalReceived = perContact.reduce((sum, c) => sum + c.received, 0);
|
|
626
|
+
const globalRatio = globalSent === 0 ? 0 : globalReceived / globalSent;
|
|
627
|
+
const contactsOnCooldown = perContact.filter(c => c.cooledUntil && Date.now() < c.cooledUntil).length;
|
|
628
|
+
return {
|
|
629
|
+
perContact,
|
|
630
|
+
globalSent,
|
|
631
|
+
globalReceived,
|
|
632
|
+
globalRatio,
|
|
633
|
+
contactsOnCooldown
|
|
634
|
+
};
|
|
635
|
+
}
|
|
636
|
+
reset() {
|
|
637
|
+
this.contacts.clear();
|
|
638
|
+
}
|
|
639
|
+
exportState() {
|
|
640
|
+
return {
|
|
641
|
+
contacts: Array.from(this.contacts.entries())
|
|
642
|
+
};
|
|
643
|
+
}
|
|
644
|
+
restoreState(state) {
|
|
645
|
+
if (state?.contacts && Array.isArray(state.contacts)) {
|
|
646
|
+
this.contacts = new Map(state.contacts);
|
|
647
|
+
}
|
|
648
|
+
}
|
|
649
|
+
isGroup(jid) {
|
|
650
|
+
return jid.endsWith('@g.us');
|
|
651
|
+
}
|
|
652
|
+
};
|
|
653
|
+
var DEFAULT_CONFIG6 = {
|
|
654
|
+
enabled: false,
|
|
655
|
+
requireHandshakeBeforeGroupSend: true,
|
|
656
|
+
handshakeMinDelayMs: 36e5,
|
|
657
|
+
groupLurkPeriodMs: 432e5,
|
|
658
|
+
maxStrangerMessagesPerDay: 5,
|
|
659
|
+
autoRegisterOnIncoming: true
|
|
660
|
+
};
|
|
661
|
+
var ContactGraphWarmer = class {
|
|
662
|
+
config;
|
|
663
|
+
contacts = new Map();
|
|
664
|
+
groups = new Map();
|
|
665
|
+
strangerMessagesToday = 0;
|
|
666
|
+
lastStrangerResetDay = this.getCurrentDay();
|
|
667
|
+
constructor(config = {}) {
|
|
668
|
+
this.config = { ...DEFAULT_CONFIG6, ...config };
|
|
669
|
+
}
|
|
670
|
+
canMessage(jid) {
|
|
671
|
+
if (!this.config.enabled) {
|
|
672
|
+
return { allowed: true };
|
|
673
|
+
}
|
|
674
|
+
const currentDay = this.getCurrentDay();
|
|
675
|
+
if (currentDay !== this.lastStrangerResetDay) {
|
|
676
|
+
this.strangerMessagesToday = 0;
|
|
677
|
+
this.lastStrangerResetDay = currentDay;
|
|
678
|
+
}
|
|
679
|
+
if (this.isGroup(jid)) {
|
|
680
|
+
return this.checkGroupMessage(jid);
|
|
681
|
+
}
|
|
682
|
+
return this.checkIndividualMessage(jid);
|
|
683
|
+
}
|
|
684
|
+
markHandshakeSent(jid) {
|
|
685
|
+
if (!this.config.enabled)
|
|
686
|
+
return;
|
|
687
|
+
if (this.isGroup(jid))
|
|
688
|
+
return;
|
|
689
|
+
const record = this.contacts.get(jid) || { state: 'stranger' };
|
|
690
|
+
record.state = 'handshake_sent';
|
|
691
|
+
record.handshakeSentAt = Date.now();
|
|
692
|
+
this.contacts.set(jid, record);
|
|
693
|
+
}
|
|
694
|
+
markHandshakeComplete(jid) {
|
|
695
|
+
if (!this.config.enabled)
|
|
696
|
+
return;
|
|
697
|
+
if (this.isGroup(jid))
|
|
698
|
+
return;
|
|
699
|
+
const record = this.contacts.get(jid) || { state: 'stranger' };
|
|
700
|
+
record.state = 'handshake_complete';
|
|
701
|
+
this.contacts.set(jid, record);
|
|
702
|
+
}
|
|
703
|
+
registerKnownContact(jid) {
|
|
704
|
+
if (!this.config.enabled)
|
|
705
|
+
return;
|
|
706
|
+
if (this.isGroup(jid))
|
|
707
|
+
return;
|
|
708
|
+
const record = this.contacts.get(jid) || { state: 'stranger' };
|
|
709
|
+
record.state = 'known';
|
|
710
|
+
this.contacts.set(jid, record);
|
|
711
|
+
}
|
|
712
|
+
registerGroupJoin(groupJid) {
|
|
713
|
+
if (!this.config.enabled)
|
|
714
|
+
return;
|
|
715
|
+
if (!this.isGroup(groupJid))
|
|
716
|
+
return;
|
|
717
|
+
this.groups.set(groupJid, { joinedAt: Date.now() });
|
|
718
|
+
}
|
|
719
|
+
getContactState(jid) {
|
|
720
|
+
if (this.isGroup(jid))
|
|
721
|
+
return 'known';
|
|
722
|
+
return this.contacts.get(jid)?.state || 'stranger';
|
|
723
|
+
}
|
|
724
|
+
onIncomingMessage(jid) {
|
|
725
|
+
if (!this.config.enabled)
|
|
726
|
+
return;
|
|
727
|
+
if (this.isGroup(jid))
|
|
728
|
+
return;
|
|
729
|
+
if (this.config.autoRegisterOnIncoming) {
|
|
730
|
+
this.registerKnownContact(jid);
|
|
731
|
+
}
|
|
732
|
+
}
|
|
733
|
+
getStats() {
|
|
734
|
+
const knownContacts = Array.from(this.contacts.values()).filter(c => c.state === 'known').length;
|
|
735
|
+
const pendingHandshakes = Array.from(this.contacts.values()).filter(c => c.state === 'handshake_sent').length;
|
|
736
|
+
const groupsJoined = Array.from(this.groups.entries()).map(([groupJid, record]) => ({
|
|
737
|
+
groupJid,
|
|
738
|
+
joinedAt: record.joinedAt,
|
|
739
|
+
firstSendUnlocksAt: record.joinedAt + this.config.groupLurkPeriodMs
|
|
740
|
+
}));
|
|
741
|
+
return {
|
|
742
|
+
knownContacts,
|
|
743
|
+
pendingHandshakes,
|
|
744
|
+
strangersToday: this.strangerMessagesToday,
|
|
745
|
+
groupsJoined
|
|
746
|
+
};
|
|
747
|
+
}
|
|
748
|
+
reset() {
|
|
749
|
+
this.contacts.clear();
|
|
750
|
+
this.groups.clear();
|
|
751
|
+
this.strangerMessagesToday = 0;
|
|
752
|
+
this.lastStrangerResetDay = this.getCurrentDay();
|
|
753
|
+
}
|
|
754
|
+
exportState() {
|
|
755
|
+
return {
|
|
756
|
+
contacts: Array.from(this.contacts.entries()),
|
|
757
|
+
groups: Array.from(this.groups.entries()),
|
|
758
|
+
strangerMessagesToday: this.strangerMessagesToday,
|
|
759
|
+
lastStrangerResetDay: this.lastStrangerResetDay
|
|
760
|
+
};
|
|
761
|
+
}
|
|
762
|
+
restoreState(state) {
|
|
763
|
+
if (state?.contacts && Array.isArray(state.contacts)) {
|
|
764
|
+
this.contacts = new Map(state.contacts);
|
|
765
|
+
}
|
|
766
|
+
if (state?.groups && Array.isArray(state.groups)) {
|
|
767
|
+
this.groups = new Map(state.groups);
|
|
768
|
+
}
|
|
769
|
+
if (typeof state?.strangerMessagesToday === 'number') {
|
|
770
|
+
this.strangerMessagesToday = state.strangerMessagesToday;
|
|
771
|
+
}
|
|
772
|
+
if (typeof state?.lastStrangerResetDay === 'number') {
|
|
773
|
+
this.lastStrangerResetDay = state.lastStrangerResetDay;
|
|
774
|
+
}
|
|
775
|
+
}
|
|
776
|
+
isGroup(jid) {
|
|
777
|
+
return jid.endsWith('@g.us');
|
|
778
|
+
}
|
|
779
|
+
getCurrentDay() {
|
|
780
|
+
return Math.floor(Date.now() / 864e5);
|
|
781
|
+
}
|
|
782
|
+
checkGroupMessage(groupJid) {
|
|
783
|
+
const record = this.groups.get(groupJid);
|
|
784
|
+
if (!record) {
|
|
785
|
+
return { allowed: true };
|
|
786
|
+
}
|
|
787
|
+
const lurkEndsAt = record.joinedAt + this.config.groupLurkPeriodMs;
|
|
788
|
+
if (Date.now() < lurkEndsAt) {
|
|
789
|
+
const minutesLeft = Math.ceil((lurkEndsAt - Date.now()) / 6e4);
|
|
790
|
+
return {
|
|
791
|
+
allowed: false,
|
|
792
|
+
reason: `Group lurk period not elapsed \u2014 wait ${minutesLeft} minutes`
|
|
793
|
+
};
|
|
794
|
+
}
|
|
795
|
+
return { allowed: true };
|
|
796
|
+
}
|
|
797
|
+
checkIndividualMessage(jid) {
|
|
798
|
+
const record = this.contacts.get(jid);
|
|
799
|
+
if (!record || record.state === 'stranger') {
|
|
800
|
+
if (this.config.requireHandshakeBeforeGroupSend) {
|
|
801
|
+
if (this.strangerMessagesToday >= this.config.maxStrangerMessagesPerDay) {
|
|
802
|
+
return {
|
|
803
|
+
allowed: false,
|
|
804
|
+
reason: `Daily new-contact limit reached (${this.config.maxStrangerMessagesPerDay})`,
|
|
805
|
+
needsHandshake: true
|
|
806
|
+
};
|
|
807
|
+
}
|
|
808
|
+
this.strangerMessagesToday++;
|
|
809
|
+
}
|
|
810
|
+
return { allowed: true, needsHandshake: true };
|
|
811
|
+
}
|
|
812
|
+
if (record.state === 'handshake_sent') {
|
|
813
|
+
if (!record.handshakeSentAt) {
|
|
814
|
+
return { allowed: true };
|
|
815
|
+
}
|
|
816
|
+
const elapsed = Date.now() - record.handshakeSentAt;
|
|
817
|
+
if (elapsed < this.config.handshakeMinDelayMs) {
|
|
818
|
+
const minutesLeft = Math.ceil((this.config.handshakeMinDelayMs - elapsed) / 6e4);
|
|
819
|
+
return {
|
|
820
|
+
allowed: false,
|
|
821
|
+
reason: `Handshake too recent \u2014 wait ${minutesLeft} minutes`
|
|
822
|
+
};
|
|
823
|
+
}
|
|
824
|
+
}
|
|
825
|
+
return { allowed: true };
|
|
826
|
+
}
|
|
827
|
+
};
|
|
828
|
+
var DEFAULT_CONFIG7 = {
|
|
829
|
+
enabled: false,
|
|
830
|
+
enableCircadianRhythm: true,
|
|
831
|
+
timezone: 'UTC',
|
|
832
|
+
activityCurve: 'office',
|
|
833
|
+
circadian: {
|
|
834
|
+
enabled: true,
|
|
835
|
+
profile: 'default',
|
|
836
|
+
timezone: 'UTC'
|
|
837
|
+
},
|
|
838
|
+
distractionPauseProbability: 0.05,
|
|
839
|
+
distractionPauseMinMs: 3e5,
|
|
840
|
+
distractionPauseMaxMs: 12e5,
|
|
841
|
+
readReceiptDelayMinMs: 3e3,
|
|
842
|
+
readReceiptDelayMaxMs: 45e3,
|
|
843
|
+
readReceiptSkipProbability: 0.15,
|
|
844
|
+
offlineGapProbability: 0.03,
|
|
845
|
+
offlineGapMinMs: 3e5,
|
|
846
|
+
offlineGapMaxMs: 9e5,
|
|
847
|
+
enableTypingModel: true,
|
|
848
|
+
typingWPM: 45,
|
|
849
|
+
typingWPMStdDev: 15,
|
|
850
|
+
thinkPauseProbability: 0.08,
|
|
851
|
+
thinkPauseMinMs: 800,
|
|
852
|
+
thinkPauseMaxMs: 3500,
|
|
853
|
+
intermittentPausedProbability: 0.4,
|
|
854
|
+
typingMaxMs: 9e4,
|
|
855
|
+
typingMinMs: 600
|
|
856
|
+
};
|
|
857
|
+
var ACTIVITY_CURVES = {
|
|
858
|
+
office: [
|
|
859
|
+
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
|
|
860
|
+
0.5, 0.5,
|
|
861
|
+
0.95, 0.95,
|
|
862
|
+
0.6,
|
|
863
|
+
0.9, 0.9, 0.9, 0.9,
|
|
864
|
+
0.6, 0.6,
|
|
865
|
+
0.4, 0.4,
|
|
866
|
+
0.2, 0.2, 0.2, 0.2
|
|
867
|
+
],
|
|
868
|
+
social: [
|
|
869
|
+
0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1, 0.1,
|
|
870
|
+
0.3, 0.4,
|
|
871
|
+
0.7, 0.8,
|
|
872
|
+
0.5,
|
|
873
|
+
0.7, 0.7,
|
|
874
|
+
0.4,
|
|
875
|
+
0.8, 0.9, 0.9,
|
|
876
|
+
0.6,
|
|
877
|
+
0.8, 0.85, 0.9, 0.95, 1
|
|
878
|
+
],
|
|
879
|
+
global: [
|
|
880
|
+
0.5, 0.5, 0.5, 0.5, 0.5, 0.5,
|
|
881
|
+
0.4, 0.4,
|
|
882
|
+
0.6, 0.7, 0.8, 0.8,
|
|
883
|
+
0.6,
|
|
884
|
+
0.8, 0.8, 0.8, 0.8,
|
|
885
|
+
0.7, 0.7,
|
|
886
|
+
0.6, 0.5, 0.5, 0.5, 0.5, 0.5
|
|
887
|
+
]
|
|
888
|
+
};
|
|
889
|
+
function getCircadianMultiplier(date = new Date(), profile = 'default', timezone) {
|
|
890
|
+
if (profile === 'always_on') {
|
|
891
|
+
return 1;
|
|
892
|
+
}
|
|
893
|
+
let hour;
|
|
894
|
+
if (timezone) {
|
|
895
|
+
try {
|
|
896
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
897
|
+
timeZone: timezone,
|
|
898
|
+
hour: 'numeric',
|
|
899
|
+
hour12: false
|
|
900
|
+
});
|
|
901
|
+
const parts = formatter.formatToParts(date);
|
|
902
|
+
const hourPart = parts.find(p => p.type === 'hour');
|
|
903
|
+
hour = hourPart ? parseInt(hourPart.value, 10) : date.getHours();
|
|
904
|
+
}
|
|
905
|
+
catch {
|
|
906
|
+
hour = date.getHours();
|
|
907
|
+
}
|
|
908
|
+
}
|
|
909
|
+
else {
|
|
910
|
+
hour = date.getHours();
|
|
911
|
+
}
|
|
912
|
+
let shiftedHour = hour;
|
|
913
|
+
if (profile === 'nightOwl') {
|
|
914
|
+
shiftedHour = (hour - 3 + 24) % 24;
|
|
915
|
+
}
|
|
916
|
+
else if (profile === 'earlyBird') {
|
|
917
|
+
shiftedHour = (hour + 2) % 24;
|
|
918
|
+
}
|
|
919
|
+
if (shiftedHour >= 9 && shiftedHour < 22) {
|
|
920
|
+
const t = (shiftedHour - 9) / 13;
|
|
921
|
+
return 1 + 0.2 * Math.cos(2 * Math.PI * t);
|
|
922
|
+
}
|
|
923
|
+
else if (shiftedHour >= 22 && shiftedHour < 24) {
|
|
924
|
+
const t = (shiftedHour - 22) / 2;
|
|
925
|
+
return 1.2 + 1.3 * t;
|
|
926
|
+
}
|
|
927
|
+
else if (shiftedHour >= 0 && shiftedHour < 2) {
|
|
928
|
+
const t = shiftedHour / 2;
|
|
929
|
+
return 2.5 + 1.5 * t;
|
|
930
|
+
}
|
|
931
|
+
else if (shiftedHour >= 2 && shiftedHour < 6) {
|
|
932
|
+
const t = (shiftedHour - 2) / 4;
|
|
933
|
+
return 5 + 1 * Math.cos(Math.PI * t);
|
|
934
|
+
}
|
|
935
|
+
else {
|
|
936
|
+
const t = (shiftedHour - 6) / 3;
|
|
937
|
+
return 4 - 3 * t;
|
|
938
|
+
}
|
|
939
|
+
}
|
|
940
|
+
var PresenceChoreographer = class {
|
|
941
|
+
config;
|
|
942
|
+
stats = {
|
|
943
|
+
distractionPausesInjected: 0,
|
|
944
|
+
offlineGapsInjected: 0,
|
|
945
|
+
readReceiptsDelayed: 0,
|
|
946
|
+
readReceiptsSkipped: 0,
|
|
947
|
+
typingPlansComputed: 0,
|
|
948
|
+
typingPlansExecuted: 0,
|
|
949
|
+
totalTypingTimeMs: 0
|
|
950
|
+
};
|
|
951
|
+
constructor(config = {}) {
|
|
952
|
+
this.config = {
|
|
953
|
+
...DEFAULT_CONFIG7,
|
|
954
|
+
...config,
|
|
955
|
+
circadian: {
|
|
956
|
+
...DEFAULT_CONFIG7.circadian,
|
|
957
|
+
...(config.circadian || {})
|
|
958
|
+
}
|
|
959
|
+
};
|
|
960
|
+
}
|
|
961
|
+
getCurrentActivityFactor() {
|
|
962
|
+
if (!this.config.enabled || !this.config.enableCircadianRhythm) {
|
|
963
|
+
return 1;
|
|
964
|
+
}
|
|
965
|
+
const hour = this.getLocalHour();
|
|
966
|
+
const curve = ACTIVITY_CURVES[this.config.activityCurve];
|
|
967
|
+
return curve[hour] || 0.5;
|
|
968
|
+
}
|
|
969
|
+
shouldPauseForDistraction() {
|
|
970
|
+
if (!this.config.enabled) {
|
|
971
|
+
return { pause: false, durationMs: 0 };
|
|
972
|
+
}
|
|
973
|
+
if (Math.random() < this.config.distractionPauseProbability) {
|
|
974
|
+
const durationMs = this.randomBetween(this.config.distractionPauseMinMs, this.config.distractionPauseMaxMs);
|
|
975
|
+
this.stats.distractionPausesInjected++;
|
|
976
|
+
return { pause: true, durationMs };
|
|
977
|
+
}
|
|
978
|
+
return { pause: false, durationMs: 0 };
|
|
979
|
+
}
|
|
980
|
+
shouldTakeOfflineGap() {
|
|
981
|
+
if (!this.config.enabled) {
|
|
982
|
+
return { offline: false, durationMs: 0 };
|
|
983
|
+
}
|
|
984
|
+
if (Math.random() < this.config.offlineGapProbability) {
|
|
985
|
+
const durationMs = this.randomBetween(this.config.offlineGapMinMs, this.config.offlineGapMaxMs);
|
|
986
|
+
this.stats.offlineGapsInjected++;
|
|
987
|
+
return { offline: true, durationMs };
|
|
988
|
+
}
|
|
989
|
+
return { offline: false, durationMs: 0 };
|
|
990
|
+
}
|
|
991
|
+
shouldMarkRead() {
|
|
992
|
+
if (!this.config.enabled) {
|
|
993
|
+
return { mark: true, delayMs: 0 };
|
|
994
|
+
}
|
|
995
|
+
if (Math.random() < this.config.readReceiptSkipProbability) {
|
|
996
|
+
this.stats.readReceiptsSkipped++;
|
|
997
|
+
return { mark: false, delayMs: 0 };
|
|
998
|
+
}
|
|
999
|
+
const baseDelayMs = this.randomBetween(this.config.readReceiptDelayMinMs, this.config.readReceiptDelayMaxMs);
|
|
1000
|
+
let delayMs = baseDelayMs;
|
|
1001
|
+
if (this.config.circadian.enabled) {
|
|
1002
|
+
const circadianMultiplier = getCircadianMultiplier(new Date(), this.config.circadian.profile, this.config.circadian.timezone);
|
|
1003
|
+
delayMs = Math.floor(baseDelayMs * circadianMultiplier);
|
|
1004
|
+
}
|
|
1005
|
+
this.stats.readReceiptsDelayed++;
|
|
1006
|
+
return { mark: true, delayMs };
|
|
1007
|
+
}
|
|
1008
|
+
computeTypingPlan(messageLength) {
|
|
1009
|
+
if (!this.config.enabled || !this.config.enableTypingModel) {
|
|
1010
|
+
return [{ state: 'composing', durationMs: this.config.typingMinMs }];
|
|
1011
|
+
}
|
|
1012
|
+
this.stats.typingPlansComputed++;
|
|
1013
|
+
if (messageLength === 0) {
|
|
1014
|
+
return [{ state: 'composing', durationMs: this.config.typingMinMs }];
|
|
1015
|
+
}
|
|
1016
|
+
const wpmSample = this.clamp(this.gaussianSample(this.config.typingWPM, this.config.typingWPMStdDev), 10, 120);
|
|
1017
|
+
const cps = (wpmSample * 5) / 60;
|
|
1018
|
+
const baseMs = (messageLength / cps) * 1e3;
|
|
1019
|
+
let circadianMultiplier = 1;
|
|
1020
|
+
if (this.config.circadian.enabled) {
|
|
1021
|
+
circadianMultiplier = getCircadianMultiplier(new Date(), this.config.circadian.profile, this.config.circadian.timezone);
|
|
1022
|
+
}
|
|
1023
|
+
const targetMs = this.clamp(baseMs * circadianMultiplier, this.config.typingMinMs, this.config.typingMaxMs);
|
|
1024
|
+
const plan = [];
|
|
1025
|
+
let remainingBudget = targetMs;
|
|
1026
|
+
let position = 0;
|
|
1027
|
+
const chunkSize = 10;
|
|
1028
|
+
const numChunks = Math.max(1, Math.ceil(messageLength / chunkSize));
|
|
1029
|
+
for (let i = 0; i < numChunks && remainingBudget > 0; i++) {
|
|
1030
|
+
const charsInChunk = Math.min(chunkSize, messageLength - position);
|
|
1031
|
+
const remainingChunks = numChunks - i;
|
|
1032
|
+
const chunkBudget = remainingBudget / remainingChunks;
|
|
1033
|
+
const chunkTypingMs = Math.floor(Math.min(chunkBudget, remainingBudget));
|
|
1034
|
+
if (chunkTypingMs <= 0)
|
|
1035
|
+
break;
|
|
1036
|
+
if (i > 0 && i < numChunks - 1 && Math.random() < this.config.thinkPauseProbability) {
|
|
1037
|
+
plan.push({ state: 'composing', durationMs: chunkTypingMs });
|
|
1038
|
+
remainingBudget -= chunkTypingMs;
|
|
1039
|
+
const basePauseMs = this.randomBetween(this.config.thinkPauseMinMs, this.config.thinkPauseMaxMs);
|
|
1040
|
+
const pauseMs = Math.floor(basePauseMs * circadianMultiplier);
|
|
1041
|
+
plan.push({ state: 'paused', durationMs: pauseMs });
|
|
1042
|
+
}
|
|
1043
|
+
else {
|
|
1044
|
+
if (plan.length === 0 || plan[plan.length - 1].state === 'paused') {
|
|
1045
|
+
plan.push({ state: 'composing', durationMs: chunkTypingMs });
|
|
1046
|
+
}
|
|
1047
|
+
else {
|
|
1048
|
+
plan[plan.length - 1].durationMs += chunkTypingMs;
|
|
1049
|
+
}
|
|
1050
|
+
remainingBudget -= chunkTypingMs;
|
|
1051
|
+
}
|
|
1052
|
+
position += charsInChunk;
|
|
1053
|
+
}
|
|
1054
|
+
if (Math.random() < this.config.intermittentPausedProbability) {
|
|
1055
|
+
const baseFinalPauseMs = this.randomBetween(200, 800);
|
|
1056
|
+
const finalPauseMs = Math.floor(baseFinalPauseMs * circadianMultiplier);
|
|
1057
|
+
plan.push({ state: 'paused', durationMs: finalPauseMs });
|
|
1058
|
+
}
|
|
1059
|
+
if (plan.length === 0 || !plan.some(step => step.state === 'composing')) {
|
|
1060
|
+
return [{ state: 'composing', durationMs: this.config.typingMinMs }];
|
|
1061
|
+
}
|
|
1062
|
+
return plan;
|
|
1063
|
+
}
|
|
1064
|
+
async executeTypingPlan(sock, jid, plan, options) {
|
|
1065
|
+
this.stats.typingPlansExecuted++;
|
|
1066
|
+
for (const step of plan) {
|
|
1067
|
+
if (options?.signal?.aborted) {
|
|
1068
|
+
await Promise.resolve(sock.sendPresenceUpdate('paused', jid));
|
|
1069
|
+
throw new Error('Typing plan aborted');
|
|
1070
|
+
}
|
|
1071
|
+
await Promise.resolve(sock.sendPresenceUpdate(step.state, jid));
|
|
1072
|
+
await this.sleep(step.durationMs);
|
|
1073
|
+
this.stats.totalTypingTimeMs += step.durationMs;
|
|
1074
|
+
}
|
|
1075
|
+
}
|
|
1076
|
+
getStats() {
|
|
1077
|
+
return {
|
|
1078
|
+
currentActivityFactor: this.getCurrentActivityFactor(),
|
|
1079
|
+
distractionPausesInjected: this.stats.distractionPausesInjected,
|
|
1080
|
+
offlineGapsInjected: this.stats.offlineGapsInjected,
|
|
1081
|
+
readReceiptsDelayed: this.stats.readReceiptsDelayed,
|
|
1082
|
+
readReceiptsSkipped: this.stats.readReceiptsSkipped,
|
|
1083
|
+
currentHourLocal: this.getLocalHour(),
|
|
1084
|
+
typingPlansComputed: this.stats.typingPlansComputed,
|
|
1085
|
+
typingPlansExecuted: this.stats.typingPlansExecuted,
|
|
1086
|
+
totalTypingTimeMs: this.stats.totalTypingTimeMs
|
|
1087
|
+
};
|
|
1088
|
+
}
|
|
1089
|
+
reset() {
|
|
1090
|
+
this.stats = {
|
|
1091
|
+
distractionPausesInjected: 0,
|
|
1092
|
+
offlineGapsInjected: 0,
|
|
1093
|
+
readReceiptsDelayed: 0,
|
|
1094
|
+
readReceiptsSkipped: 0,
|
|
1095
|
+
typingPlansComputed: 0,
|
|
1096
|
+
typingPlansExecuted: 0,
|
|
1097
|
+
totalTypingTimeMs: 0
|
|
1098
|
+
};
|
|
1099
|
+
}
|
|
1100
|
+
getLocalHour() {
|
|
1101
|
+
try {
|
|
1102
|
+
const formatter = new Intl.DateTimeFormat('en-US', {
|
|
1103
|
+
timeZone: this.config.timezone,
|
|
1104
|
+
hour: 'numeric',
|
|
1105
|
+
hour12: false
|
|
1106
|
+
});
|
|
1107
|
+
const parts = formatter.formatToParts(new Date());
|
|
1108
|
+
const hourPart = parts.find(p => p.type === 'hour');
|
|
1109
|
+
if (hourPart) {
|
|
1110
|
+
return parseInt(hourPart.value, 10);
|
|
1111
|
+
}
|
|
1112
|
+
}
|
|
1113
|
+
catch (error) { }
|
|
1114
|
+
return new Date().getUTCHours();
|
|
1115
|
+
}
|
|
1116
|
+
randomBetween(min, max) {
|
|
1117
|
+
return Math.floor(Math.random() * (max - min + 1)) + min;
|
|
1118
|
+
}
|
|
1119
|
+
clamp(value, min, max) {
|
|
1120
|
+
return Math.max(min, Math.min(max, value));
|
|
1121
|
+
}
|
|
1122
|
+
gaussianSample(mean, stdDev) {
|
|
1123
|
+
const u1 = Math.random();
|
|
1124
|
+
const u2 = Math.random();
|
|
1125
|
+
const z0 = Math.sqrt(-2 * Math.log(u1)) * Math.cos(2 * Math.PI * u2);
|
|
1126
|
+
return mean + z0 * stdDev;
|
|
1127
|
+
}
|
|
1128
|
+
sleep(ms) {
|
|
1129
|
+
return new Promise(resolve => setTimeout(resolve, ms));
|
|
1130
|
+
}
|
|
1131
|
+
};
|
|
1132
|
+
var DEFAULT_CONFIG8 = {
|
|
1133
|
+
enabled: false,
|
|
1134
|
+
maxRetries: 5,
|
|
1135
|
+
spiralThreshold: 3,
|
|
1136
|
+
onSpiral: () => { }
|
|
1137
|
+
};
|
|
1138
|
+
var RetryReasonTracker = class {
|
|
1139
|
+
config;
|
|
1140
|
+
retries = new Map();
|
|
1141
|
+
totalRetries = 0;
|
|
1142
|
+
reasonCounts = {
|
|
1143
|
+
no_session: 0,
|
|
1144
|
+
invalid_key: 0,
|
|
1145
|
+
bad_mac: 0,
|
|
1146
|
+
decryption_failure: 0,
|
|
1147
|
+
server_error_463: 0,
|
|
1148
|
+
server_error_429: 0,
|
|
1149
|
+
timeout: 0,
|
|
1150
|
+
no_route: 0,
|
|
1151
|
+
node_malformed: 0,
|
|
1152
|
+
unknown: 0
|
|
1153
|
+
};
|
|
1154
|
+
spiralsDetected = 0;
|
|
1155
|
+
constructor(config) {
|
|
1156
|
+
this.config = { ...DEFAULT_CONFIG8, ...config };
|
|
1157
|
+
}
|
|
1158
|
+
onMessageUpdate(update) {
|
|
1159
|
+
if (!this.config.enabled)
|
|
1160
|
+
return;
|
|
1161
|
+
const msgId = update.key?.id;
|
|
1162
|
+
if (!msgId)
|
|
1163
|
+
return;
|
|
1164
|
+
if (update.status !== 0 && !update.error)
|
|
1165
|
+
return;
|
|
1166
|
+
const reason = this.classify(update.error || update);
|
|
1167
|
+
this.recordRetry(msgId, reason);
|
|
1168
|
+
}
|
|
1169
|
+
classify(err) {
|
|
1170
|
+
if (!err)
|
|
1171
|
+
return 'unknown';
|
|
1172
|
+
const statusCode = err.output?.statusCode || err.statusCode || err.status;
|
|
1173
|
+
if (statusCode === 463)
|
|
1174
|
+
return 'server_error_463';
|
|
1175
|
+
if (statusCode === 429)
|
|
1176
|
+
return 'server_error_429';
|
|
1177
|
+
const errorMsg = (err.message || err.text || String(err)).toLowerCase();
|
|
1178
|
+
if (errorMsg.includes('bad mac'))
|
|
1179
|
+
return 'bad_mac';
|
|
1180
|
+
if (errorMsg.includes('no session') || errorMsg.includes('session not found'))
|
|
1181
|
+
return 'no_session';
|
|
1182
|
+
if (errorMsg.includes('invalid key') || errorMsg.includes('key error'))
|
|
1183
|
+
return 'invalid_key';
|
|
1184
|
+
if (errorMsg.includes('decryption') || errorMsg.includes('decrypt'))
|
|
1185
|
+
return 'decryption_failure';
|
|
1186
|
+
if (errorMsg.includes('timeout') || errorMsg.includes('timed out'))
|
|
1187
|
+
return 'timeout';
|
|
1188
|
+
if (errorMsg.includes('no route') || errorMsg.includes('unreachable') || errorMsg.includes('offline'))
|
|
1189
|
+
return 'no_route';
|
|
1190
|
+
if (errorMsg.includes('malformed') || errorMsg.includes('invalid node'))
|
|
1191
|
+
return 'node_malformed';
|
|
1192
|
+
return 'unknown';
|
|
1193
|
+
}
|
|
1194
|
+
recordRetry(msgId, reason) {
|
|
1195
|
+
const now = Date.now();
|
|
1196
|
+
let record = this.retries.get(msgId);
|
|
1197
|
+
if (!record) {
|
|
1198
|
+
record = {
|
|
1199
|
+
msgId,
|
|
1200
|
+
count: 0,
|
|
1201
|
+
reasons: [],
|
|
1202
|
+
firstRetry: now,
|
|
1203
|
+
lastRetry: now
|
|
1204
|
+
};
|
|
1205
|
+
this.retries.set(msgId, record);
|
|
1206
|
+
}
|
|
1207
|
+
record.count++;
|
|
1208
|
+
record.reasons.push(reason);
|
|
1209
|
+
record.lastRetry = now;
|
|
1210
|
+
this.totalRetries++;
|
|
1211
|
+
this.reasonCounts[reason]++;
|
|
1212
|
+
if (record.count >= this.config.spiralThreshold) {
|
|
1213
|
+
this.spiralsDetected++;
|
|
1214
|
+
this.config.onSpiral(msgId, reason);
|
|
1215
|
+
}
|
|
1216
|
+
}
|
|
1217
|
+
isSpiraling(msgId) {
|
|
1218
|
+
const record = this.retries.get(msgId);
|
|
1219
|
+
return record ? record.count >= this.config.spiralThreshold : false;
|
|
1220
|
+
}
|
|
1221
|
+
clear(msgId) {
|
|
1222
|
+
this.retries.delete(msgId);
|
|
1223
|
+
}
|
|
1224
|
+
getStats() {
|
|
1225
|
+
return {
|
|
1226
|
+
totalRetries: this.totalRetries,
|
|
1227
|
+
byReason: { ...this.reasonCounts },
|
|
1228
|
+
spiralsDetected: this.spiralsDetected,
|
|
1229
|
+
activeRetries: this.retries.size
|
|
1230
|
+
};
|
|
1231
|
+
}
|
|
1232
|
+
cleanup() {
|
|
1233
|
+
const now = Date.now();
|
|
1234
|
+
const maxAge = 5 * 60 * 1e3;
|
|
1235
|
+
for (const [msgId, record] of this.retries.entries()) {
|
|
1236
|
+
if (now - record.lastRetry > maxAge) {
|
|
1237
|
+
this.retries.delete(msgId);
|
|
1238
|
+
}
|
|
1239
|
+
}
|
|
1240
|
+
}
|
|
1241
|
+
destroy() {
|
|
1242
|
+
this.retries.clear();
|
|
1243
|
+
this.cleanup();
|
|
1244
|
+
}
|
|
1245
|
+
};
|
|
1246
|
+
var DEFAULT_CONFIG9 = {
|
|
1247
|
+
enabled: false,
|
|
1248
|
+
rampDurationMs: 6e4,
|
|
1249
|
+
initialRateMultiplier: 0.1,
|
|
1250
|
+
rampSteps: 6,
|
|
1251
|
+
baselineRatePerMinute: null
|
|
1252
|
+
};
|
|
1253
|
+
var PostReconnectThrottle = class {
|
|
1254
|
+
config;
|
|
1255
|
+
throttledSince = null;
|
|
1256
|
+
throttledSendCount = 0;
|
|
1257
|
+
lifetimeReconnects = 0;
|
|
1258
|
+
rampTimer = null;
|
|
1259
|
+
currentStep = 0;
|
|
1260
|
+
sendsInCurrentWindow = 0;
|
|
1261
|
+
currentWindowStart = 0;
|
|
1262
|
+
WINDOW_DURATION_MS = 6e4;
|
|
1263
|
+
constructor(config) {
|
|
1264
|
+
this.config = {
|
|
1265
|
+
...DEFAULT_CONFIG9,
|
|
1266
|
+
...config,
|
|
1267
|
+
baselineRatePerMinute: config?.baselineRatePerMinute || null
|
|
1268
|
+
};
|
|
1269
|
+
}
|
|
1270
|
+
onReconnect() {
|
|
1271
|
+
if (!this.config.enabled)
|
|
1272
|
+
return;
|
|
1273
|
+
this.throttledSince = Date.now();
|
|
1274
|
+
this.currentStep = 0;
|
|
1275
|
+
this.throttledSendCount = 0;
|
|
1276
|
+
this.lifetimeReconnects++;
|
|
1277
|
+
this.sendsInCurrentWindow = 0;
|
|
1278
|
+
this.currentWindowStart = Date.now();
|
|
1279
|
+
if (this.rampTimer) {
|
|
1280
|
+
clearTimeout(this.rampTimer);
|
|
1281
|
+
}
|
|
1282
|
+
this.scheduleNextRampStep();
|
|
1283
|
+
}
|
|
1284
|
+
onDisconnect() { }
|
|
1285
|
+
scheduleNextRampStep() {
|
|
1286
|
+
if (this.currentStep >= this.config.rampSteps) {
|
|
1287
|
+
this.throttledSince = null;
|
|
1288
|
+
this.rampTimer = null;
|
|
1289
|
+
return;
|
|
1290
|
+
}
|
|
1291
|
+
const stepDuration = this.config.rampDurationMs / this.config.rampSteps;
|
|
1292
|
+
this.rampTimer = setTimeout(() => {
|
|
1293
|
+
this.currentStep++;
|
|
1294
|
+
this.scheduleNextRampStep();
|
|
1295
|
+
}, stepDuration);
|
|
1296
|
+
}
|
|
1297
|
+
getCurrentMultiplier() {
|
|
1298
|
+
if (!this.config.enabled || !this.throttledSince) {
|
|
1299
|
+
return 1;
|
|
1300
|
+
}
|
|
1301
|
+
const elapsed = Date.now() - this.throttledSince;
|
|
1302
|
+
if (elapsed >= this.config.rampDurationMs) {
|
|
1303
|
+
return 1;
|
|
1304
|
+
}
|
|
1305
|
+
const progress = this.currentStep / this.config.rampSteps;
|
|
1306
|
+
const multiplier = this.config.initialRateMultiplier + (1 - this.config.initialRateMultiplier) * progress;
|
|
1307
|
+
return Math.min(1, multiplier);
|
|
1308
|
+
}
|
|
1309
|
+
beforeSend() {
|
|
1310
|
+
if (!this.config.enabled || !this.throttledSince) {
|
|
1311
|
+
return { allowed: true };
|
|
1312
|
+
}
|
|
1313
|
+
const now = Date.now();
|
|
1314
|
+
const multiplier = this.getCurrentMultiplier();
|
|
1315
|
+
if (multiplier >= 1) {
|
|
1316
|
+
this.throttledSince = null;
|
|
1317
|
+
return { allowed: true };
|
|
1318
|
+
}
|
|
1319
|
+
if (now - this.currentWindowStart >= this.WINDOW_DURATION_MS) {
|
|
1320
|
+
this.sendsInCurrentWindow = 0;
|
|
1321
|
+
this.currentWindowStart = now;
|
|
1322
|
+
}
|
|
1323
|
+
const baselineRate = this.config.baselineRatePerMinute ? this.config.baselineRatePerMinute() : 8;
|
|
1324
|
+
const allowedInWindow = Math.max(1, Math.floor(baselineRate * multiplier));
|
|
1325
|
+
if (this.sendsInCurrentWindow >= allowedInWindow) {
|
|
1326
|
+
const windowRemaining = this.WINDOW_DURATION_MS - (now - this.currentWindowStart);
|
|
1327
|
+
return {
|
|
1328
|
+
allowed: false,
|
|
1329
|
+
reason: `Post-reconnect throttle: ${Math.floor(multiplier * 100)}% rate (${this.sendsInCurrentWindow}/${allowedInWindow} sends in window)`,
|
|
1330
|
+
retryAfterMs: windowRemaining
|
|
1331
|
+
};
|
|
1332
|
+
}
|
|
1333
|
+
this.sendsInCurrentWindow++;
|
|
1334
|
+
this.throttledSendCount++;
|
|
1335
|
+
return { allowed: true };
|
|
1336
|
+
}
|
|
1337
|
+
getStats() {
|
|
1338
|
+
const multiplier = this.getCurrentMultiplier();
|
|
1339
|
+
const isThrottled = this.throttledSince !== null && multiplier < 1;
|
|
1340
|
+
const remainingMs = isThrottled && this.throttledSince
|
|
1341
|
+
? Math.max(0, this.config.rampDurationMs - (Date.now() - this.throttledSince))
|
|
1342
|
+
: 0;
|
|
1343
|
+
return {
|
|
1344
|
+
isThrottled,
|
|
1345
|
+
currentMultiplier: multiplier,
|
|
1346
|
+
throttledSinceMs: this.throttledSince,
|
|
1347
|
+
remainingMs,
|
|
1348
|
+
throttledSendCount: this.throttledSendCount,
|
|
1349
|
+
lifetimeReconnects: this.lifetimeReconnects
|
|
1350
|
+
};
|
|
1351
|
+
}
|
|
1352
|
+
destroy() {
|
|
1353
|
+
if (this.rampTimer) {
|
|
1354
|
+
clearTimeout(this.rampTimer);
|
|
1355
|
+
this.rampTimer = null;
|
|
1356
|
+
}
|
|
1357
|
+
this.throttledSince = null;
|
|
1358
|
+
}
|
|
1359
|
+
};
|
|
1360
|
+
var DEFAULT_CONFIG10 = {
|
|
1361
|
+
canonical: 'pn',
|
|
1362
|
+
maxEntries: 1e4
|
|
1363
|
+
};
|
|
1364
|
+
var LidResolver = class {
|
|
1365
|
+
config;
|
|
1366
|
+
persistence;
|
|
1367
|
+
lidToPn = new Map();
|
|
1368
|
+
pnToLid = new Map();
|
|
1369
|
+
stats = {
|
|
1370
|
+
learnedFromEvents: 0,
|
|
1371
|
+
lookupsServed: 0,
|
|
1372
|
+
lookupMisses: 0
|
|
1373
|
+
};
|
|
1374
|
+
constructor(config = {}) {
|
|
1375
|
+
this.config = { ...DEFAULT_CONFIG10, ...config };
|
|
1376
|
+
this.persistence = config.persistence;
|
|
1377
|
+
if (this.persistence?.load) {
|
|
1378
|
+
void this.hydrate();
|
|
1379
|
+
}
|
|
1380
|
+
}
|
|
1381
|
+
learn(mapping) {
|
|
1382
|
+
let lid = mapping.lid ? this.normalizeJid(mapping.lid) : void 0;
|
|
1383
|
+
let pn = mapping.pn ? this.normalizeJid(mapping.pn) : void 0;
|
|
1384
|
+
const phone = mapping.phone;
|
|
1385
|
+
if (!lid || (!pn && !phone)) {
|
|
1386
|
+
return;
|
|
1387
|
+
}
|
|
1388
|
+
if (!pn && phone) {
|
|
1389
|
+
pn = `${phone}@s.whatsapp.net`;
|
|
1390
|
+
}
|
|
1391
|
+
if (!lid || !pn)
|
|
1392
|
+
return;
|
|
1393
|
+
if (!lid.endsWith('@lid'))
|
|
1394
|
+
return;
|
|
1395
|
+
if (!pn.endsWith('@s.whatsapp.net'))
|
|
1396
|
+
return;
|
|
1397
|
+
const existing = this.lidToPn.get(lid);
|
|
1398
|
+
if (existing) {
|
|
1399
|
+
existing.seenCount++;
|
|
1400
|
+
existing.learnedAt = Date.now();
|
|
1401
|
+
return;
|
|
1402
|
+
}
|
|
1403
|
+
const extractedPhone = phone || pn.split('@')[0];
|
|
1404
|
+
const newMapping = {
|
|
1405
|
+
lid,
|
|
1406
|
+
pn,
|
|
1407
|
+
phone: extractedPhone,
|
|
1408
|
+
learnedAt: Date.now(),
|
|
1409
|
+
seenCount: 1
|
|
1410
|
+
};
|
|
1411
|
+
if (this.lidToPn.size >= this.config.maxEntries) {
|
|
1412
|
+
this.evictLRU();
|
|
1413
|
+
}
|
|
1414
|
+
this.lidToPn.set(lid, newMapping);
|
|
1415
|
+
this.pnToLid.set(pn, lid);
|
|
1416
|
+
this.stats.learnedFromEvents++;
|
|
1417
|
+
if (this.persistence?.save) {
|
|
1418
|
+
void this.flush();
|
|
1419
|
+
}
|
|
1420
|
+
}
|
|
1421
|
+
resolveCanonical(jid) {
|
|
1422
|
+
const normalized = this.normalizeJid(jid);
|
|
1423
|
+
if (this.config.canonical === 'pn') {
|
|
1424
|
+
if (normalized.endsWith('@lid')) {
|
|
1425
|
+
const mapping = this.lidToPn.get(normalized);
|
|
1426
|
+
if (mapping) {
|
|
1427
|
+
this.stats.lookupsServed++;
|
|
1428
|
+
mapping.learnedAt = Date.now();
|
|
1429
|
+
return mapping.pn;
|
|
1430
|
+
}
|
|
1431
|
+
this.stats.lookupMisses++;
|
|
1432
|
+
return jid;
|
|
1433
|
+
}
|
|
1434
|
+
this.stats.lookupsServed++;
|
|
1435
|
+
return normalized;
|
|
1436
|
+
}
|
|
1437
|
+
else {
|
|
1438
|
+
if (normalized.endsWith('@s.whatsapp.net')) {
|
|
1439
|
+
const lid = this.pnToLid.get(normalized);
|
|
1440
|
+
if (lid) {
|
|
1441
|
+
this.stats.lookupsServed++;
|
|
1442
|
+
const mapping = this.lidToPn.get(lid);
|
|
1443
|
+
if (mapping) {
|
|
1444
|
+
mapping.learnedAt = Date.now();
|
|
1445
|
+
}
|
|
1446
|
+
return lid;
|
|
1447
|
+
}
|
|
1448
|
+
this.stats.lookupMisses++;
|
|
1449
|
+
return jid;
|
|
1450
|
+
}
|
|
1451
|
+
this.stats.lookupsServed++;
|
|
1452
|
+
return normalized;
|
|
1453
|
+
}
|
|
1454
|
+
}
|
|
1455
|
+
getLid(pn) {
|
|
1456
|
+
const normalized = this.normalizeJid(pn);
|
|
1457
|
+
const lid = this.pnToLid.get(normalized);
|
|
1458
|
+
if (lid) {
|
|
1459
|
+
const mapping = this.lidToPn.get(lid);
|
|
1460
|
+
if (mapping) {
|
|
1461
|
+
mapping.learnedAt = Date.now();
|
|
1462
|
+
}
|
|
1463
|
+
}
|
|
1464
|
+
return lid || null;
|
|
1465
|
+
}
|
|
1466
|
+
getPn(lid) {
|
|
1467
|
+
const normalized = this.normalizeJid(lid);
|
|
1468
|
+
const mapping = this.lidToPn.get(normalized);
|
|
1469
|
+
if (mapping) {
|
|
1470
|
+
mapping.learnedAt = Date.now();
|
|
1471
|
+
return mapping.pn;
|
|
1472
|
+
}
|
|
1473
|
+
return null;
|
|
1474
|
+
}
|
|
1475
|
+
getMapping(jid) {
|
|
1476
|
+
const normalized = this.normalizeJid(jid);
|
|
1477
|
+
const byLid = this.lidToPn.get(normalized);
|
|
1478
|
+
if (byLid) {
|
|
1479
|
+
byLid.learnedAt = Date.now();
|
|
1480
|
+
return byLid;
|
|
1481
|
+
}
|
|
1482
|
+
const lid = this.pnToLid.get(normalized);
|
|
1483
|
+
if (lid) {
|
|
1484
|
+
const mapping = this.lidToPn.get(lid);
|
|
1485
|
+
if (mapping) {
|
|
1486
|
+
mapping.learnedAt = Date.now();
|
|
1487
|
+
return mapping;
|
|
1488
|
+
}
|
|
1489
|
+
}
|
|
1490
|
+
return null;
|
|
1491
|
+
}
|
|
1492
|
+
async hydrate() {
|
|
1493
|
+
if (!this.persistence?.load)
|
|
1494
|
+
return;
|
|
1495
|
+
try {
|
|
1496
|
+
const stored = await this.persistence.load();
|
|
1497
|
+
if (!stored || typeof stored !== 'object')
|
|
1498
|
+
return;
|
|
1499
|
+
for (const [lid, serialized] of Object.entries(stored)) {
|
|
1500
|
+
if (typeof serialized === 'string') {
|
|
1501
|
+
const pn = serialized;
|
|
1502
|
+
const phone = pn.split('@')[0];
|
|
1503
|
+
const mapping = {
|
|
1504
|
+
lid,
|
|
1505
|
+
pn,
|
|
1506
|
+
phone,
|
|
1507
|
+
learnedAt: Date.now(),
|
|
1508
|
+
seenCount: 1
|
|
1509
|
+
};
|
|
1510
|
+
this.lidToPn.set(lid, mapping);
|
|
1511
|
+
this.pnToLid.set(pn, lid);
|
|
1512
|
+
}
|
|
1513
|
+
else if (typeof serialized === 'object' && serialized !== null) {
|
|
1514
|
+
const mapping = serialized;
|
|
1515
|
+
this.lidToPn.set(lid, mapping);
|
|
1516
|
+
this.pnToLid.set(mapping.pn, lid);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
1519
|
+
}
|
|
1520
|
+
catch (error) { }
|
|
1521
|
+
}
|
|
1522
|
+
async flush() {
|
|
1523
|
+
if (!this.persistence?.save)
|
|
1524
|
+
return;
|
|
1525
|
+
try {
|
|
1526
|
+
const toStore = {};
|
|
1527
|
+
for (const [lid, mapping] of this.lidToPn.entries()) {
|
|
1528
|
+
toStore[lid] = mapping;
|
|
1529
|
+
}
|
|
1530
|
+
await this.persistence.save(toStore);
|
|
1531
|
+
}
|
|
1532
|
+
catch (error) { }
|
|
1533
|
+
}
|
|
1534
|
+
getStats() {
|
|
1535
|
+
return {
|
|
1536
|
+
totalMappings: this.lidToPn.size,
|
|
1537
|
+
learnedFromEvents: this.stats.learnedFromEvents,
|
|
1538
|
+
lookupsServed: this.stats.lookupsServed,
|
|
1539
|
+
lookupMisses: this.stats.lookupMisses,
|
|
1540
|
+
canonicalForm: this.config.canonical
|
|
1541
|
+
};
|
|
1542
|
+
}
|
|
1543
|
+
reset() {
|
|
1544
|
+
this.lidToPn.clear();
|
|
1545
|
+
this.pnToLid.clear();
|
|
1546
|
+
this.stats = {
|
|
1547
|
+
learnedFromEvents: 0,
|
|
1548
|
+
lookupsServed: 0,
|
|
1549
|
+
lookupMisses: 0
|
|
1550
|
+
};
|
|
1551
|
+
}
|
|
1552
|
+
destroy() {
|
|
1553
|
+
this.reset();
|
|
1554
|
+
if (this.persistence?.save) {
|
|
1555
|
+
void this.flush();
|
|
1556
|
+
}
|
|
1557
|
+
}
|
|
1558
|
+
normalizeJid(jid) {
|
|
1559
|
+
return jid.replace(/:\d+@/, '@');
|
|
1560
|
+
}
|
|
1561
|
+
evictLRU() {
|
|
1562
|
+
let oldestLid = null;
|
|
1563
|
+
let oldestTime = Infinity;
|
|
1564
|
+
for (const [lid, mapping] of this.lidToPn.entries()) {
|
|
1565
|
+
if (mapping.learnedAt < oldestTime) {
|
|
1566
|
+
oldestTime = mapping.learnedAt;
|
|
1567
|
+
oldestLid = lid;
|
|
1568
|
+
}
|
|
1569
|
+
}
|
|
1570
|
+
if (oldestLid) {
|
|
1571
|
+
const mapping = this.lidToPn.get(oldestLid);
|
|
1572
|
+
if (mapping) {
|
|
1573
|
+
this.pnToLid.delete(mapping.pn);
|
|
1574
|
+
}
|
|
1575
|
+
this.lidToPn.delete(oldestLid);
|
|
1576
|
+
}
|
|
1577
|
+
}
|
|
1578
|
+
};
|
|
1579
|
+
var DEFAULT_CONFIG11 = {
|
|
1580
|
+
enabled: false,
|
|
1581
|
+
canonicalizeOutbound: true,
|
|
1582
|
+
learnFromEvents: true
|
|
1583
|
+
};
|
|
1584
|
+
var JidCanonicalizer = class {
|
|
1585
|
+
config;
|
|
1586
|
+
lidResolver;
|
|
1587
|
+
ownsResolver;
|
|
1588
|
+
stats = {
|
|
1589
|
+
outboundCanonicalized: 0,
|
|
1590
|
+
outboundPassthrough: 0,
|
|
1591
|
+
inboundLearned: 0,
|
|
1592
|
+
canonicalKeyHits: 0,
|
|
1593
|
+
canonicalKeyMisses: 0
|
|
1594
|
+
};
|
|
1595
|
+
constructor(config = {}) {
|
|
1596
|
+
this.config = { ...DEFAULT_CONFIG11, ...config };
|
|
1597
|
+
if (config.resolver) {
|
|
1598
|
+
this.lidResolver = config.resolver;
|
|
1599
|
+
this.ownsResolver = false;
|
|
1600
|
+
}
|
|
1601
|
+
else {
|
|
1602
|
+
this.lidResolver = new LidResolver(config.resolverConfig);
|
|
1603
|
+
this.ownsResolver = true;
|
|
1604
|
+
}
|
|
1605
|
+
}
|
|
1606
|
+
get resolver() {
|
|
1607
|
+
return this.lidResolver;
|
|
1608
|
+
}
|
|
1609
|
+
canonicalizeTarget(jid) {
|
|
1610
|
+
if (!this.config.enabled || !this.config.canonicalizeOutbound) {
|
|
1611
|
+
return jid;
|
|
1612
|
+
}
|
|
1613
|
+
const canonical = this.lidResolver.resolveCanonical(jid);
|
|
1614
|
+
if (canonical !== jid) {
|
|
1615
|
+
this.stats.outboundCanonicalized++;
|
|
1616
|
+
}
|
|
1617
|
+
else {
|
|
1618
|
+
this.stats.outboundPassthrough++;
|
|
1619
|
+
}
|
|
1620
|
+
return canonical;
|
|
1621
|
+
}
|
|
1622
|
+
canonicalKey(jid) {
|
|
1623
|
+
if (!jid || typeof jid !== 'string' || jid.trim() === '') {
|
|
1624
|
+
return 'thread:invalid';
|
|
1625
|
+
}
|
|
1626
|
+
const normalized = jid.trim().toLowerCase();
|
|
1627
|
+
const atIndex = normalized.indexOf('@');
|
|
1628
|
+
if (atIndex === -1) {
|
|
1629
|
+
return 'thread:invalid';
|
|
1630
|
+
}
|
|
1631
|
+
const user = normalized.substring(0, atIndex);
|
|
1632
|
+
const domain = normalized.substring(atIndex + 1);
|
|
1633
|
+
if (domain === 'g.us') {
|
|
1634
|
+
return `thread:group:${user}`;
|
|
1635
|
+
}
|
|
1636
|
+
if (domain === 'broadcast') {
|
|
1637
|
+
return `thread:broadcast:${user}`;
|
|
1638
|
+
}
|
|
1639
|
+
if (domain === 'newsletter') {
|
|
1640
|
+
return `thread:newsletter:${user}`;
|
|
1641
|
+
}
|
|
1642
|
+
if (domain === 's.whatsapp.net') {
|
|
1643
|
+
this.stats.canonicalKeyHits++;
|
|
1644
|
+
return `thread:${user}`;
|
|
1645
|
+
}
|
|
1646
|
+
if (domain === 'lid') {
|
|
1647
|
+
const mapping = this.lidResolver.getMapping(normalized);
|
|
1648
|
+
if (mapping?.pn) {
|
|
1649
|
+
const pnUser = mapping.pn.split('@')[0];
|
|
1650
|
+
this.stats.canonicalKeyHits++;
|
|
1651
|
+
return `thread:${pnUser}`;
|
|
1652
|
+
}
|
|
1653
|
+
else {
|
|
1654
|
+
this.stats.canonicalKeyMisses++;
|
|
1655
|
+
return `thread:lid:${user}`;
|
|
1656
|
+
}
|
|
1657
|
+
}
|
|
1658
|
+
return `thread:${domain}:${user}`;
|
|
1659
|
+
}
|
|
1660
|
+
onIncomingEvent(upsert) {
|
|
1661
|
+
if (!this.config.enabled || !this.config.learnFromEvents) {
|
|
1662
|
+
return;
|
|
1663
|
+
}
|
|
1664
|
+
for (const msg of upsert.messages || []) {
|
|
1665
|
+
this.learnFromMessage(msg);
|
|
1666
|
+
}
|
|
1667
|
+
}
|
|
1668
|
+
onMessageUpdate(updates) {
|
|
1669
|
+
if (!this.config.enabled || !this.config.learnFromEvents) {
|
|
1670
|
+
return;
|
|
1671
|
+
}
|
|
1672
|
+
for (const update of updates) {
|
|
1673
|
+
if (update.key) {
|
|
1674
|
+
this.learnFromMessageKey(update.key);
|
|
1675
|
+
}
|
|
1676
|
+
}
|
|
1677
|
+
}
|
|
1678
|
+
getStats() {
|
|
1679
|
+
return {
|
|
1680
|
+
resolver: this.lidResolver.getStats(),
|
|
1681
|
+
outboundCanonicalized: this.stats.outboundCanonicalized,
|
|
1682
|
+
outboundPassthrough: this.stats.outboundPassthrough,
|
|
1683
|
+
inboundLearned: this.stats.inboundLearned,
|
|
1684
|
+
canonicalKeyHits: this.stats.canonicalKeyHits,
|
|
1685
|
+
canonicalKeyMisses: this.stats.canonicalKeyMisses
|
|
1686
|
+
};
|
|
1687
|
+
}
|
|
1688
|
+
destroy() {
|
|
1689
|
+
if (this.ownsResolver) {
|
|
1690
|
+
this.lidResolver.destroy();
|
|
1691
|
+
}
|
|
1692
|
+
}
|
|
1693
|
+
learnFromMessage(msg) {
|
|
1694
|
+
if (!msg.key)
|
|
1695
|
+
return;
|
|
1696
|
+
this.learnFromMessageKey(msg.key);
|
|
1697
|
+
if (msg.participantPn && msg.key.participant) {
|
|
1698
|
+
this.lidResolver.learn({
|
|
1699
|
+
lid: msg.key.participant.endsWith('@lid') ? msg.key.participant : void 0,
|
|
1700
|
+
pn: msg.participantPn
|
|
1701
|
+
});
|
|
1702
|
+
this.stats.inboundLearned++;
|
|
1703
|
+
}
|
|
1704
|
+
}
|
|
1705
|
+
learnFromMessageKey(key) {
|
|
1706
|
+
if (!key)
|
|
1707
|
+
return;
|
|
1708
|
+
if (key.participant && key.participantPn) {
|
|
1709
|
+
if (key.participant.endsWith('@lid')) {
|
|
1710
|
+
this.lidResolver.learn({
|
|
1711
|
+
lid: key.participant,
|
|
1712
|
+
pn: key.participantPn
|
|
1713
|
+
});
|
|
1714
|
+
this.stats.inboundLearned++;
|
|
1715
|
+
}
|
|
1716
|
+
}
|
|
1717
|
+
if (key.remoteJid && key.senderPn) {
|
|
1718
|
+
if (key.remoteJid.endsWith('@lid')) {
|
|
1719
|
+
this.lidResolver.learn({
|
|
1720
|
+
lid: key.remoteJid,
|
|
1721
|
+
pn: key.senderPn
|
|
1722
|
+
});
|
|
1723
|
+
this.stats.inboundLearned++;
|
|
1724
|
+
}
|
|
1725
|
+
}
|
|
1726
|
+
if (key.participant && key.remoteJid) {
|
|
1727
|
+
if (key.participant.endsWith('@s.whatsapp.net') && key.remoteJid.endsWith('@lid')) {
|
|
1728
|
+
this.lidResolver.learn({
|
|
1729
|
+
lid: key.remoteJid,
|
|
1730
|
+
pn: key.participant
|
|
1731
|
+
});
|
|
1732
|
+
this.stats.inboundLearned++;
|
|
1733
|
+
}
|
|
1734
|
+
}
|
|
1735
|
+
}
|
|
1736
|
+
};
|
|
1737
|
+
function classifyDisconnect(statusCode) {
|
|
1738
|
+
if (statusCode === 401 || statusCode === 440) {
|
|
1739
|
+
return {
|
|
1740
|
+
category: 'fatal',
|
|
1741
|
+
shouldReconnect: false,
|
|
1742
|
+
message: 'Logged out \u2014 restart with QR code required',
|
|
1743
|
+
code: statusCode
|
|
1744
|
+
};
|
|
1745
|
+
}
|
|
1746
|
+
if (statusCode === 515) {
|
|
1747
|
+
return {
|
|
1748
|
+
category: 'fatal',
|
|
1749
|
+
shouldReconnect: false,
|
|
1750
|
+
message: 'Restart required by WhatsApp \u2014 client too old or protocol mismatch',
|
|
1751
|
+
code: statusCode
|
|
1752
|
+
};
|
|
1753
|
+
}
|
|
1754
|
+
if (statusCode === 405) {
|
|
1755
|
+
return {
|
|
1756
|
+
category: 'fatal',
|
|
1757
|
+
shouldReconnect: false,
|
|
1758
|
+
message: 'Method not allowed \u2014 server rejected connection method',
|
|
1759
|
+
code: statusCode
|
|
1760
|
+
};
|
|
1761
|
+
}
|
|
1762
|
+
if (statusCode === 409 || statusCode === 428) {
|
|
1763
|
+
return {
|
|
1764
|
+
category: 'fatal',
|
|
1765
|
+
shouldReconnect: false,
|
|
1766
|
+
message: 'Connection replaced \u2014 another device took over',
|
|
1767
|
+
code: statusCode
|
|
1768
|
+
};
|
|
1769
|
+
}
|
|
1770
|
+
if (statusCode === 412) {
|
|
1771
|
+
return {
|
|
1772
|
+
category: 'recoverable',
|
|
1773
|
+
shouldReconnect: true,
|
|
1774
|
+
backoffMs: 3e4,
|
|
1775
|
+
message: 'Precondition failed \u2014 auth state mismatch, retry after delay',
|
|
1776
|
+
code: statusCode
|
|
1777
|
+
};
|
|
1778
|
+
}
|
|
1779
|
+
if (statusCode === 429) {
|
|
1780
|
+
return {
|
|
1781
|
+
category: 'rate-limited',
|
|
1782
|
+
shouldReconnect: true,
|
|
1783
|
+
backoffMs: 3e5,
|
|
1784
|
+
message: 'Rate limited by WhatsApp \u2014 cool-off period required',
|
|
1785
|
+
code: statusCode
|
|
1786
|
+
};
|
|
1787
|
+
}
|
|
1788
|
+
if (statusCode === 503) {
|
|
1789
|
+
return {
|
|
1790
|
+
category: 'rate-limited',
|
|
1791
|
+
shouldReconnect: true,
|
|
1792
|
+
backoffMs: 6e4,
|
|
1793
|
+
message: 'WhatsApp service unavailable \u2014 temporary outage',
|
|
1794
|
+
code: statusCode
|
|
1795
|
+
};
|
|
1796
|
+
}
|
|
1797
|
+
if (statusCode === 408) {
|
|
1798
|
+
return {
|
|
1799
|
+
category: 'recoverable',
|
|
1800
|
+
shouldReconnect: true,
|
|
1801
|
+
backoffMs: 5e3,
|
|
1802
|
+
message: 'Connection timeout \u2014 network issue, safe to retry',
|
|
1803
|
+
code: statusCode
|
|
1804
|
+
};
|
|
1805
|
+
}
|
|
1806
|
+
if (statusCode === 500) {
|
|
1807
|
+
return {
|
|
1808
|
+
category: 'recoverable',
|
|
1809
|
+
shouldReconnect: true,
|
|
1810
|
+
backoffMs: 1e4,
|
|
1811
|
+
message: 'WhatsApp internal error \u2014 temporary server issue',
|
|
1812
|
+
code: statusCode
|
|
1813
|
+
};
|
|
1814
|
+
}
|
|
1815
|
+
if (statusCode === 1e3) {
|
|
1816
|
+
return {
|
|
1817
|
+
category: 'recoverable',
|
|
1818
|
+
shouldReconnect: true,
|
|
1819
|
+
backoffMs: 2e3,
|
|
1820
|
+
message: 'Connection closed gracefully \u2014 safe to reconnect',
|
|
1821
|
+
code: statusCode
|
|
1822
|
+
};
|
|
1823
|
+
}
|
|
1824
|
+
return {
|
|
1825
|
+
category: 'unknown',
|
|
1826
|
+
shouldReconnect: true,
|
|
1827
|
+
backoffMs: 15e3,
|
|
1828
|
+
message: `Unknown disconnect reason (code ${statusCode}) \u2014 reconnect with caution`,
|
|
1829
|
+
code: statusCode
|
|
1830
|
+
};
|
|
1831
|
+
}
|
|
1832
|
+
var DEFAULT_HEALTH_CONFIG = {
|
|
1833
|
+
badMacThreshold: 3,
|
|
1834
|
+
badMacWindowMs: 6e4
|
|
1835
|
+
};
|
|
1836
|
+
var SessionHealthMonitor = class {
|
|
1837
|
+
config;
|
|
1838
|
+
onDegraded;
|
|
1839
|
+
onRecovered;
|
|
1840
|
+
stats = {
|
|
1841
|
+
decryptSuccess: 0,
|
|
1842
|
+
decryptFail: 0,
|
|
1843
|
+
badMacCount: 0,
|
|
1844
|
+
isDegraded: false
|
|
1845
|
+
};
|
|
1846
|
+
badMacTimestamps = [];
|
|
1847
|
+
constructor(config = {}) {
|
|
1848
|
+
this.config = { ...DEFAULT_HEALTH_CONFIG, ...config };
|
|
1849
|
+
this.onDegraded = config.onDegraded;
|
|
1850
|
+
this.onRecovered = config.onRecovered;
|
|
1851
|
+
}
|
|
1852
|
+
recordDecryptSuccess() {
|
|
1853
|
+
this.stats.decryptSuccess++;
|
|
1854
|
+
this.checkRecovery();
|
|
1855
|
+
}
|
|
1856
|
+
recordDecryptFail(isBadMac = false) {
|
|
1857
|
+
this.stats.decryptFail++;
|
|
1858
|
+
if (isBadMac) {
|
|
1859
|
+
const now = Date.now();
|
|
1860
|
+
this.stats.badMacCount++;
|
|
1861
|
+
this.stats.lastBadMac = new Date(now);
|
|
1862
|
+
this.badMacTimestamps.push(now);
|
|
1863
|
+
const cutoff = now - this.config.badMacWindowMs;
|
|
1864
|
+
this.badMacTimestamps = this.badMacTimestamps.filter(ts => ts > cutoff);
|
|
1865
|
+
if (!this.stats.isDegraded && this.badMacTimestamps.length >= this.config.badMacThreshold) {
|
|
1866
|
+
this.stats.isDegraded = true;
|
|
1867
|
+
this.stats.degradedSince = new Date(now);
|
|
1868
|
+
this.onDegraded?.(this.getStats());
|
|
1869
|
+
}
|
|
1870
|
+
}
|
|
1871
|
+
}
|
|
1872
|
+
checkRecovery() {
|
|
1873
|
+
if (!this.stats.isDegraded)
|
|
1874
|
+
return;
|
|
1875
|
+
const now = Date.now();
|
|
1876
|
+
const cutoff = now - this.config.badMacWindowMs;
|
|
1877
|
+
this.badMacTimestamps = this.badMacTimestamps.filter(ts => ts > cutoff);
|
|
1878
|
+
if (this.badMacTimestamps.length < this.config.badMacThreshold) {
|
|
1879
|
+
this.stats.isDegraded = false;
|
|
1880
|
+
this.stats.degradedSince = void 0;
|
|
1881
|
+
this.onRecovered?.(this.getStats());
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
getStats() {
|
|
1885
|
+
return { ...this.stats };
|
|
1886
|
+
}
|
|
1887
|
+
reset() {
|
|
1888
|
+
this.stats = {
|
|
1889
|
+
decryptSuccess: 0,
|
|
1890
|
+
decryptFail: 0,
|
|
1891
|
+
badMacCount: 0,
|
|
1892
|
+
isDegraded: false
|
|
1893
|
+
};
|
|
1894
|
+
this.badMacTimestamps = [];
|
|
1895
|
+
}
|
|
1896
|
+
};
|
|
1897
|
+
function wrapWithSessionStability(sock, config = {}) {
|
|
1898
|
+
const { canonicalJidNormalization = true, healthMonitoring = true, health: healthConfig, lidResolver } = config;
|
|
1899
|
+
const healthMonitor = healthMonitoring ? new SessionHealthMonitor(healthConfig) : null;
|
|
1900
|
+
return new Proxy(sock, {
|
|
1901
|
+
get(target, prop) {
|
|
1902
|
+
if (prop === 'sendMessage' && canonicalJidNormalization && lidResolver) {
|
|
1903
|
+
return async (jid, content, options) => {
|
|
1904
|
+
const canonical = lidResolver.resolveCanonical(jid);
|
|
1905
|
+
return target.sendMessage(canonical, content, options);
|
|
1906
|
+
};
|
|
1907
|
+
}
|
|
1908
|
+
if (prop === 'sessionHealthStats' && healthMonitor) {
|
|
1909
|
+
return healthMonitor.getStats();
|
|
1910
|
+
}
|
|
1911
|
+
if (prop === 'sessionHealthMonitor' && healthMonitor) {
|
|
1912
|
+
return healthMonitor;
|
|
1913
|
+
}
|
|
1914
|
+
return target[prop];
|
|
1915
|
+
}
|
|
1916
|
+
});
|
|
1917
|
+
}
|
|
1918
|
+
var PRESETS = {
|
|
1919
|
+
conservative: {
|
|
1920
|
+
maxPerMinute: 5,
|
|
1921
|
+
maxPerHour: 100,
|
|
1922
|
+
maxPerDay: 800,
|
|
1923
|
+
minDelayMs: 2500,
|
|
1924
|
+
maxDelayMs: 7e3,
|
|
1925
|
+
newChatDelayMs: 4e3,
|
|
1926
|
+
warmupDays: 10,
|
|
1927
|
+
day1Limit: 15,
|
|
1928
|
+
growthFactor: 1.8,
|
|
1929
|
+
inactivityThresholdHours: 72,
|
|
1930
|
+
autoPauseAt: 'medium',
|
|
1931
|
+
groupMultiplier: 0.5,
|
|
1932
|
+
groupProfiles: false,
|
|
1933
|
+
logging: true
|
|
1934
|
+
},
|
|
1935
|
+
moderate: {
|
|
1936
|
+
maxPerMinute: 10,
|
|
1937
|
+
maxPerHour: 300,
|
|
1938
|
+
maxPerDay: 1500,
|
|
1939
|
+
minDelayMs: 1500,
|
|
1940
|
+
maxDelayMs: 5e3,
|
|
1941
|
+
newChatDelayMs: 3e3,
|
|
1942
|
+
warmupDays: 7,
|
|
1943
|
+
day1Limit: 20,
|
|
1944
|
+
growthFactor: 1.8,
|
|
1945
|
+
inactivityThresholdHours: 72,
|
|
1946
|
+
autoPauseAt: 'high',
|
|
1947
|
+
groupMultiplier: 0.7,
|
|
1948
|
+
groupProfiles: false,
|
|
1949
|
+
logging: true
|
|
1950
|
+
},
|
|
1951
|
+
aggressive: {
|
|
1952
|
+
maxPerMinute: 20,
|
|
1953
|
+
maxPerHour: 800,
|
|
1954
|
+
maxPerDay: 4e3,
|
|
1955
|
+
minDelayMs: 800,
|
|
1956
|
+
maxDelayMs: 3e3,
|
|
1957
|
+
newChatDelayMs: 2e3,
|
|
1958
|
+
warmupDays: 4,
|
|
1959
|
+
day1Limit: 35,
|
|
1960
|
+
growthFactor: 2,
|
|
1961
|
+
inactivityThresholdHours: 48,
|
|
1962
|
+
autoPauseAt: 'critical',
|
|
1963
|
+
groupMultiplier: 0.9,
|
|
1964
|
+
groupProfiles: false,
|
|
1965
|
+
logging: true
|
|
1966
|
+
}
|
|
1967
|
+
};
|
|
1968
|
+
function resolveConfig(input) {
|
|
1969
|
+
if (input === void 0) {
|
|
1970
|
+
return { ...PRESETS.conservative };
|
|
1971
|
+
}
|
|
1972
|
+
if (typeof input === 'string') {
|
|
1973
|
+
if (!(input in PRESETS)) {
|
|
1974
|
+
throw new Error(`Unknown preset "${input}". Valid: ${Object.keys(PRESETS).join(', ')}`);
|
|
1975
|
+
}
|
|
1976
|
+
return { ...PRESETS[input] };
|
|
1977
|
+
}
|
|
1978
|
+
const { preset = 'conservative', ...overrides } = input;
|
|
1979
|
+
if (!(preset in PRESETS)) {
|
|
1980
|
+
throw new Error(`Unknown preset "${preset}". Valid: ${Object.keys(PRESETS).join(', ')}`);
|
|
1981
|
+
}
|
|
1982
|
+
return { ...PRESETS[preset], ...overrides };
|
|
1983
|
+
}
|
|
1984
|
+
var KNOWN_CHATS_MAX = 1e3;
|
|
1985
|
+
var DEBOUNCE_MS = 5e3;
|
|
1986
|
+
var StateManager = class {
|
|
1987
|
+
path;
|
|
1988
|
+
debounceTimer = null;
|
|
1989
|
+
constructor(filePath) {
|
|
1990
|
+
this.path = filePath;
|
|
1991
|
+
}
|
|
1992
|
+
load() {
|
|
1993
|
+
try {
|
|
1994
|
+
const raw = fs.readFileSync(this.path, 'utf-8');
|
|
1995
|
+
const parsed = JSON.parse(raw);
|
|
1996
|
+
if (parsed.version !== 3) {
|
|
1997
|
+
process.stderr.write('[baileys-antiban] WARN: corrupt state file or version mismatch, starting fresh\n');
|
|
1998
|
+
return null;
|
|
1999
|
+
}
|
|
2000
|
+
return parsed;
|
|
2001
|
+
}
|
|
2002
|
+
catch {
|
|
2003
|
+
if (fs.existsSync(this.path)) {
|
|
2004
|
+
process.stderr.write('[baileys-antiban] WARN: corrupt state file, starting fresh\n');
|
|
2005
|
+
}
|
|
2006
|
+
return null;
|
|
2007
|
+
}
|
|
2008
|
+
}
|
|
2009
|
+
saveDebounced(state) {
|
|
2010
|
+
if (this.debounceTimer) {
|
|
2011
|
+
clearTimeout(this.debounceTimer);
|
|
2012
|
+
}
|
|
2013
|
+
this.debounceTimer = setTimeout(() => {
|
|
2014
|
+
this.writeFile(state);
|
|
2015
|
+
this.debounceTimer = null;
|
|
2016
|
+
}, DEBOUNCE_MS);
|
|
2017
|
+
}
|
|
2018
|
+
saveImmediate(state) {
|
|
2019
|
+
if (this.debounceTimer) {
|
|
2020
|
+
clearTimeout(this.debounceTimer);
|
|
2021
|
+
this.debounceTimer = null;
|
|
2022
|
+
}
|
|
2023
|
+
this.writeFile(state);
|
|
2024
|
+
}
|
|
2025
|
+
flush() {
|
|
2026
|
+
if (this.debounceTimer) {
|
|
2027
|
+
clearTimeout(this.debounceTimer);
|
|
2028
|
+
this.debounceTimer = null;
|
|
2029
|
+
}
|
|
2030
|
+
}
|
|
2031
|
+
destroy() {
|
|
2032
|
+
this.flush();
|
|
2033
|
+
}
|
|
2034
|
+
writeFile(state) {
|
|
2035
|
+
const toSave = {
|
|
2036
|
+
...state,
|
|
2037
|
+
savedAt: Date.now(),
|
|
2038
|
+
knownChats: state.knownChats.length > KNOWN_CHATS_MAX ? state.knownChats.slice(-KNOWN_CHATS_MAX) : state.knownChats
|
|
2039
|
+
};
|
|
2040
|
+
try {
|
|
2041
|
+
fs.writeFileSync(this.path, JSON.stringify(toSave, null, 2), 'utf-8');
|
|
2042
|
+
}
|
|
2043
|
+
catch (err) {
|
|
2044
|
+
process.stderr.write(`[baileys-antiban] WARN: failed to write state to ${this.path}: ${err}\n`);
|
|
2045
|
+
}
|
|
2046
|
+
}
|
|
2047
|
+
};
|
|
2048
|
+
function isGroup(jid) {
|
|
2049
|
+
return jid.endsWith('@g.us');
|
|
2050
|
+
}
|
|
2051
|
+
function isNewsletter(jid) {
|
|
2052
|
+
return jid.endsWith('@newsletter');
|
|
2053
|
+
}
|
|
2054
|
+
function isBroadcast(jid) {
|
|
2055
|
+
return jid === 'status@broadcast' || jid.endsWith('@broadcast');
|
|
2056
|
+
}
|
|
2057
|
+
function shouldUseGroupProfile(jid) {
|
|
2058
|
+
return isGroup(jid) || isNewsletter(jid);
|
|
2059
|
+
}
|
|
2060
|
+
function applyGroupMultiplier(limits, multiplier) {
|
|
2061
|
+
return {
|
|
2062
|
+
maxPerMinute: Math.max(1, Math.floor(limits.maxPerMinute * multiplier)),
|
|
2063
|
+
maxPerHour: Math.max(1, Math.floor(limits.maxPerHour * multiplier)),
|
|
2064
|
+
maxPerDay: Math.max(1, Math.floor(limits.maxPerDay * multiplier))
|
|
2065
|
+
};
|
|
2066
|
+
}
|
|
2067
|
+
function isLegacyConfig(cfg) {
|
|
2068
|
+
if (typeof cfg !== 'object' || cfg === null)
|
|
2069
|
+
return false;
|
|
2070
|
+
return ('rateLimiter' in cfg ||
|
|
2071
|
+
'warmUp' in cfg ||
|
|
2072
|
+
'health' in cfg ||
|
|
2073
|
+
'timelock' in cfg ||
|
|
2074
|
+
'replyRatio' in cfg ||
|
|
2075
|
+
'contactGraph' in cfg ||
|
|
2076
|
+
'presence' in cfg ||
|
|
2077
|
+
'retryTracker' in cfg ||
|
|
2078
|
+
'reconnectThrottle' in cfg ||
|
|
2079
|
+
'lidResolver' in cfg ||
|
|
2080
|
+
'jidCanonicalizer' in cfg ||
|
|
2081
|
+
'sessionStability' in cfg);
|
|
2082
|
+
}
|
|
2083
|
+
function mapLegacyToFlat(legacy) {
|
|
2084
|
+
process.stderr.write('[baileys-antiban] DEPRECATED: Nested config (v2 style) detected. Migrate to flat config: new AntiBan({ maxPerMinute: 8 }).\n');
|
|
2085
|
+
const flat = {};
|
|
2086
|
+
if (legacy.rateLimiter?.maxPerMinute !== void 0)
|
|
2087
|
+
flat.maxPerMinute = legacy.rateLimiter.maxPerMinute;
|
|
2088
|
+
if (legacy.rateLimiter?.maxPerHour !== void 0)
|
|
2089
|
+
flat.maxPerHour = legacy.rateLimiter.maxPerHour;
|
|
2090
|
+
if (legacy.rateLimiter?.maxPerDay !== void 0)
|
|
2091
|
+
flat.maxPerDay = legacy.rateLimiter.maxPerDay;
|
|
2092
|
+
if (legacy.rateLimiter?.minDelayMs !== void 0)
|
|
2093
|
+
flat.minDelayMs = legacy.rateLimiter.minDelayMs;
|
|
2094
|
+
if (legacy.rateLimiter?.maxDelayMs !== void 0)
|
|
2095
|
+
flat.maxDelayMs = legacy.rateLimiter.maxDelayMs;
|
|
2096
|
+
if (legacy.rateLimiter?.newChatDelayMs !== void 0)
|
|
2097
|
+
flat.newChatDelayMs = legacy.rateLimiter.newChatDelayMs;
|
|
2098
|
+
if (legacy.warmUp?.warmUpDays !== void 0)
|
|
2099
|
+
flat.warmupDays = legacy.warmUp.warmUpDays;
|
|
2100
|
+
if (legacy.warmUp?.day1Limit !== void 0)
|
|
2101
|
+
flat.day1Limit = legacy.warmUp.day1Limit;
|
|
2102
|
+
if (legacy.warmUp?.growthFactor !== void 0)
|
|
2103
|
+
flat.growthFactor = legacy.warmUp.growthFactor;
|
|
2104
|
+
if (legacy.logging !== void 0)
|
|
2105
|
+
flat.logging = legacy.logging;
|
|
2106
|
+
return flat;
|
|
2107
|
+
}
|
|
2108
|
+
var AntiBan = class {
|
|
2109
|
+
rateLimiter;
|
|
2110
|
+
warmUp;
|
|
2111
|
+
health;
|
|
2112
|
+
timelockGuard;
|
|
2113
|
+
replyRatioGuard;
|
|
2114
|
+
contactGraphWarmer;
|
|
2115
|
+
presenceChoreographer;
|
|
2116
|
+
retryTrackerModule;
|
|
2117
|
+
reconnectThrottleModule;
|
|
2118
|
+
lidResolverModule = null;
|
|
2119
|
+
jidCanonicalizerModule = null;
|
|
2120
|
+
sessionStabilityMonitor = null;
|
|
2121
|
+
stateManager = null;
|
|
2122
|
+
resolvedConfig;
|
|
2123
|
+
logging;
|
|
2124
|
+
stats = {
|
|
2125
|
+
messagesAllowed: 0,
|
|
2126
|
+
messagesBlocked: 0,
|
|
2127
|
+
totalDelayMs: 0
|
|
2128
|
+
};
|
|
2129
|
+
constructor(input, warmUpStateArg) {
|
|
2130
|
+
let flatConfig;
|
|
2131
|
+
let legacyPassthrough = null;
|
|
2132
|
+
let warmUpState = warmUpStateArg;
|
|
2133
|
+
if (isLegacyConfig(input)) {
|
|
2134
|
+
legacyPassthrough = input;
|
|
2135
|
+
flatConfig = mapLegacyToFlat(legacyPassthrough);
|
|
2136
|
+
}
|
|
2137
|
+
else {
|
|
2138
|
+
flatConfig = {};
|
|
2139
|
+
legacyPassthrough = null;
|
|
2140
|
+
}
|
|
2141
|
+
const cfg = isLegacyConfig(input) ? resolveConfig(flatConfig) : resolveConfig(input);
|
|
2142
|
+
this.resolvedConfig = cfg;
|
|
2143
|
+
let savedState = null;
|
|
2144
|
+
if (cfg.persist) {
|
|
2145
|
+
this.stateManager = new StateManager(cfg.persist);
|
|
2146
|
+
savedState = this.stateManager.load();
|
|
2147
|
+
if (savedState) {
|
|
2148
|
+
warmUpState = savedState.warmup;
|
|
2149
|
+
}
|
|
2150
|
+
}
|
|
2151
|
+
this.logging = cfg.logging ?? true;
|
|
2152
|
+
this._log = (msg) => { if (this.logging)
|
|
2153
|
+
process.stdout.write(`[antiban] ${msg}\n`); };
|
|
2154
|
+
this.rateLimiter = new RateLimiter({
|
|
2155
|
+
maxPerMinute: cfg.maxPerMinute,
|
|
2156
|
+
maxPerHour: cfg.maxPerHour,
|
|
2157
|
+
maxPerDay: cfg.maxPerDay,
|
|
2158
|
+
minDelayMs: cfg.minDelayMs,
|
|
2159
|
+
maxDelayMs: cfg.maxDelayMs,
|
|
2160
|
+
newChatDelayMs: cfg.newChatDelayMs,
|
|
2161
|
+
...(legacyPassthrough?.rateLimiter || {})
|
|
2162
|
+
});
|
|
2163
|
+
if (savedState?.knownChats) {
|
|
2164
|
+
this.rateLimiter.restoreKnownChats(savedState.knownChats);
|
|
2165
|
+
}
|
|
2166
|
+
this.warmUp = new WarmUp({
|
|
2167
|
+
warmUpDays: cfg.warmupDays,
|
|
2168
|
+
day1Limit: cfg.day1Limit,
|
|
2169
|
+
growthFactor: cfg.growthFactor,
|
|
2170
|
+
inactivityThresholdHours: cfg.inactivityThresholdHours,
|
|
2171
|
+
...(legacyPassthrough?.warmUp || {})
|
|
2172
|
+
}, warmUpState);
|
|
2173
|
+
this.health = new HealthMonitor({
|
|
2174
|
+
autoPauseAt: cfg.autoPauseAt,
|
|
2175
|
+
...(legacyPassthrough?.health || {}),
|
|
2176
|
+
onRiskChange: status => {
|
|
2177
|
+
const emoji = { low: '\u{1F7E2}', medium: '\u{1F7E1}', high: '\u{1F7E0}', critical: '\u{1F534}' };
|
|
2178
|
+
this._log(`${emoji[status.risk]} Risk level: ${status.risk.toUpperCase()} (score: ${status.score})`);
|
|
2179
|
+
this._log(status.recommendation);
|
|
2180
|
+
status.reasons.forEach(r => this._log(` \u2192 ${r}`));
|
|
2181
|
+
legacyPassthrough?.health?.onRiskChange?.(status);
|
|
2182
|
+
}
|
|
2183
|
+
});
|
|
2184
|
+
this.timelockGuard = new TimelockGuard({
|
|
2185
|
+
...(legacyPassthrough?.timelock || {}),
|
|
2186
|
+
onTimelockDetected: state => {
|
|
2187
|
+
this.health.recordReachoutTimelock(state.enforcementType);
|
|
2188
|
+
this._log(`REACHOUT TIMELOCKED \u2014 ${state.enforcementType || 'unknown'}, expires ${state.expiresAt?.toISOString() || 'unknown'}`);
|
|
2189
|
+
legacyPassthrough?.timelock?.onTimelockDetected?.(state);
|
|
2190
|
+
},
|
|
2191
|
+
onTimelockLifted: state => {
|
|
2192
|
+
this._log('Timelock lifted \u2014 resuming new contact messages');
|
|
2193
|
+
legacyPassthrough?.timelock?.onTimelockLifted?.(state);
|
|
2194
|
+
}
|
|
2195
|
+
});
|
|
2196
|
+
this.replyRatioGuard = new ReplyRatioGuard(legacyPassthrough?.replyRatio);
|
|
2197
|
+
this.contactGraphWarmer = new ContactGraphWarmer(legacyPassthrough?.contactGraph);
|
|
2198
|
+
this.presenceChoreographer = new PresenceChoreographer(legacyPassthrough?.presence);
|
|
2199
|
+
this.retryTrackerModule = new RetryReasonTracker({
|
|
2200
|
+
...(legacyPassthrough?.retryTracker || {}),
|
|
2201
|
+
onSpiral: (msgId, reason) => {
|
|
2202
|
+
this._log(`\u26A0\uFE0F Message ${msgId} stuck in retry spiral (${reason})`);
|
|
2203
|
+
legacyPassthrough?.retryTracker?.onSpiral?.(msgId, reason);
|
|
2204
|
+
}
|
|
2205
|
+
});
|
|
2206
|
+
this.reconnectThrottleModule = new PostReconnectThrottle({
|
|
2207
|
+
...(legacyPassthrough?.reconnectThrottle || {}),
|
|
2208
|
+
baselineRatePerMinute: () => this.rateLimiter.getStats().limits.perMinute
|
|
2209
|
+
});
|
|
2210
|
+
if (legacyPassthrough?.jidCanonicalizer?.enabled) {
|
|
2211
|
+
if (legacyPassthrough.jidCanonicalizer.resolver) {
|
|
2212
|
+
this.jidCanonicalizerModule = new JidCanonicalizer(legacyPassthrough.jidCanonicalizer);
|
|
2213
|
+
this.lidResolverModule = legacyPassthrough.jidCanonicalizer.resolver;
|
|
2214
|
+
}
|
|
2215
|
+
else {
|
|
2216
|
+
const resolverConfig = legacyPassthrough.lidResolver || legacyPassthrough.jidCanonicalizer.resolverConfig;
|
|
2217
|
+
const resolver = new LidResolver(resolverConfig);
|
|
2218
|
+
this.lidResolverModule = resolver;
|
|
2219
|
+
this.jidCanonicalizerModule = new JidCanonicalizer({
|
|
2220
|
+
...legacyPassthrough.jidCanonicalizer,
|
|
2221
|
+
resolver
|
|
2222
|
+
});
|
|
2223
|
+
}
|
|
2224
|
+
}
|
|
2225
|
+
else if (legacyPassthrough?.lidResolver) {
|
|
2226
|
+
this.lidResolverModule = new LidResolver(legacyPassthrough.lidResolver);
|
|
2227
|
+
}
|
|
2228
|
+
if (legacyPassthrough?.sessionStability?.enabled) {
|
|
2229
|
+
const healthConfig = {
|
|
2230
|
+
badMacThreshold: legacyPassthrough.sessionStability.badMacThreshold,
|
|
2231
|
+
badMacWindowMs: legacyPassthrough.sessionStability.badMacWindowMs,
|
|
2232
|
+
onDegraded: stats => {
|
|
2233
|
+
this._log(`\u{1F534} SESSION DEGRADED \u2014 Bad MAC rate: ${stats.badMacCount} in last ${legacyPassthrough?.sessionStability?.badMacWindowMs || 6e4}ms`);
|
|
2234
|
+
this._log('Consider restarting session or switching to LID-based canonical form');
|
|
2235
|
+
},
|
|
2236
|
+
onRecovered: () => {
|
|
2237
|
+
this._log('\u{1F7E2} SESSION RECOVERED \u2014 decrypt success rate improved');
|
|
2238
|
+
}
|
|
2239
|
+
};
|
|
2240
|
+
this.sessionStabilityMonitor = new SessionHealthMonitor(healthConfig);
|
|
2241
|
+
}
|
|
2242
|
+
}
|
|
2243
|
+
async beforeSend(recipient, content) {
|
|
2244
|
+
// JEDA 10 MENIT (600.000 ms) SEBELUM MENGIRIM PESAN
|
|
2245
|
+
await new Promise(resolve => setTimeout(resolve, 600000));
|
|
2246
|
+
|
|
2247
|
+
const healthStatus = this.health.getStatus();
|
|
2248
|
+
if (this.health.isPaused()) {
|
|
2249
|
+
this.stats.messagesBlocked++;
|
|
2250
|
+
this._log(`\u26D4 BLOCKED \u2014 health risk too high (${healthStatus.risk})`);
|
|
2251
|
+
return {
|
|
2252
|
+
allowed: false,
|
|
2253
|
+
delayMs: 0,
|
|
2254
|
+
reason: `Health risk ${healthStatus.risk}: ${healthStatus.recommendation}`,
|
|
2255
|
+
health: healthStatus
|
|
2256
|
+
};
|
|
2257
|
+
}
|
|
2258
|
+
const timelockDecision = this.timelockGuard.canSend(recipient);
|
|
2259
|
+
if (!timelockDecision.allowed) {
|
|
2260
|
+
this.stats.messagesBlocked++;
|
|
2261
|
+
this._log(`TIMELOCKED \u2014 ${timelockDecision.reason}`);
|
|
2262
|
+
return {
|
|
2263
|
+
allowed: false,
|
|
2264
|
+
delayMs: 0,
|
|
2265
|
+
reason: timelockDecision.reason,
|
|
2266
|
+
health: healthStatus
|
|
2267
|
+
};
|
|
2268
|
+
}
|
|
2269
|
+
if (!this.warmUp.canSend()) {
|
|
2270
|
+
this.stats.messagesBlocked++;
|
|
2271
|
+
const warmUpStatus = this.warmUp.getStatus();
|
|
2272
|
+
this._log(`\u23F3 BLOCKED \u2014 warm-up day ${warmUpStatus.day}/${warmUpStatus.totalDays}, limit reached (${warmUpStatus.todaySent}/${warmUpStatus.todayLimit})`);
|
|
2273
|
+
return {
|
|
2274
|
+
allowed: false,
|
|
2275
|
+
delayMs: 0,
|
|
2276
|
+
reason: `Warm-up limit: ${warmUpStatus.todaySent}/${warmUpStatus.todayLimit} messages today (day ${warmUpStatus.day})`,
|
|
2277
|
+
health: healthStatus,
|
|
2278
|
+
warmUpDay: warmUpStatus.day
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
const contactGraphDecision = this.contactGraphWarmer.canMessage(recipient);
|
|
2282
|
+
if (!contactGraphDecision.allowed) {
|
|
2283
|
+
this.stats.messagesBlocked++;
|
|
2284
|
+
this._log(`\u{1F4CA} BLOCKED \u2014 contact graph: ${contactGraphDecision.reason}`);
|
|
2285
|
+
return {
|
|
2286
|
+
allowed: false,
|
|
2287
|
+
delayMs: 0,
|
|
2288
|
+
reason: `Contact graph: ${contactGraphDecision.reason}`,
|
|
2289
|
+
health: healthStatus
|
|
2290
|
+
};
|
|
2291
|
+
}
|
|
2292
|
+
const replyRatioDecision = this.replyRatioGuard.beforeSend(recipient);
|
|
2293
|
+
if (!replyRatioDecision.allowed) {
|
|
2294
|
+
this.stats.messagesBlocked++;
|
|
2295
|
+
this._log(`\u{1F4AC} BLOCKED \u2014 reply ratio: ${replyRatioDecision.reason}`);
|
|
2296
|
+
return {
|
|
2297
|
+
allowed: false,
|
|
2298
|
+
delayMs: 0,
|
|
2299
|
+
reason: `Reply ratio: ${replyRatioDecision.reason}`,
|
|
2300
|
+
health: healthStatus
|
|
2301
|
+
};
|
|
2302
|
+
}
|
|
2303
|
+
const reconnectThrottleDecision = this.reconnectThrottleModule.beforeSend();
|
|
2304
|
+
if (!reconnectThrottleDecision.allowed) {
|
|
2305
|
+
this.stats.messagesBlocked++;
|
|
2306
|
+
this._log(`\u{1F504} BLOCKED \u2014 reconnect throttle: ${reconnectThrottleDecision.reason}`);
|
|
2307
|
+
return {
|
|
2308
|
+
allowed: false,
|
|
2309
|
+
delayMs: reconnectThrottleDecision.retryAfterMs || 0,
|
|
2310
|
+
reason: reconnectThrottleDecision.reason || 'Post-reconnect throttle',
|
|
2311
|
+
health: healthStatus
|
|
2312
|
+
};
|
|
2313
|
+
}
|
|
2314
|
+
if (this.resolvedConfig.groupProfiles && shouldUseGroupProfile(recipient)) {
|
|
2315
|
+
const groupLimits = applyGroupMultiplier({
|
|
2316
|
+
maxPerMinute: this.resolvedConfig.maxPerMinute,
|
|
2317
|
+
maxPerHour: this.resolvedConfig.maxPerHour,
|
|
2318
|
+
maxPerDay: this.resolvedConfig.maxPerDay
|
|
2319
|
+
}, this.resolvedConfig.groupMultiplier);
|
|
2320
|
+
const stats = this.rateLimiter.getStats();
|
|
2321
|
+
if (stats.lastMinute >= groupLimits.maxPerMinute ||
|
|
2322
|
+
stats.lastHour >= groupLimits.maxPerHour ||
|
|
2323
|
+
stats.lastDay >= groupLimits.maxPerDay) {
|
|
2324
|
+
this.stats.messagesBlocked++;
|
|
2325
|
+
this._log(`\u{1F6AB} BLOCKED \u2014 group rate limit exceeded for ${recipient}`);
|
|
2326
|
+
return { allowed: false, delayMs: 0, reason: 'Group rate limit exceeded', health: healthStatus };
|
|
2327
|
+
}
|
|
2328
|
+
}
|
|
2329
|
+
let delay = await this.rateLimiter.getDelay(recipient, content);
|
|
2330
|
+
if (delay === -1) {
|
|
2331
|
+
this.stats.messagesBlocked++;
|
|
2332
|
+
this._log(`\u{1F6AB} BLOCKED \u2014 rate limit or identical message spam`);
|
|
2333
|
+
return {
|
|
2334
|
+
allowed: false,
|
|
2335
|
+
delayMs: 0,
|
|
2336
|
+
reason: 'Rate limit exceeded or identical message spam detected',
|
|
2337
|
+
health: healthStatus
|
|
2338
|
+
};
|
|
2339
|
+
}
|
|
2340
|
+
const activityFactor = this.presenceChoreographer.getCurrentActivityFactor();
|
|
2341
|
+
if (activityFactor < 1) {
|
|
2342
|
+
const multiplier = Math.min(5, 1 / activityFactor);
|
|
2343
|
+
delay = Math.floor(delay * multiplier);
|
|
2344
|
+
}
|
|
2345
|
+
const distractionCheck = this.presenceChoreographer.shouldPauseForDistraction();
|
|
2346
|
+
if (distractionCheck.pause) {
|
|
2347
|
+
delay += distractionCheck.durationMs;
|
|
2348
|
+
this._log(`\u23F8\uFE0F Distraction pause: +${Math.floor(distractionCheck.durationMs / 6e4)}min`);
|
|
2349
|
+
}
|
|
2350
|
+
const offlineCheck = this.presenceChoreographer.shouldTakeOfflineGap();
|
|
2351
|
+
if (offlineCheck.offline) {
|
|
2352
|
+
delay += offlineCheck.durationMs;
|
|
2353
|
+
this._log(`\u{1F4F4} Offline gap: +${Math.floor(offlineCheck.durationMs / 6e4)}min`);
|
|
2354
|
+
}
|
|
2355
|
+
this.stats.totalDelayMs += delay;
|
|
2356
|
+
return {
|
|
2357
|
+
allowed: true,
|
|
2358
|
+
delayMs: delay,
|
|
2359
|
+
health: healthStatus
|
|
2360
|
+
};
|
|
2361
|
+
}
|
|
2362
|
+
afterSend(recipient, content) {
|
|
2363
|
+
this.rateLimiter.record(recipient, content);
|
|
2364
|
+
this.warmUp.record();
|
|
2365
|
+
this.replyRatioGuard.recordSent(recipient);
|
|
2366
|
+
this.stats.messagesAllowed++;
|
|
2367
|
+
this.persistStateDebounced();
|
|
2368
|
+
}
|
|
2369
|
+
afterSendFailed(error) {
|
|
2370
|
+
this.health.recordMessageFailed(error);
|
|
2371
|
+
}
|
|
2372
|
+
onDisconnect(reason) {
|
|
2373
|
+
this.health.recordDisconnect(reason);
|
|
2374
|
+
this.reconnectThrottleModule.onDisconnect();
|
|
2375
|
+
const reasonStr = String(reason);
|
|
2376
|
+
if (reasonStr === '403' || reasonStr === '401' || reasonStr === 'forbidden' || reasonStr === 'loggedOut') {
|
|
2377
|
+
this.persistStateImmediate();
|
|
2378
|
+
}
|
|
2379
|
+
}
|
|
2380
|
+
onReconnect() {
|
|
2381
|
+
this.health.recordReconnect();
|
|
2382
|
+
this.reconnectThrottleModule.onReconnect();
|
|
2383
|
+
}
|
|
2384
|
+
onIncomingMessage(jid, msgText) {
|
|
2385
|
+
this.replyRatioGuard.recordReceived(jid);
|
|
2386
|
+
this.contactGraphWarmer.onIncomingMessage(jid);
|
|
2387
|
+
return this.replyRatioGuard.suggestReply(jid, msgText);
|
|
2388
|
+
}
|
|
2389
|
+
getStats() {
|
|
2390
|
+
const stats = {
|
|
2391
|
+
...this.stats,
|
|
2392
|
+
health: this.health.getStatus(),
|
|
2393
|
+
warmUp: this.warmUp.getStatus(),
|
|
2394
|
+
rateLimiter: this.rateLimiter.getStats()
|
|
2395
|
+
};
|
|
2396
|
+
if (this.replyRatioGuard['config']?.enabled) {
|
|
2397
|
+
stats.replyRatio = this.replyRatioGuard.getStats();
|
|
2398
|
+
}
|
|
2399
|
+
if (this.contactGraphWarmer['config']?.enabled) {
|
|
2400
|
+
stats.contactGraph = this.contactGraphWarmer.getStats();
|
|
2401
|
+
}
|
|
2402
|
+
if (this.presenceChoreographer['config']?.enabled) {
|
|
2403
|
+
stats.presence = this.presenceChoreographer.getStats();
|
|
2404
|
+
}
|
|
2405
|
+
if (this.retryTrackerModule['config']?.enabled) {
|
|
2406
|
+
stats.retryTracker = this.retryTrackerModule.getStats();
|
|
2407
|
+
}
|
|
2408
|
+
if (this.reconnectThrottleModule['config']?.enabled) {
|
|
2409
|
+
stats.reconnectThrottle = this.reconnectThrottleModule.getStats();
|
|
2410
|
+
}
|
|
2411
|
+
if (this.lidResolverModule) {
|
|
2412
|
+
stats.lidResolver = this.lidResolverModule.getStats();
|
|
2413
|
+
}
|
|
2414
|
+
if (this.jidCanonicalizerModule) {
|
|
2415
|
+
stats.jidCanonicalizer = this.jidCanonicalizerModule.getStats();
|
|
2416
|
+
}
|
|
2417
|
+
if (this.sessionStabilityMonitor) {
|
|
2418
|
+
stats.sessionStability = this.sessionStabilityMonitor.getStats();
|
|
2419
|
+
}
|
|
2420
|
+
return stats;
|
|
2421
|
+
}
|
|
2422
|
+
get timelock() {
|
|
2423
|
+
return this.timelockGuard;
|
|
2424
|
+
}
|
|
2425
|
+
get replyRatio() {
|
|
2426
|
+
return this.replyRatioGuard;
|
|
2427
|
+
}
|
|
2428
|
+
get contactGraph() {
|
|
2429
|
+
return this.contactGraphWarmer;
|
|
2430
|
+
}
|
|
2431
|
+
get presence() {
|
|
2432
|
+
return this.presenceChoreographer;
|
|
2433
|
+
}
|
|
2434
|
+
get retryTracker() {
|
|
2435
|
+
return this.retryTrackerModule;
|
|
2436
|
+
}
|
|
2437
|
+
get reconnectThrottle() {
|
|
2438
|
+
return this.reconnectThrottleModule;
|
|
2439
|
+
}
|
|
2440
|
+
get lidResolver() {
|
|
2441
|
+
return this.lidResolverModule;
|
|
2442
|
+
}
|
|
2443
|
+
get jidCanonicalizer() {
|
|
2444
|
+
return this.jidCanonicalizerModule;
|
|
2445
|
+
}
|
|
2446
|
+
get sessionStability() {
|
|
2447
|
+
return this.sessionStabilityMonitor;
|
|
2448
|
+
}
|
|
2449
|
+
exportWarmUpState() {
|
|
2450
|
+
return this.warmUp.exportState();
|
|
2451
|
+
}
|
|
2452
|
+
pause() {
|
|
2453
|
+
this.health.setPaused(true);
|
|
2454
|
+
this._log('\u23F8\uFE0F Sending paused manually');
|
|
2455
|
+
}
|
|
2456
|
+
resume() {
|
|
2457
|
+
this.health.setPaused(false);
|
|
2458
|
+
this._log('\u25B6\uFE0F Sending resumed');
|
|
2459
|
+
}
|
|
2460
|
+
reset() {
|
|
2461
|
+
this.timelockGuard.reset();
|
|
2462
|
+
this.health.reset();
|
|
2463
|
+
this.warmUp.reset();
|
|
2464
|
+
this.replyRatioGuard.reset();
|
|
2465
|
+
this.contactGraphWarmer.reset();
|
|
2466
|
+
this.presenceChoreographer.reset();
|
|
2467
|
+
this.retryTrackerModule.destroy();
|
|
2468
|
+
this.reconnectThrottleModule.destroy();
|
|
2469
|
+
this.stats = { messagesAllowed: 0, messagesBlocked: 0, totalDelayMs: 0 };
|
|
2470
|
+
this._log('\u{1F504} Reset \u2014 starting fresh warm-up');
|
|
2471
|
+
}
|
|
2472
|
+
persistStateDebounced() {
|
|
2473
|
+
if (!this.stateManager)
|
|
2474
|
+
return;
|
|
2475
|
+
const state = {
|
|
2476
|
+
warmup: this.warmUp.exportState(),
|
|
2477
|
+
knownChats: Array.from(this.rateLimiter.getKnownChats()),
|
|
2478
|
+
savedAt: Date.now(),
|
|
2479
|
+
version: 3
|
|
2480
|
+
};
|
|
2481
|
+
this.stateManager.saveDebounced(state);
|
|
2482
|
+
}
|
|
2483
|
+
persistStateImmediate() {
|
|
2484
|
+
if (!this.stateManager)
|
|
2485
|
+
return;
|
|
2486
|
+
const state = {
|
|
2487
|
+
warmup: this.warmUp.exportState(),
|
|
2488
|
+
knownChats: Array.from(this.rateLimiter.getKnownChats()),
|
|
2489
|
+
savedAt: Date.now(),
|
|
2490
|
+
version: 3
|
|
2491
|
+
};
|
|
2492
|
+
this.stateManager.saveImmediate(state);
|
|
2493
|
+
}
|
|
2494
|
+
destroy() {
|
|
2495
|
+
this.stateManager?.destroy();
|
|
2496
|
+
this.timelockGuard.reset();
|
|
2497
|
+
this.replyRatioGuard.reset();
|
|
2498
|
+
this.contactGraphWarmer.reset();
|
|
2499
|
+
this.presenceChoreographer.reset();
|
|
2500
|
+
this.retryTrackerModule.destroy();
|
|
2501
|
+
this.reconnectThrottleModule.destroy();
|
|
2502
|
+
this.jidCanonicalizerModule?.destroy();
|
|
2503
|
+
this.lidResolverModule?.destroy();
|
|
2504
|
+
this.sessionStabilityMonitor?.reset();
|
|
2505
|
+
this._log('\u{1F9F9} Destroyed \u2014 all timers cleared');
|
|
2506
|
+
}
|
|
2507
|
+
};
|
|
2508
|
+
var LidFirstResolver = class {
|
|
2509
|
+
lidToPhone = new Map();
|
|
2510
|
+
phoneToLid = new Map();
|
|
2511
|
+
loadFromAuthDir(authDir) {
|
|
2512
|
+
try {
|
|
2513
|
+
if (!fs2.existsSync(authDir)) {
|
|
2514
|
+
return;
|
|
2515
|
+
}
|
|
2516
|
+
const files = fs2.readdirSync(authDir);
|
|
2517
|
+
const reverseMappingFiles = files.filter(f => f.startsWith('lid-mapping-') && f.endsWith('_reverse.json'));
|
|
2518
|
+
for (const file of reverseMappingFiles) {
|
|
2519
|
+
const filePath = path.join(authDir, file);
|
|
2520
|
+
const content = fs2.readFileSync(filePath, 'utf-8');
|
|
2521
|
+
const data = JSON.parse(content);
|
|
2522
|
+
for (const [lid, pnJid] of Object.entries(data)) {
|
|
2523
|
+
if (typeof pnJid === 'string') {
|
|
2524
|
+
const phone = this.extractPhone(pnJid);
|
|
2525
|
+
if (phone && lid.endsWith('@lid')) {
|
|
2526
|
+
const mapping = {
|
|
2527
|
+
lid: this.normalizeLid(lid),
|
|
2528
|
+
phone,
|
|
2529
|
+
learnedAt: Date.now(),
|
|
2530
|
+
source: 'auth-dir'
|
|
2531
|
+
};
|
|
2532
|
+
this.lidToPhone.set(mapping.lid, mapping);
|
|
2533
|
+
this.phoneToLid.set(phone, mapping.lid);
|
|
2534
|
+
}
|
|
2535
|
+
}
|
|
2536
|
+
}
|
|
2537
|
+
}
|
|
2538
|
+
}
|
|
2539
|
+
catch (error) { }
|
|
2540
|
+
}
|
|
2541
|
+
learnFromEvent(event) {
|
|
2542
|
+
try {
|
|
2543
|
+
if (event.key?.remoteJid) {
|
|
2544
|
+
const jid = event.key.remoteJid;
|
|
2545
|
+
this.learnJid(jid, 'event');
|
|
2546
|
+
}
|
|
2547
|
+
if (event.key?.participant) {
|
|
2548
|
+
const jid = event.key.participant;
|
|
2549
|
+
this.learnJid(jid, 'event');
|
|
2550
|
+
}
|
|
2551
|
+
if (event.id) {
|
|
2552
|
+
this.learnJid(event.id, 'event');
|
|
2553
|
+
}
|
|
2554
|
+
if (event.pushName && event.key?.remoteJid) {
|
|
2555
|
+
this.learnJid(event.key.remoteJid, 'event');
|
|
2556
|
+
}
|
|
2557
|
+
}
|
|
2558
|
+
catch (error) { }
|
|
2559
|
+
}
|
|
2560
|
+
resolveToLID(phoneOrJid) {
|
|
2561
|
+
const phone = this.extractPhone(phoneOrJid);
|
|
2562
|
+
if (!phone)
|
|
2563
|
+
return null;
|
|
2564
|
+
return this.phoneToLid.get(phone) || null;
|
|
2565
|
+
}
|
|
2566
|
+
resolveToPhone(lid) {
|
|
2567
|
+
const normalized = this.normalizeLid(lid);
|
|
2568
|
+
const mapping = this.lidToPhone.get(normalized);
|
|
2569
|
+
return mapping ? mapping.phone : null;
|
|
2570
|
+
}
|
|
2571
|
+
getMapping(jid) {
|
|
2572
|
+
const normalized = this.normalizeLid(jid);
|
|
2573
|
+
const byLid = this.lidToPhone.get(normalized);
|
|
2574
|
+
if (byLid)
|
|
2575
|
+
return byLid;
|
|
2576
|
+
const phone = this.extractPhone(jid);
|
|
2577
|
+
if (phone) {
|
|
2578
|
+
const lid = this.phoneToLid.get(phone);
|
|
2579
|
+
if (lid)
|
|
2580
|
+
return this.lidToPhone.get(lid) || null;
|
|
2581
|
+
}
|
|
2582
|
+
return null;
|
|
2583
|
+
}
|
|
2584
|
+
size() {
|
|
2585
|
+
return this.lidToPhone.size;
|
|
2586
|
+
}
|
|
2587
|
+
clear() {
|
|
2588
|
+
this.lidToPhone.clear();
|
|
2589
|
+
this.phoneToLid.clear();
|
|
2590
|
+
}
|
|
2591
|
+
learnJid(_jid, _source) { }
|
|
2592
|
+
extractPhone(jid) {
|
|
2593
|
+
if (!jid)
|
|
2594
|
+
return null;
|
|
2595
|
+
let cleaned = jid.replace('@s.whatsapp.net', '');
|
|
2596
|
+
cleaned = cleaned.replace(/:\d+$/, '');
|
|
2597
|
+
if (/^\d+$/.test(cleaned)) {
|
|
2598
|
+
return cleaned;
|
|
2599
|
+
}
|
|
2600
|
+
return null;
|
|
2601
|
+
}
|
|
2602
|
+
normalizeLid(lid) {
|
|
2603
|
+
return lid.replace(/:\d+@/, '@');
|
|
2604
|
+
}
|
|
2605
|
+
};
|
|
2606
|
+
function createLidFirstResolver() {
|
|
2607
|
+
return new LidFirstResolver();
|
|
2608
|
+
}
|
|
2609
|
+
var MessageRetryReason;
|
|
2610
|
+
(function (MessageRetryReason2) {
|
|
2611
|
+
MessageRetryReason2[(MessageRetryReason2['UnknownError'] = 0)] = 'UnknownError';
|
|
2612
|
+
MessageRetryReason2[(MessageRetryReason2['GenericError'] = 1)] = 'GenericError';
|
|
2613
|
+
MessageRetryReason2[(MessageRetryReason2['SignalErrorInvalidKeyId'] = 3)] = 'SignalErrorInvalidKeyId';
|
|
2614
|
+
MessageRetryReason2[(MessageRetryReason2['SignalErrorInvalidMessage'] = 4)] = 'SignalErrorInvalidMessage';
|
|
2615
|
+
MessageRetryReason2[(MessageRetryReason2['SignalErrorNoSession'] = 5)] = 'SignalErrorNoSession';
|
|
2616
|
+
MessageRetryReason2[(MessageRetryReason2['SignalErrorBadMac'] = 7)] = 'SignalErrorBadMac';
|
|
2617
|
+
MessageRetryReason2[(MessageRetryReason2['MessageExpired'] = 8)] = 'MessageExpired';
|
|
2618
|
+
MessageRetryReason2[(MessageRetryReason2['DecryptionError'] = 9)] = 'DecryptionError';
|
|
2619
|
+
})(MessageRetryReason || (MessageRetryReason = {}));
|
|
2620
|
+
var MAC_ERROR_CODES = new Set([
|
|
2621
|
+
MessageRetryReason.SignalErrorBadMac,
|
|
2622
|
+
MessageRetryReason.SignalErrorInvalidMessage,
|
|
2623
|
+
MessageRetryReason.SignalErrorNoSession,
|
|
2624
|
+
MessageRetryReason.SignalErrorInvalidKeyId
|
|
2625
|
+
]);
|
|
2626
|
+
function parseRetryReason(code) {
|
|
2627
|
+
if (code === void 0 || code === null) {
|
|
2628
|
+
return MessageRetryReason.UnknownError;
|
|
2629
|
+
}
|
|
2630
|
+
const n = typeof code === 'string' ? parseInt(code, 10) : code;
|
|
2631
|
+
if (isNaN(n)) {
|
|
2632
|
+
return MessageRetryReason.UnknownError;
|
|
2633
|
+
}
|
|
2634
|
+
if (Object.values(MessageRetryReason).includes(n)) {
|
|
2635
|
+
return n;
|
|
2636
|
+
}
|
|
2637
|
+
return MessageRetryReason.UnknownError;
|
|
2638
|
+
}
|
|
2639
|
+
function isMacError(reason) {
|
|
2640
|
+
return MAC_ERROR_CODES.has(reason);
|
|
2641
|
+
}
|
|
2642
|
+
function getRetryReasonDescription(reason) {
|
|
2643
|
+
switch (reason) {
|
|
2644
|
+
case MessageRetryReason.UnknownError:
|
|
2645
|
+
return 'Unknown error';
|
|
2646
|
+
case MessageRetryReason.GenericError:
|
|
2647
|
+
return 'Generic error';
|
|
2648
|
+
case MessageRetryReason.SignalErrorInvalidKeyId:
|
|
2649
|
+
return 'Invalid key ID \u2014 peer prekey rotated';
|
|
2650
|
+
case MessageRetryReason.SignalErrorInvalidMessage:
|
|
2651
|
+
return 'Invalid message format';
|
|
2652
|
+
case MessageRetryReason.SignalErrorNoSession:
|
|
2653
|
+
return 'No session \u2014 peer not initialized';
|
|
2654
|
+
case MessageRetryReason.SignalErrorBadMac:
|
|
2655
|
+
return 'Bad MAC \u2014 encryption session mismatch';
|
|
2656
|
+
case MessageRetryReason.MessageExpired:
|
|
2657
|
+
return 'Message expired \u2014 too old to decrypt';
|
|
2658
|
+
case MessageRetryReason.DecryptionError:
|
|
2659
|
+
return 'Decryption failed';
|
|
2660
|
+
default:
|
|
2661
|
+
return `Unknown reason code ${reason}`;
|
|
2662
|
+
}
|
|
2663
|
+
}
|
|
2664
|
+
function wrapSocket(sock, config, warmUpState, wrapOptions) {
|
|
2665
|
+
const antiban = new AntiBan(config, warmUpState);
|
|
2666
|
+
const options = {
|
|
2667
|
+
autoRespondToIncoming: false,
|
|
2668
|
+
...wrapOptions
|
|
2669
|
+
};
|
|
2670
|
+
if (typeof sock.ev.process === 'function') {
|
|
2671
|
+
sock.ev.process(async (events) => {
|
|
2672
|
+
if (events['connection.update']) {
|
|
2673
|
+
const update = events['connection.update'];
|
|
2674
|
+
if (update.connection === 'close') {
|
|
2675
|
+
const reason = update.lastDisconnect?.error?.output?.statusCode || 'unknown';
|
|
2676
|
+
antiban.onDisconnect(reason);
|
|
2677
|
+
}
|
|
2678
|
+
if (update.connection === 'open') {
|
|
2679
|
+
antiban.onReconnect();
|
|
2680
|
+
}
|
|
2681
|
+
if (update.reachoutTimeLock) {
|
|
2682
|
+
antiban.timelock.onTimelockUpdate({
|
|
2683
|
+
isActive: update.reachoutTimeLock.isActive,
|
|
2684
|
+
timeEnforcementEnds: update.reachoutTimeLock.timeEnforcementEnds,
|
|
2685
|
+
enforcementType: update.reachoutTimeLock.enforcementType
|
|
2686
|
+
});
|
|
2687
|
+
}
|
|
2688
|
+
}
|
|
2689
|
+
if (events['messages.update']) {
|
|
2690
|
+
const updates = events['messages.update'];
|
|
2691
|
+
for (const update of updates) {
|
|
2692
|
+
if (update?.update?.messageStubParameters) {
|
|
2693
|
+
const params = update.update.messageStubParameters;
|
|
2694
|
+
if (params.includes(463) || params.includes('463')) {
|
|
2695
|
+
antiban.timelock.record463Error();
|
|
2696
|
+
}
|
|
2697
|
+
}
|
|
2698
|
+
antiban.retryTracker.onMessageUpdate(update);
|
|
2699
|
+
}
|
|
2700
|
+
antiban.jidCanonicalizer?.onMessageUpdate(updates);
|
|
2701
|
+
}
|
|
2702
|
+
if (events['messages.upsert']) {
|
|
2703
|
+
const { messages } = events['messages.upsert'];
|
|
2704
|
+
antiban.jidCanonicalizer?.onIncomingEvent(events['messages.upsert']);
|
|
2705
|
+
for (const msg of messages || []) {
|
|
2706
|
+
const jid = msg.key?.remoteJid;
|
|
2707
|
+
if (!jid)
|
|
2708
|
+
continue;
|
|
2709
|
+
antiban.timelock.registerKnownChat(jid);
|
|
2710
|
+
const isSelf = msg.key?.fromMe || false;
|
|
2711
|
+
if (isSelf)
|
|
2712
|
+
continue;
|
|
2713
|
+
const msgText = msg.message?.conversation ||
|
|
2714
|
+
msg.message?.extendedTextMessage?.text ||
|
|
2715
|
+
msg.message?.imageMessage?.caption ||
|
|
2716
|
+
msg.message?.videoMessage?.caption ||
|
|
2717
|
+
'';
|
|
2718
|
+
const replySuggestion = antiban.onIncomingMessage(jid, msgText);
|
|
2719
|
+
if (options.autoRespondToIncoming && replySuggestion.shouldReply && replySuggestion.suggestedText) {
|
|
2720
|
+
const replyDelay = Math.floor(Math.random() * 12e3) + 3e3;
|
|
2721
|
+
setTimeout(async () => {
|
|
2722
|
+
try {
|
|
2723
|
+
await sock.sendMessage(jid, { text: replySuggestion.suggestedText });
|
|
2724
|
+
}
|
|
2725
|
+
catch (error) { }
|
|
2726
|
+
}, replyDelay);
|
|
2727
|
+
}
|
|
2728
|
+
}
|
|
2729
|
+
}
|
|
2730
|
+
});
|
|
2731
|
+
}
|
|
2732
|
+
else {
|
|
2733
|
+
sock.ev.on('connection.update', update => {
|
|
2734
|
+
if (update.connection === 'close') {
|
|
2735
|
+
const reason = update.lastDisconnect?.error?.output?.statusCode || 'unknown';
|
|
2736
|
+
antiban.onDisconnect(reason);
|
|
2737
|
+
}
|
|
2738
|
+
if (update.connection === 'open') {
|
|
2739
|
+
antiban.onReconnect();
|
|
2740
|
+
}
|
|
2741
|
+
if (update.reachoutTimeLock) {
|
|
2742
|
+
antiban.timelock.onTimelockUpdate({
|
|
2743
|
+
isActive: update.reachoutTimeLock.isActive,
|
|
2744
|
+
timeEnforcementEnds: update.reachoutTimeLock.timeEnforcementEnds,
|
|
2745
|
+
enforcementType: update.reachoutTimeLock.enforcementType
|
|
2746
|
+
});
|
|
2747
|
+
}
|
|
2748
|
+
});
|
|
2749
|
+
sock.ev.on('messages.update', updates => {
|
|
2750
|
+
for (const update of updates) {
|
|
2751
|
+
if (update?.update?.messageStubParameters) {
|
|
2752
|
+
const params = update.update.messageStubParameters;
|
|
2753
|
+
if (params.includes(463) || params.includes('463')) {
|
|
2754
|
+
antiban.timelock.record463Error();
|
|
2755
|
+
}
|
|
2756
|
+
}
|
|
2757
|
+
antiban.retryTracker.onMessageUpdate(update);
|
|
2758
|
+
}
|
|
2759
|
+
antiban.jidCanonicalizer?.onMessageUpdate(updates);
|
|
2760
|
+
});
|
|
2761
|
+
sock.ev.on('messages.upsert', upsert => {
|
|
2762
|
+
const { messages } = upsert;
|
|
2763
|
+
antiban.jidCanonicalizer?.onIncomingEvent(upsert);
|
|
2764
|
+
for (const msg of messages || []) {
|
|
2765
|
+
const jid = msg.key?.remoteJid;
|
|
2766
|
+
if (!jid)
|
|
2767
|
+
continue;
|
|
2768
|
+
antiban.timelock.registerKnownChat(jid);
|
|
2769
|
+
const isSelf = msg.key?.fromMe || false;
|
|
2770
|
+
if (isSelf)
|
|
2771
|
+
continue;
|
|
2772
|
+
const msgText = msg.message?.conversation ||
|
|
2773
|
+
msg.message?.extendedTextMessage?.text ||
|
|
2774
|
+
msg.message?.imageMessage?.caption ||
|
|
2775
|
+
msg.message?.videoMessage?.caption ||
|
|
2776
|
+
'';
|
|
2777
|
+
const replySuggestion = antiban.onIncomingMessage(jid, msgText);
|
|
2778
|
+
if (options.autoRespondToIncoming && replySuggestion.shouldReply && replySuggestion.suggestedText) {
|
|
2779
|
+
const replyDelay = Math.floor(Math.random() * 12e3) + 3e3;
|
|
2780
|
+
setTimeout(async () => {
|
|
2781
|
+
try {
|
|
2782
|
+
await sock.sendMessage(jid, { text: replySuggestion.suggestedText });
|
|
2783
|
+
}
|
|
2784
|
+
catch (error) { }
|
|
2785
|
+
}, replyDelay);
|
|
2786
|
+
}
|
|
2787
|
+
}
|
|
2788
|
+
});
|
|
2789
|
+
}
|
|
2790
|
+
const originalSendMessage = sock.sendMessage.bind(sock);
|
|
2791
|
+
const wrappedSendMessage = async (jid, content, options2) => {
|
|
2792
|
+
const canonicalJid = antiban.jidCanonicalizer?.canonicalizeTarget(jid) || jid;
|
|
2793
|
+
const text = content?.text || content?.caption || content?.image?.caption || '';
|
|
2794
|
+
const decision = await antiban.beforeSend(canonicalJid, text);
|
|
2795
|
+
if (!decision.allowed) {
|
|
2796
|
+
throw new Error(`[baileys-antiban] Message blocked: ${decision.reason}`);
|
|
2797
|
+
}
|
|
2798
|
+
if (decision.delayMs > 0) {
|
|
2799
|
+
await new Promise(resolve => setTimeout(resolve, decision.delayMs));
|
|
2800
|
+
}
|
|
2801
|
+
try {
|
|
2802
|
+
const result = await originalSendMessage(canonicalJid, content, options2);
|
|
2803
|
+
antiban.afterSend(canonicalJid, text);
|
|
2804
|
+
antiban.timelock.registerKnownChat(canonicalJid);
|
|
2805
|
+
if (result?.key?.id) {
|
|
2806
|
+
antiban.retryTracker.clear(result.key.id);
|
|
2807
|
+
}
|
|
2808
|
+
return result;
|
|
2809
|
+
}
|
|
2810
|
+
catch (error) {
|
|
2811
|
+
antiban.afterSendFailed(error instanceof Error ? error.message : String(error));
|
|
2812
|
+
throw error;
|
|
2813
|
+
}
|
|
2814
|
+
};
|
|
2815
|
+
const wrapped = Object.create(sock);
|
|
2816
|
+
wrapped.sendMessage = wrappedSendMessage;
|
|
2817
|
+
wrapped.antiban = antiban;
|
|
2818
|
+
wrapped.antiban.destroy = antiban.destroy.bind(antiban);
|
|
2819
|
+
return wrapped;
|
|
2820
|
+
}
|
|
2821
|
+
var DEFAULT_CONFIG12 = {
|
|
2822
|
+
maxAttempts: 3,
|
|
2823
|
+
retryBaseDelayMs: 3e4,
|
|
2824
|
+
maxQueueSize: 1e3,
|
|
2825
|
+
priorityOrder: true
|
|
2826
|
+
};
|
|
2827
|
+
var MessageQueue = class extends import_events.EventEmitter {
|
|
2828
|
+
config;
|
|
2829
|
+
queue = [];
|
|
2830
|
+
processing = false;
|
|
2831
|
+
sendFn = null;
|
|
2832
|
+
drainTimer = null;
|
|
2833
|
+
idCounter = 0;
|
|
2834
|
+
constructor(config = {}) {
|
|
2835
|
+
super();
|
|
2836
|
+
this.config = { ...DEFAULT_CONFIG12, ...config };
|
|
2837
|
+
}
|
|
2838
|
+
setSendFunction(fn) {
|
|
2839
|
+
this.sendFn = fn;
|
|
2840
|
+
}
|
|
2841
|
+
add(recipient, content, options) {
|
|
2842
|
+
if (this.queue.length >= this.config.maxQueueSize) {
|
|
2843
|
+
throw new Error(`Queue full (${this.config.maxQueueSize} messages)`);
|
|
2844
|
+
}
|
|
2845
|
+
const id = `msg_${Date.now()}_${++this.idCounter}`;
|
|
2846
|
+
const message = {
|
|
2847
|
+
id,
|
|
2848
|
+
recipient,
|
|
2849
|
+
content,
|
|
2850
|
+
priority: options?.priority || 'normal',
|
|
2851
|
+
addedAt: Date.now(),
|
|
2852
|
+
attempts: 0,
|
|
2853
|
+
maxAttempts: this.config.maxAttempts,
|
|
2854
|
+
scheduledFor: options?.scheduledFor?.getTime(),
|
|
2855
|
+
metadata: options?.metadata
|
|
2856
|
+
};
|
|
2857
|
+
this.queue.push(message);
|
|
2858
|
+
this.sortQueue();
|
|
2859
|
+
this.emit('added', message);
|
|
2860
|
+
return id;
|
|
2861
|
+
}
|
|
2862
|
+
addBulk(recipients, content, options) {
|
|
2863
|
+
return recipients.map(r => this.add(r, content, options));
|
|
2864
|
+
}
|
|
2865
|
+
start(intervalMs = 1e3) {
|
|
2866
|
+
if (this.drainTimer)
|
|
2867
|
+
return;
|
|
2868
|
+
this.drainTimer = setInterval(() => this.processNext(), intervalMs);
|
|
2869
|
+
this.emit('started');
|
|
2870
|
+
}
|
|
2871
|
+
stop() {
|
|
2872
|
+
if (this.drainTimer) {
|
|
2873
|
+
clearInterval(this.drainTimer);
|
|
2874
|
+
this.drainTimer = null;
|
|
2875
|
+
}
|
|
2876
|
+
this.emit('stopped');
|
|
2877
|
+
}
|
|
2878
|
+
destroy() {
|
|
2879
|
+
this.stop();
|
|
2880
|
+
}
|
|
2881
|
+
async processNext() {
|
|
2882
|
+
if (this.processing || !this.sendFn)
|
|
2883
|
+
return;
|
|
2884
|
+
const now = Date.now();
|
|
2885
|
+
const message = this.queue.find(m => !m.scheduledFor || m.scheduledFor <= now);
|
|
2886
|
+
if (!message)
|
|
2887
|
+
return;
|
|
2888
|
+
this.processing = true;
|
|
2889
|
+
try {
|
|
2890
|
+
message.attempts++;
|
|
2891
|
+
await this.sendFn(message.recipient, message.content);
|
|
2892
|
+
this.queue = this.queue.filter(m => m.id !== message.id);
|
|
2893
|
+
this.emit('sent', message);
|
|
2894
|
+
}
|
|
2895
|
+
catch (err) {
|
|
2896
|
+
message.lastError = err.message;
|
|
2897
|
+
if (err.message?.includes('baileys-antiban')) {
|
|
2898
|
+
message.attempts--;
|
|
2899
|
+
this.emit('delayed', message, err.message);
|
|
2900
|
+
}
|
|
2901
|
+
else if (message.attempts >= message.maxAttempts) {
|
|
2902
|
+
this.queue = this.queue.filter(m => m.id !== message.id);
|
|
2903
|
+
this.emit('failed', message, err.message);
|
|
2904
|
+
}
|
|
2905
|
+
else {
|
|
2906
|
+
const backoff = this.config.retryBaseDelayMs * Math.pow(2, message.attempts - 1);
|
|
2907
|
+
message.scheduledFor = Date.now() + backoff;
|
|
2908
|
+
this.emit('retry', message, message.attempts, backoff);
|
|
2909
|
+
}
|
|
2910
|
+
}
|
|
2911
|
+
finally {
|
|
2912
|
+
this.processing = false;
|
|
2913
|
+
}
|
|
2914
|
+
}
|
|
2915
|
+
getStats() {
|
|
2916
|
+
const now = Date.now();
|
|
2917
|
+
return {
|
|
2918
|
+
total: this.queue.length,
|
|
2919
|
+
pending: this.queue.filter(m => !m.scheduledFor || m.scheduledFor <= now).length,
|
|
2920
|
+
scheduled: this.queue.filter(m => m.scheduledFor && m.scheduledFor > now).length,
|
|
2921
|
+
byPriority: {
|
|
2922
|
+
high: this.queue.filter(m => m.priority === 'high').length,
|
|
2923
|
+
normal: this.queue.filter(m => m.priority === 'normal').length,
|
|
2924
|
+
low: this.queue.filter(m => m.priority === 'low').length
|
|
2925
|
+
},
|
|
2926
|
+
processing: this.processing,
|
|
2927
|
+
isRunning: this.drainTimer !== null
|
|
2928
|
+
};
|
|
2929
|
+
}
|
|
2930
|
+
clear() {
|
|
2931
|
+
const count = this.queue.length;
|
|
2932
|
+
this.queue = [];
|
|
2933
|
+
this.emit('cleared', count);
|
|
2934
|
+
}
|
|
2935
|
+
remove(id) {
|
|
2936
|
+
const before = this.queue.length;
|
|
2937
|
+
this.queue = this.queue.filter(m => m.id !== id);
|
|
2938
|
+
return this.queue.length < before;
|
|
2939
|
+
}
|
|
2940
|
+
export() {
|
|
2941
|
+
return [...this.queue];
|
|
2942
|
+
}
|
|
2943
|
+
import(messages) {
|
|
2944
|
+
this.queue = [...messages];
|
|
2945
|
+
this.sortQueue();
|
|
2946
|
+
}
|
|
2947
|
+
sortQueue() {
|
|
2948
|
+
if (!this.config.priorityOrder)
|
|
2949
|
+
return;
|
|
2950
|
+
const priorityWeight = { high: 0, normal: 1, low: 2 };
|
|
2951
|
+
this.queue.sort((a, b) => {
|
|
2952
|
+
const pDiff = priorityWeight[a.priority] - priorityWeight[b.priority];
|
|
2953
|
+
if (pDiff !== 0)
|
|
2954
|
+
return pDiff;
|
|
2955
|
+
return a.addedAt - b.addedAt;
|
|
2956
|
+
});
|
|
2957
|
+
}
|
|
2958
|
+
};
|
|
2959
|
+
var DEFAULT_CONFIG13 = {
|
|
2960
|
+
zeroWidthChars: true,
|
|
2961
|
+
punctuationVariation: true,
|
|
2962
|
+
emojiPadding: false,
|
|
2963
|
+
synonyms: false
|
|
2964
|
+
};
|
|
2965
|
+
var ZERO_WIDTH = [
|
|
2966
|
+
'\u200B',
|
|
2967
|
+
'\u200C',
|
|
2968
|
+
'\u200D',
|
|
2969
|
+
'\uFEFF'
|
|
2970
|
+
];
|
|
2971
|
+
var SYNONYMS = {
|
|
2972
|
+
hello: ['hi', 'hey', 'howdy'],
|
|
2973
|
+
hi: ['hello', 'hey', 'howdy'],
|
|
2974
|
+
thanks: ['thank you', 'thx', 'cheers'],
|
|
2975
|
+
please: ['kindly', 'pls'],
|
|
2976
|
+
great: ['awesome', 'excellent', 'wonderful'],
|
|
2977
|
+
good: ['great', 'nice', 'fine'],
|
|
2978
|
+
buy: ['purchase', 'get', 'grab'],
|
|
2979
|
+
sell: ['offer', 'list'],
|
|
2980
|
+
price: ['cost', 'amount', 'value'],
|
|
2981
|
+
available: ['in stock', 'on offer'],
|
|
2982
|
+
check: ['look at', 'see', 'view'],
|
|
2983
|
+
join: ['participate', 'enter', 'come to'],
|
|
2984
|
+
start: ['begin', 'kick off', 'commence'],
|
|
2985
|
+
end: ['finish', 'close', 'conclude'],
|
|
2986
|
+
bid: ['offer', 'place a bid'],
|
|
2987
|
+
win: ['secure', 'take home'],
|
|
2988
|
+
item: ['lot', 'piece', 'product']
|
|
2989
|
+
};
|
|
2990
|
+
var ContentVariator = class {
|
|
2991
|
+
config;
|
|
2992
|
+
counter = 0;
|
|
2993
|
+
constructor(config = {}) {
|
|
2994
|
+
this.config = { ...DEFAULT_CONFIG13, ...config };
|
|
2995
|
+
}
|
|
2996
|
+
vary(text) {
|
|
2997
|
+
let result = text;
|
|
2998
|
+
this.counter++;
|
|
2999
|
+
if (this.config.customVariator) {
|
|
3000
|
+
return this.config.customVariator(result, this.counter);
|
|
3001
|
+
}
|
|
3002
|
+
if (this.config.synonyms) {
|
|
3003
|
+
result = this.applySynonyms(result);
|
|
3004
|
+
}
|
|
3005
|
+
if (this.config.zeroWidthChars) {
|
|
3006
|
+
result = this.addZeroWidth(result);
|
|
3007
|
+
}
|
|
3008
|
+
if (this.config.punctuationVariation) {
|
|
3009
|
+
result = this.varyPunctuation(result);
|
|
3010
|
+
}
|
|
3011
|
+
if (this.config.emojiPadding) {
|
|
3012
|
+
result = this.addEmojiPadding(result);
|
|
3013
|
+
}
|
|
3014
|
+
return result;
|
|
3015
|
+
}
|
|
3016
|
+
varyBulk(text, count) {
|
|
3017
|
+
const results = [];
|
|
3018
|
+
const seen = new Set();
|
|
3019
|
+
for (let i = 0; i < count; i++) {
|
|
3020
|
+
let variation = this.vary(text);
|
|
3021
|
+
let attempts = 0;
|
|
3022
|
+
while (seen.has(variation) && attempts < 10) {
|
|
3023
|
+
variation = this.vary(text);
|
|
3024
|
+
attempts++;
|
|
3025
|
+
}
|
|
3026
|
+
seen.add(variation);
|
|
3027
|
+
results.push(variation);
|
|
3028
|
+
}
|
|
3029
|
+
return results;
|
|
3030
|
+
}
|
|
3031
|
+
addZeroWidth(text) {
|
|
3032
|
+
const words = text.split(' ');
|
|
3033
|
+
if (words.length < 2)
|
|
3034
|
+
return text;
|
|
3035
|
+
const positions = this.randomPositions(words.length - 1, Math.min(2, words.length - 1));
|
|
3036
|
+
return words
|
|
3037
|
+
.map((word, i) => {
|
|
3038
|
+
if (positions.includes(i)) {
|
|
3039
|
+
const zwc = ZERO_WIDTH[Math.floor(Math.random() * ZERO_WIDTH.length)];
|
|
3040
|
+
return word + zwc;
|
|
3041
|
+
}
|
|
3042
|
+
return word;
|
|
3043
|
+
})
|
|
3044
|
+
.join(' ');
|
|
3045
|
+
}
|
|
3046
|
+
varyPunctuation(text) {
|
|
3047
|
+
const variations = [
|
|
3048
|
+
() => text + ' ',
|
|
3049
|
+
() => text + ' ',
|
|
3050
|
+
() => (text.endsWith('.') ? text.slice(0, -1) : text + '.'),
|
|
3051
|
+
() => text,
|
|
3052
|
+
() => (text.charAt(0) === text.charAt(0).toUpperCase() ? text.charAt(0).toLowerCase() + text.slice(1) : text)
|
|
3053
|
+
];
|
|
3054
|
+
return variations[this.counter % variations.length]();
|
|
3055
|
+
}
|
|
3056
|
+
addEmojiPadding(text) {
|
|
3057
|
+
const emojis = ['', ' \u{1F44D}', ' \u2705', ' \u{1F4CC}', ' \u{1F4AC}', ' \u{1F4E2}'];
|
|
3058
|
+
return text + emojis[this.counter % emojis.length];
|
|
3059
|
+
}
|
|
3060
|
+
applySynonyms(text) {
|
|
3061
|
+
const words = text.split(/\b/);
|
|
3062
|
+
let replaced = false;
|
|
3063
|
+
return words
|
|
3064
|
+
.map(word => {
|
|
3065
|
+
if (replaced)
|
|
3066
|
+
return word;
|
|
3067
|
+
const lower = word.toLowerCase();
|
|
3068
|
+
const synonymList = SYNONYMS[lower];
|
|
3069
|
+
if (synonymList && Math.random() > 0.5) {
|
|
3070
|
+
replaced = true;
|
|
3071
|
+
const synonym = synonymList[Math.floor(Math.random() * synonymList.length)];
|
|
3072
|
+
return word[0] === word[0].toUpperCase() ? synonym.charAt(0).toUpperCase() + synonym.slice(1) : synonym;
|
|
3073
|
+
}
|
|
3074
|
+
return word;
|
|
3075
|
+
})
|
|
3076
|
+
.join('');
|
|
3077
|
+
}
|
|
3078
|
+
randomPositions(max, count) {
|
|
3079
|
+
const positions = [];
|
|
3080
|
+
while (positions.length < count) {
|
|
3081
|
+
const pos = Math.floor(Math.random() * max);
|
|
3082
|
+
if (!positions.includes(pos))
|
|
3083
|
+
positions.push(pos);
|
|
3084
|
+
}
|
|
3085
|
+
return positions;
|
|
3086
|
+
}
|
|
3087
|
+
};
|
|
3088
|
+
var DEFAULT_CONFIG14 = {
|
|
3089
|
+
urls: [],
|
|
3090
|
+
minRiskLevel: 'medium',
|
|
3091
|
+
cooldownMs: 3e5,
|
|
3092
|
+
includeStats: true
|
|
3093
|
+
};
|
|
3094
|
+
var WebhookAlerts = class {
|
|
3095
|
+
config;
|
|
3096
|
+
lastAlertTime = 0;
|
|
3097
|
+
constructor(config = {}) {
|
|
3098
|
+
this.config = { ...DEFAULT_CONFIG14, ...config };
|
|
3099
|
+
}
|
|
3100
|
+
async alert(data) {
|
|
3101
|
+
const riskOrder = ['low', 'medium', 'high', 'critical'];
|
|
3102
|
+
if (riskOrder.indexOf(data.risk) < riskOrder.indexOf(this.config.minRiskLevel)) {
|
|
3103
|
+
return;
|
|
3104
|
+
}
|
|
3105
|
+
const now = Date.now();
|
|
3106
|
+
if (now - this.lastAlertTime < this.config.cooldownMs) {
|
|
3107
|
+
return;
|
|
3108
|
+
}
|
|
3109
|
+
this.lastAlertTime = now;
|
|
3110
|
+
const payload = {
|
|
3111
|
+
source: 'baileys-antiban',
|
|
3112
|
+
timestamp: new Date().toISOString(),
|
|
3113
|
+
...data
|
|
3114
|
+
};
|
|
3115
|
+
for (const url of this.config.urls) {
|
|
3116
|
+
this.postWebhook(url, payload).catch(() => { });
|
|
3117
|
+
}
|
|
3118
|
+
if (this.config.telegram) {
|
|
3119
|
+
const emoji = { low: '\u{1F7E2}', medium: '\u{1F7E1}', high: '\u{1F7E0}', critical: '\u{1F534}' }[data.risk] || '\u26AA';
|
|
3120
|
+
const text = `${emoji} *baileys-antiban Alert*
|
|
3121
|
+
|
|
3122
|
+
Risk: *${data.risk.toUpperCase()}* (score: ${data.score})
|
|
3123
|
+
${data.recommendation}
|
|
3124
|
+
|
|
3125
|
+
Reasons:
|
|
3126
|
+
${data.reasons.map(r => `\u2022 ${r}`).join('\n')}`;
|
|
3127
|
+
this.postWebhook(`https://api.telegram.org/bot${this.config.telegram.botToken}/sendMessage`, {
|
|
3128
|
+
chat_id: this.config.telegram.chatId,
|
|
3129
|
+
text,
|
|
3130
|
+
parse_mode: 'Markdown'
|
|
3131
|
+
}).catch(() => { });
|
|
3132
|
+
}
|
|
3133
|
+
if (this.config.discord) {
|
|
3134
|
+
const color = { low: 65280, medium: 16776960, high: 16746496, critical: 16711680 }[data.risk] || 0;
|
|
3135
|
+
this.postWebhook(this.config.discord.webhookUrl, {
|
|
3136
|
+
embeds: [
|
|
3137
|
+
{
|
|
3138
|
+
title: '\u{1F6E1}\uFE0F baileys-antiban Alert',
|
|
3139
|
+
color,
|
|
3140
|
+
fields: [
|
|
3141
|
+
{ name: 'Risk', value: data.risk.toUpperCase(), inline: true },
|
|
3142
|
+
{ name: 'Score', value: String(data.score), inline: true },
|
|
3143
|
+
{ name: 'Recommendation', value: data.recommendation },
|
|
3144
|
+
{ name: 'Reasons', value: data.reasons.join('\n') }
|
|
3145
|
+
],
|
|
3146
|
+
timestamp: new Date().toISOString()
|
|
3147
|
+
}
|
|
3148
|
+
]
|
|
3149
|
+
}).catch(() => { });
|
|
3150
|
+
}
|
|
3151
|
+
}
|
|
3152
|
+
async postWebhook(url, payload) {
|
|
3153
|
+
try {
|
|
3154
|
+
const response = await fetch(url, {
|
|
3155
|
+
method: 'POST',
|
|
3156
|
+
headers: {
|
|
3157
|
+
'Content-Type': 'application/json',
|
|
3158
|
+
...this.config.headers
|
|
3159
|
+
},
|
|
3160
|
+
body: JSON.stringify(payload)
|
|
3161
|
+
});
|
|
3162
|
+
if (!response.ok) {
|
|
3163
|
+
process.stderr.write(`[baileys-antiban] Webhook failed: ${response.status}\n`);
|
|
3164
|
+
}
|
|
3165
|
+
}
|
|
3166
|
+
catch (err) {
|
|
3167
|
+
process.stderr.write(`[baileys-antiban] Webhook error: ${err}\n`);
|
|
3168
|
+
}
|
|
3169
|
+
}
|
|
3170
|
+
};
|
|
3171
|
+
var DEFAULT_CONFIG15 = {
|
|
3172
|
+
timezone: 'UTC',
|
|
3173
|
+
activeHours: [8, 21],
|
|
3174
|
+
weekendFactor: 0.5,
|
|
3175
|
+
peakHours: [10, 14],
|
|
3176
|
+
peakFactor: 1.3,
|
|
3177
|
+
lunchBreak: [12, 13],
|
|
3178
|
+
lunchFactor: 0.5
|
|
3179
|
+
};
|
|
3180
|
+
var Scheduler = class {
|
|
3181
|
+
config;
|
|
3182
|
+
constructor(config = {}) {
|
|
3183
|
+
this.config = { ...DEFAULT_CONFIG15, ...config };
|
|
3184
|
+
}
|
|
3185
|
+
isActiveTime() {
|
|
3186
|
+
const hour = this.getCurrentHour();
|
|
3187
|
+
const [start, end] = this.config.activeHours;
|
|
3188
|
+
return hour >= start && hour < end;
|
|
3189
|
+
}
|
|
3190
|
+
getSpeedFactor() {
|
|
3191
|
+
if (!this.isActiveTime())
|
|
3192
|
+
return 0;
|
|
3193
|
+
const hour = this.getCurrentHour();
|
|
3194
|
+
const day = this.getCurrentDay();
|
|
3195
|
+
let factor = 1;
|
|
3196
|
+
if (day === 0 || day === 6) {
|
|
3197
|
+
factor *= this.config.weekendFactor;
|
|
3198
|
+
}
|
|
3199
|
+
const [peakStart, peakEnd] = this.config.peakHours;
|
|
3200
|
+
if (hour >= peakStart && hour < peakEnd) {
|
|
3201
|
+
factor *= this.config.peakFactor;
|
|
3202
|
+
}
|
|
3203
|
+
const [lunchStart, lunchEnd] = this.config.lunchBreak;
|
|
3204
|
+
if (hour >= lunchStart && hour < lunchEnd) {
|
|
3205
|
+
factor *= this.config.lunchFactor;
|
|
3206
|
+
}
|
|
3207
|
+
return factor;
|
|
3208
|
+
}
|
|
3209
|
+
msUntilActive() {
|
|
3210
|
+
if (this.isActiveTime())
|
|
3211
|
+
return 0;
|
|
3212
|
+
const now = new Date();
|
|
3213
|
+
const hour = now.getHours();
|
|
3214
|
+
const [start] = this.config.activeHours;
|
|
3215
|
+
let nextActive;
|
|
3216
|
+
if (hour >= this.config.activeHours[1]) {
|
|
3217
|
+
nextActive = new Date(now);
|
|
3218
|
+
nextActive.setDate(nextActive.getDate() + 1);
|
|
3219
|
+
nextActive.setHours(start, 0, 0, 0);
|
|
3220
|
+
}
|
|
3221
|
+
else {
|
|
3222
|
+
nextActive = new Date(now);
|
|
3223
|
+
nextActive.setHours(start, 0, 0, 0);
|
|
3224
|
+
}
|
|
3225
|
+
return nextActive.getTime() - now.getTime();
|
|
3226
|
+
}
|
|
3227
|
+
adjustDelay(baseDelayMs) {
|
|
3228
|
+
const factor = this.getSpeedFactor();
|
|
3229
|
+
if (factor === 0)
|
|
3230
|
+
return -1;
|
|
3231
|
+
return Math.round(baseDelayMs / factor);
|
|
3232
|
+
}
|
|
3233
|
+
getStatus() {
|
|
3234
|
+
const hour = this.getCurrentHour();
|
|
3235
|
+
const day = this.getCurrentDay();
|
|
3236
|
+
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
3237
|
+
return {
|
|
3238
|
+
active: this.isActiveTime(),
|
|
3239
|
+
currentHour: hour,
|
|
3240
|
+
day: dayNames[day],
|
|
3241
|
+
isWeekend: day === 0 || day === 6,
|
|
3242
|
+
speedFactor: this.getSpeedFactor(),
|
|
3243
|
+
msUntilActive: this.msUntilActive(),
|
|
3244
|
+
activeWindow: `${this.config.activeHours[0]}:00 - ${this.config.activeHours[1]}:00`
|
|
3245
|
+
};
|
|
3246
|
+
}
|
|
3247
|
+
getCurrentHour() {
|
|
3248
|
+
return new Date().getHours();
|
|
3249
|
+
}
|
|
3250
|
+
getCurrentDay() {
|
|
3251
|
+
return new Date().getDay();
|
|
3252
|
+
}
|
|
3253
|
+
};
|
|
3254
|
+
var FileStateAdapter = class {
|
|
3255
|
+
basePath;
|
|
3256
|
+
constructor(basePath) {
|
|
3257
|
+
this.basePath = basePath;
|
|
3258
|
+
}
|
|
3259
|
+
async save(key, state) {
|
|
3260
|
+
const filePath = _path3.join(this.basePath, `${key}.json`);
|
|
3261
|
+
await _fsPromises.mkdir(this.basePath, { recursive: true });
|
|
3262
|
+
await _fsPromises.writeFile(filePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
3263
|
+
}
|
|
3264
|
+
async load(key) {
|
|
3265
|
+
const filePath = _path3.join(this.basePath, `${key}.json`);
|
|
3266
|
+
try {
|
|
3267
|
+
const data = await _fsPromises.readFile(filePath, 'utf-8');
|
|
3268
|
+
return JSON.parse(data);
|
|
3269
|
+
}
|
|
3270
|
+
catch (err) {
|
|
3271
|
+
if (err.code === 'ENOENT')
|
|
3272
|
+
return null;
|
|
3273
|
+
throw err;
|
|
3274
|
+
}
|
|
3275
|
+
}
|
|
3276
|
+
async delete(key) {
|
|
3277
|
+
const filePath = _path3.join(this.basePath, `${key}.json`);
|
|
3278
|
+
try {
|
|
3279
|
+
await _fsPromises.unlink(filePath);
|
|
3280
|
+
}
|
|
3281
|
+
catch (err) {
|
|
3282
|
+
if (err.code !== 'ENOENT')
|
|
3283
|
+
throw err;
|
|
3284
|
+
}
|
|
3285
|
+
}
|
|
3286
|
+
async list() {
|
|
3287
|
+
try {
|
|
3288
|
+
const files = await _fsPromises.readdir(this.basePath);
|
|
3289
|
+
return files.filter(f => f.endsWith('.json')).map(f => f.replace(/\.json$/, ''));
|
|
3290
|
+
}
|
|
3291
|
+
catch (err) {
|
|
3292
|
+
if (err.code === 'ENOENT')
|
|
3293
|
+
return [];
|
|
3294
|
+
throw err;
|
|
3295
|
+
}
|
|
3296
|
+
}
|
|
3297
|
+
};
|
|
3298
|
+
var DEFAULT_CONFIG16 = {
|
|
3299
|
+
maxTrackedChats: 1e3,
|
|
3300
|
+
maxGapMs: 30 * 6e4,
|
|
3301
|
+
persistDebounceMs: 2e3,
|
|
3302
|
+
onGapFilled: () => { },
|
|
3303
|
+
logger: {
|
|
3304
|
+
info: () => { },
|
|
3305
|
+
warn: () => { },
|
|
3306
|
+
error: () => { }
|
|
3307
|
+
}
|
|
3308
|
+
};
|
|
3309
|
+
function messageRecovery(sock, config) {
|
|
3310
|
+
const cfg = { ...DEFAULT_CONFIG16, ...config };
|
|
3311
|
+
const logger = cfg.logger;
|
|
3312
|
+
const lastSeen = new Map();
|
|
3313
|
+
let disconnectedAt = null;
|
|
3314
|
+
let totalRecovered = 0;
|
|
3315
|
+
let lastReconnectAt = null;
|
|
3316
|
+
let lastGapMs = null;
|
|
3317
|
+
let persistTimer = null;
|
|
3318
|
+
let loggedFetchWarning = false;
|
|
3319
|
+
if (cfg.persistPath) {
|
|
3320
|
+
loadPersistence();
|
|
3321
|
+
}
|
|
3322
|
+
const messagesListener = sock.ev.process ? setupProcessListener() : setupLegacyListener();
|
|
3323
|
+
const connectionListener = update => {
|
|
3324
|
+
if (update.connection === 'close') {
|
|
3325
|
+
disconnectedAt = Date.now();
|
|
3326
|
+
logger.info?.(`[messageRecovery] Disconnected at ${new Date(disconnectedAt).toISOString()}`);
|
|
3327
|
+
}
|
|
3328
|
+
if (update.connection === 'open' && disconnectedAt !== null) {
|
|
3329
|
+
void recoverMessages();
|
|
3330
|
+
}
|
|
3331
|
+
};
|
|
3332
|
+
sock.ev.on('connection.update', connectionListener);
|
|
3333
|
+
function setupProcessListener() {
|
|
3334
|
+
const listener = async (events) => {
|
|
3335
|
+
if (events['messages.upsert']) {
|
|
3336
|
+
const { messages, type } = events['messages.upsert'];
|
|
3337
|
+
if (type === 'notify') {
|
|
3338
|
+
for (const msg of messages || []) {
|
|
3339
|
+
trackMessage(msg);
|
|
3340
|
+
}
|
|
3341
|
+
}
|
|
3342
|
+
}
|
|
3343
|
+
};
|
|
3344
|
+
sock.ev.process(listener);
|
|
3345
|
+
return listener;
|
|
3346
|
+
}
|
|
3347
|
+
function setupLegacyListener() {
|
|
3348
|
+
const listener = upsert => {
|
|
3349
|
+
const { messages, type } = upsert;
|
|
3350
|
+
if (type === 'notify') {
|
|
3351
|
+
for (const msg of messages || []) {
|
|
3352
|
+
trackMessage(msg);
|
|
3353
|
+
}
|
|
3354
|
+
}
|
|
3355
|
+
};
|
|
3356
|
+
sock.ev.on('messages.upsert', listener);
|
|
3357
|
+
return listener;
|
|
3358
|
+
}
|
|
3359
|
+
function trackMessage(msg) {
|
|
3360
|
+
const jid = msg.key?.remoteJid;
|
|
3361
|
+
const messageId = msg.key?.id;
|
|
3362
|
+
const timestamp = msg.messageTimestamp;
|
|
3363
|
+
if (!jid || !messageId || !timestamp)
|
|
3364
|
+
return;
|
|
3365
|
+
if (msg.key?.fromMe)
|
|
3366
|
+
return;
|
|
3367
|
+
const now = Date.now();
|
|
3368
|
+
lastSeen.set(jid, {
|
|
3369
|
+
messageId,
|
|
3370
|
+
timestamp: typeof timestamp === 'number' ? timestamp : parseInt(timestamp, 10),
|
|
3371
|
+
lastTouchedAt: now
|
|
3372
|
+
});
|
|
3373
|
+
if (lastSeen.size > cfg.maxTrackedChats) {
|
|
3374
|
+
evictOldest();
|
|
3375
|
+
}
|
|
3376
|
+
schedulePersist();
|
|
3377
|
+
}
|
|
3378
|
+
function evictOldest() {
|
|
3379
|
+
let oldestJid = null;
|
|
3380
|
+
let oldestTime = Infinity;
|
|
3381
|
+
for (const [jid, entry] of lastSeen) {
|
|
3382
|
+
if (entry.lastTouchedAt < oldestTime) {
|
|
3383
|
+
oldestTime = entry.lastTouchedAt;
|
|
3384
|
+
oldestJid = jid;
|
|
3385
|
+
}
|
|
3386
|
+
}
|
|
3387
|
+
if (oldestJid) {
|
|
3388
|
+
lastSeen.delete(oldestJid);
|
|
3389
|
+
}
|
|
3390
|
+
}
|
|
3391
|
+
async function recoverMessages() {
|
|
3392
|
+
const recoveryStartMs = Date.now();
|
|
3393
|
+
const gapMs = recoveryStartMs - disconnectedAt;
|
|
3394
|
+
logger.info?.(`[messageRecovery] Reconnected after ${(gapMs / 1e3).toFixed(1)}s`);
|
|
3395
|
+
if (gapMs > cfg.maxGapMs) {
|
|
3396
|
+
logger.warn?.(`[messageRecovery] Gap too large (${(gapMs / 1e3).toFixed(0)}s > ${(cfg.maxGapMs / 1e3).toFixed(0)}s) \u2014 skipping recovery`);
|
|
3397
|
+
disconnectedAt = null;
|
|
3398
|
+
lastGapMs = gapMs;
|
|
3399
|
+
await cfg.onGapTooLarge?.(gapMs);
|
|
3400
|
+
return;
|
|
3401
|
+
}
|
|
3402
|
+
let recovered = 0;
|
|
3403
|
+
const chatsToRecover = Array.from(lastSeen.entries());
|
|
3404
|
+
if (typeof sock.fetchMessageHistory !== 'function') {
|
|
3405
|
+
if (!loggedFetchWarning) {
|
|
3406
|
+
logger.warn?.(`[messageRecovery] sock.fetchMessageHistory not available \u2014 recovery disabled. Baileys version may not support history fetch. User must implement manual reconciliation.`);
|
|
3407
|
+
loggedFetchWarning = true;
|
|
3408
|
+
}
|
|
3409
|
+
disconnectedAt = null;
|
|
3410
|
+
lastReconnectAt = new Date();
|
|
3411
|
+
lastGapMs = gapMs;
|
|
3412
|
+
await cfg.onRecoveryComplete?.({
|
|
3413
|
+
chats: 0,
|
|
3414
|
+
recovered: 0,
|
|
3415
|
+
durationMs: Date.now() - recoveryStartMs
|
|
3416
|
+
});
|
|
3417
|
+
return;
|
|
3418
|
+
}
|
|
3419
|
+
for (const [jid, lastSeenEntry] of chatsToRecover) {
|
|
3420
|
+
try {
|
|
3421
|
+
const messages = await sock.fetchMessageHistory(jid, 50, {
|
|
3422
|
+
before: void 0
|
|
3423
|
+
});
|
|
3424
|
+
if (!messages || !Array.isArray(messages))
|
|
3425
|
+
continue;
|
|
3426
|
+
const gapMessages = messages.filter(msg => {
|
|
3427
|
+
const ts = msg.messageTimestamp;
|
|
3428
|
+
if (!ts)
|
|
3429
|
+
return false;
|
|
3430
|
+
const msgTs = typeof ts === 'number' ? ts : parseInt(ts, 10);
|
|
3431
|
+
return msgTs > lastSeenEntry.timestamp;
|
|
3432
|
+
});
|
|
3433
|
+
gapMessages.sort((a, b) => {
|
|
3434
|
+
const aTs = typeof a.messageTimestamp === 'number' ? a.messageTimestamp : parseInt(a.messageTimestamp, 10);
|
|
3435
|
+
const bTs = typeof b.messageTimestamp === 'number' ? b.messageTimestamp : parseInt(b.messageTimestamp, 10);
|
|
3436
|
+
return aTs - bTs;
|
|
3437
|
+
});
|
|
3438
|
+
for (const msg of gapMessages) {
|
|
3439
|
+
await cfg.onGapFilled(msg, jid);
|
|
3440
|
+
recovered++;
|
|
3441
|
+
const msgTs = typeof msg.messageTimestamp === 'number' ? msg.messageTimestamp : parseInt(msg.messageTimestamp, 10);
|
|
3442
|
+
if (msgTs > lastSeenEntry.timestamp) {
|
|
3443
|
+
lastSeenEntry.timestamp = msgTs;
|
|
3444
|
+
lastSeenEntry.messageId = msg.key?.id || lastSeenEntry.messageId;
|
|
3445
|
+
lastSeenEntry.lastTouchedAt = Date.now();
|
|
3446
|
+
}
|
|
3447
|
+
}
|
|
3448
|
+
if (gapMessages.length > 0) {
|
|
3449
|
+
logger.info?.(`[messageRecovery] Recovered ${gapMessages.length} messages from ${jid}`);
|
|
3450
|
+
}
|
|
3451
|
+
}
|
|
3452
|
+
catch (err) {
|
|
3453
|
+
logger.error?.(`[messageRecovery] Failed to recover from ${jid}: ${err.message}`);
|
|
3454
|
+
}
|
|
3455
|
+
}
|
|
3456
|
+
totalRecovered += recovered;
|
|
3457
|
+
lastReconnectAt = new Date();
|
|
3458
|
+
lastGapMs = gapMs;
|
|
3459
|
+
disconnectedAt = null;
|
|
3460
|
+
logger.info?.(`[messageRecovery] Recovery complete: ${recovered} messages across ${chatsToRecover.length} chats in ${Date.now() - recoveryStartMs}ms`);
|
|
3461
|
+
await cfg.onRecoveryComplete?.({
|
|
3462
|
+
chats: chatsToRecover.length,
|
|
3463
|
+
recovered,
|
|
3464
|
+
durationMs: Date.now() - recoveryStartMs
|
|
3465
|
+
});
|
|
3466
|
+
}
|
|
3467
|
+
function schedulePersist() {
|
|
3468
|
+
if (!cfg.persistPath)
|
|
3469
|
+
return;
|
|
3470
|
+
if (persistTimer) {
|
|
3471
|
+
clearTimeout(persistTimer);
|
|
3472
|
+
}
|
|
3473
|
+
persistTimer = setTimeout(() => {
|
|
3474
|
+
void flushPersistence();
|
|
3475
|
+
}, cfg.persistDebounceMs);
|
|
3476
|
+
}
|
|
3477
|
+
async function flushPersistence() {
|
|
3478
|
+
if (!cfg.persistPath)
|
|
3479
|
+
return;
|
|
3480
|
+
try {
|
|
3481
|
+
const data = {};
|
|
3482
|
+
for (const [jid, entry] of lastSeen) {
|
|
3483
|
+
data[jid] = {
|
|
3484
|
+
id: entry.messageId,
|
|
3485
|
+
timestamp: entry.timestamp
|
|
3486
|
+
};
|
|
3487
|
+
}
|
|
3488
|
+
await _fsPromises.writeFile(cfg.persistPath, JSON.stringify(data, null, 2), 'utf-8');
|
|
3489
|
+
}
|
|
3490
|
+
catch (err) {
|
|
3491
|
+
logger.error?.(`[messageRecovery] Failed to persist state: ${err.message}`);
|
|
3492
|
+
}
|
|
3493
|
+
}
|
|
3494
|
+
function loadPersistence() {
|
|
3495
|
+
if (!cfg.persistPath)
|
|
3496
|
+
return;
|
|
3497
|
+
try {
|
|
3498
|
+
if (!fs2.existsSync(cfg.persistPath))
|
|
3499
|
+
return;
|
|
3500
|
+
const raw = fs2.readFileSync(cfg.persistPath, 'utf-8');
|
|
3501
|
+
const data = JSON.parse(raw);
|
|
3502
|
+
for (const [jid, entry] of Object.entries(data)) {
|
|
3503
|
+
lastSeen.set(jid, {
|
|
3504
|
+
messageId: entry.id,
|
|
3505
|
+
timestamp: entry.timestamp,
|
|
3506
|
+
lastTouchedAt: Date.now()
|
|
3507
|
+
});
|
|
3508
|
+
}
|
|
3509
|
+
logger.info?.(`[messageRecovery] Loaded ${lastSeen.size} entries from ${cfg.persistPath}`);
|
|
3510
|
+
}
|
|
3511
|
+
catch (err) {
|
|
3512
|
+
logger.warn?.(`[messageRecovery] Failed to load persisted state: ${err.message}`);
|
|
3513
|
+
}
|
|
3514
|
+
}
|
|
3515
|
+
return {
|
|
3516
|
+
async stop() {
|
|
3517
|
+
sock.ev.off('connection.update', connectionListener);
|
|
3518
|
+
if (!sock.ev.process) {
|
|
3519
|
+
sock.ev.off('messages.upsert', messagesListener);
|
|
3520
|
+
}
|
|
3521
|
+
if (persistTimer) {
|
|
3522
|
+
clearTimeout(persistTimer);
|
|
3523
|
+
persistTimer = null;
|
|
3524
|
+
}
|
|
3525
|
+
await flushPersistence();
|
|
3526
|
+
logger.info?.(`[messageRecovery] Stopped \u2014 total recovered: ${totalRecovered}`);
|
|
3527
|
+
},
|
|
3528
|
+
markSeen(chatJid, messageId, timestamp) {
|
|
3529
|
+
lastSeen.set(chatJid, {
|
|
3530
|
+
messageId,
|
|
3531
|
+
timestamp,
|
|
3532
|
+
lastTouchedAt: Date.now()
|
|
3533
|
+
});
|
|
3534
|
+
schedulePersist();
|
|
3535
|
+
},
|
|
3536
|
+
getStats() {
|
|
3537
|
+
return {
|
|
3538
|
+
trackedChats: lastSeen.size,
|
|
3539
|
+
totalRecovered,
|
|
3540
|
+
lastReconnectAt,
|
|
3541
|
+
lastGapMs
|
|
3542
|
+
};
|
|
3543
|
+
}
|
|
3544
|
+
};
|
|
3545
|
+
}
|
|
3546
|
+
var DEFAULT_APP_VERSION_POOL = [
|
|
3547
|
+
[2, 24, 5, 18],
|
|
3548
|
+
[2, 24, 5, 17],
|
|
3549
|
+
[2, 24, 4, 77],
|
|
3550
|
+
[2, 24, 5, 15],
|
|
3551
|
+
[2, 24, 3, 91],
|
|
3552
|
+
[2, 24, 5, 20]
|
|
3553
|
+
];
|
|
3554
|
+
var DEFAULT_OS_VERSION_POOL = ['10', '11', '12', '13', '14'];
|
|
3555
|
+
var DEFAULT_DEVICE_MODEL_POOL = [
|
|
3556
|
+
'Pixel 6',
|
|
3557
|
+
'Pixel 7',
|
|
3558
|
+
'Galaxy S22',
|
|
3559
|
+
'Galaxy S23',
|
|
3560
|
+
'Xiaomi 13',
|
|
3561
|
+
'Xiaomi 12',
|
|
3562
|
+
'OnePlus 11',
|
|
3563
|
+
'Moto G84',
|
|
3564
|
+
'Moto G54',
|
|
3565
|
+
'Realme 11',
|
|
3566
|
+
'Vivo V29',
|
|
3567
|
+
'Oppo Find X6'
|
|
3568
|
+
];
|
|
3569
|
+
var SeededRandom = class {
|
|
3570
|
+
state;
|
|
3571
|
+
constructor(seed) {
|
|
3572
|
+
let hash = 0;
|
|
3573
|
+
for (let i = 0; i < seed.length; i++) {
|
|
3574
|
+
hash = (hash << 5) - hash + seed.charCodeAt(i);
|
|
3575
|
+
hash = hash & hash;
|
|
3576
|
+
}
|
|
3577
|
+
this.state = Math.abs(hash) || 1;
|
|
3578
|
+
}
|
|
3579
|
+
next() {
|
|
3580
|
+
let t = (this.state += 1831565813);
|
|
3581
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
3582
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
3583
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
3584
|
+
}
|
|
3585
|
+
pick(array) {
|
|
3586
|
+
return array[Math.floor(this.next() * array.length)];
|
|
3587
|
+
}
|
|
3588
|
+
};
|
|
3589
|
+
function generateFingerprint(config = {}, sessionId) {
|
|
3590
|
+
const { enabled = true, randomizeAppVersion = true, randomizeOsVersion = true, randomizeDeviceModel = true, seed, appVersionPool = DEFAULT_APP_VERSION_POOL, osVersionPool = DEFAULT_OS_VERSION_POOL, deviceModelPool = DEFAULT_DEVICE_MODEL_POOL } = config;
|
|
3591
|
+
const finalSessionId = sessionId || `session-${Date.now()}-${Math.random()}`;
|
|
3592
|
+
const rng = new SeededRandom(seed || finalSessionId);
|
|
3593
|
+
const appVersion = enabled && randomizeAppVersion ? rng.pick(appVersionPool) : appVersionPool[0];
|
|
3594
|
+
const osVersion = enabled && randomizeOsVersion ? rng.pick(osVersionPool) : osVersionPool[0];
|
|
3595
|
+
const deviceModel = enabled && randomizeDeviceModel ? rng.pick(deviceModelPool) : deviceModelPool[0];
|
|
3596
|
+
return {
|
|
3597
|
+
appVersion: [...appVersion],
|
|
3598
|
+
osVersion,
|
|
3599
|
+
deviceModel,
|
|
3600
|
+
sessionId: finalSessionId
|
|
3601
|
+
};
|
|
3602
|
+
}
|
|
3603
|
+
function applyFingerprint(socketConfig, fp) {
|
|
3604
|
+
const config = { ...socketConfig };
|
|
3605
|
+
if (config.version !== void 0 || 'version' in config || true) {
|
|
3606
|
+
config.version = fp.appVersion;
|
|
3607
|
+
}
|
|
3608
|
+
if (config.browser !== void 0 || 'browser' in config || true) {
|
|
3609
|
+
config.browser = [fp.deviceModel, fp.osVersion, `WhatsApp/${fp.appVersion.join('.')}`];
|
|
3610
|
+
}
|
|
3611
|
+
return config;
|
|
3612
|
+
}
|
|
3613
|
+
var noop = () => { };
|
|
3614
|
+
function credsSnapshot(config) {
|
|
3615
|
+
const { credsPath, snapshotDir = path2.join(path2.dirname(credsPath), '.snapshots'), keep = 3, logger = {} } = config;
|
|
3616
|
+
const log = {
|
|
3617
|
+
info: logger.info || noop,
|
|
3618
|
+
warn: logger.warn || noop,
|
|
3619
|
+
error: logger.error || noop
|
|
3620
|
+
};
|
|
3621
|
+
async function take() {
|
|
3622
|
+
try {
|
|
3623
|
+
try {
|
|
3624
|
+
await import_fs.promises.access(credsPath);
|
|
3625
|
+
}
|
|
3626
|
+
catch {
|
|
3627
|
+
log.warn(`[credsSnapshot] Creds file not found: ${credsPath}`);
|
|
3628
|
+
return null;
|
|
3629
|
+
}
|
|
3630
|
+
await import_fs.promises.mkdir(snapshotDir, { recursive: true });
|
|
3631
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
3632
|
+
const snapshotPath = path2.join(snapshotDir, `creds-${timestamp}.json`);
|
|
3633
|
+
const tmpPath = `${snapshotPath}.tmp`;
|
|
3634
|
+
await import_fs.promises.copyFile(credsPath, tmpPath);
|
|
3635
|
+
await import_fs.promises.rename(tmpPath, snapshotPath);
|
|
3636
|
+
log.info(`[credsSnapshot] Snapshot taken: ${snapshotPath}`);
|
|
3637
|
+
await rotate();
|
|
3638
|
+
return snapshotPath;
|
|
3639
|
+
}
|
|
3640
|
+
catch (err) {
|
|
3641
|
+
log.error(`[credsSnapshot] Failed to take snapshot: ${err}`);
|
|
3642
|
+
return null;
|
|
3643
|
+
}
|
|
3644
|
+
}
|
|
3645
|
+
async function rotate() {
|
|
3646
|
+
try {
|
|
3647
|
+
const snapshots = await list();
|
|
3648
|
+
const toDelete = snapshots.slice(keep);
|
|
3649
|
+
for (const snap of toDelete) {
|
|
3650
|
+
await import_fs.promises.unlink(snap.path);
|
|
3651
|
+
log.info(`[credsSnapshot] Rotated out: ${snap.path}`);
|
|
3652
|
+
}
|
|
3653
|
+
}
|
|
3654
|
+
catch (err) {
|
|
3655
|
+
log.error(`[credsSnapshot] Rotation failed: ${err}`);
|
|
3656
|
+
}
|
|
3657
|
+
}
|
|
3658
|
+
async function list() {
|
|
3659
|
+
try {
|
|
3660
|
+
await import_fs.promises.access(snapshotDir);
|
|
3661
|
+
}
|
|
3662
|
+
catch {
|
|
3663
|
+
return [];
|
|
3664
|
+
}
|
|
3665
|
+
try {
|
|
3666
|
+
const files = await import_fs.promises.readdir(snapshotDir);
|
|
3667
|
+
const snapshots = await Promise.all(files
|
|
3668
|
+
.filter(f => f.startsWith('creds-') && f.endsWith('.json'))
|
|
3669
|
+
.map(async (f) => {
|
|
3670
|
+
const fullPath = path2.join(snapshotDir, f);
|
|
3671
|
+
const stat = await import_fs.promises.stat(fullPath);
|
|
3672
|
+
return {
|
|
3673
|
+
path: fullPath,
|
|
3674
|
+
takenAt: stat.mtime,
|
|
3675
|
+
size: stat.size
|
|
3676
|
+
};
|
|
3677
|
+
}));
|
|
3678
|
+
return snapshots.sort((a, b) => b.takenAt.getTime() - a.takenAt.getTime());
|
|
3679
|
+
}
|
|
3680
|
+
catch (err) {
|
|
3681
|
+
log.error(`[credsSnapshot] Failed to list snapshots: ${err}`);
|
|
3682
|
+
return [];
|
|
3683
|
+
}
|
|
3684
|
+
}
|
|
3685
|
+
async function restoreLatest() {
|
|
3686
|
+
const snapshots = await list();
|
|
3687
|
+
if (snapshots.length === 0) {
|
|
3688
|
+
log.warn('[credsSnapshot] No snapshots available to restore');
|
|
3689
|
+
return false;
|
|
3690
|
+
}
|
|
3691
|
+
return restore(snapshots[0].path);
|
|
3692
|
+
}
|
|
3693
|
+
async function restore(snapshotPath) {
|
|
3694
|
+
try {
|
|
3695
|
+
await import_fs.promises.access(snapshotPath);
|
|
3696
|
+
const tmpPath = `${credsPath}.tmp`;
|
|
3697
|
+
await import_fs.promises.copyFile(snapshotPath, tmpPath);
|
|
3698
|
+
await import_fs.promises.rename(tmpPath, credsPath);
|
|
3699
|
+
log.info(`[credsSnapshot] Restored from: ${snapshotPath}`);
|
|
3700
|
+
return true;
|
|
3701
|
+
}
|
|
3702
|
+
catch (err) {
|
|
3703
|
+
log.error(`[credsSnapshot] Failed to restore from ${snapshotPath}: ${err}`);
|
|
3704
|
+
return false;
|
|
3705
|
+
}
|
|
3706
|
+
}
|
|
3707
|
+
return {
|
|
3708
|
+
take,
|
|
3709
|
+
restoreLatest,
|
|
3710
|
+
restore,
|
|
3711
|
+
list
|
|
3712
|
+
};
|
|
3713
|
+
}
|
|
3714
|
+
function gaussianRandom() {
|
|
3715
|
+
let u = 0;
|
|
3716
|
+
let v = 0;
|
|
3717
|
+
while (u === 0)
|
|
3718
|
+
u = Math.random();
|
|
3719
|
+
while (v === 0)
|
|
3720
|
+
v = Math.random();
|
|
3721
|
+
return Math.sqrt(-2 * Math.log(u)) * Math.cos(2 * Math.PI * v);
|
|
3722
|
+
}
|
|
3723
|
+
function readReceiptVariance(config = {}) {
|
|
3724
|
+
const { meanMs = 1500, stdDevMs = 800, minMs = 200, maxMs = 8e3, skipIfOlderThanMs = 6e4 } = config;
|
|
3725
|
+
const pendingTimers = new Set();
|
|
3726
|
+
function delayMs() {
|
|
3727
|
+
const gaussian = gaussianRandom();
|
|
3728
|
+
const value = meanMs + gaussian * stdDevMs;
|
|
3729
|
+
return Math.max(minMs, Math.min(maxMs, value));
|
|
3730
|
+
}
|
|
3731
|
+
function wrap(sock) {
|
|
3732
|
+
const originalReadMessages = sock.readMessages.bind(sock);
|
|
3733
|
+
const wrappedReadMessages = async (keys) => {
|
|
3734
|
+
const now = Date.now();
|
|
3735
|
+
const oldMessages = keys.every(key => {
|
|
3736
|
+
if (!key.messageTimestamp)
|
|
3737
|
+
return false;
|
|
3738
|
+
const msgTime = typeof key.messageTimestamp === 'number'
|
|
3739
|
+
? key.messageTimestamp * 1e3
|
|
3740
|
+
: parseInt(key.messageTimestamp, 10) * 1e3;
|
|
3741
|
+
return now - msgTime > skipIfOlderThanMs;
|
|
3742
|
+
});
|
|
3743
|
+
if (oldMessages) {
|
|
3744
|
+
return originalReadMessages(keys);
|
|
3745
|
+
}
|
|
3746
|
+
const delay = delayMs();
|
|
3747
|
+
return new Promise((resolve, reject) => {
|
|
3748
|
+
const timer = setTimeout(async () => {
|
|
3749
|
+
pendingTimers.delete(timer);
|
|
3750
|
+
try {
|
|
3751
|
+
const result = await originalReadMessages(keys);
|
|
3752
|
+
resolve(result);
|
|
3753
|
+
}
|
|
3754
|
+
catch (err) {
|
|
3755
|
+
reject(err);
|
|
3756
|
+
}
|
|
3757
|
+
}, delay);
|
|
3758
|
+
pendingTimers.add(timer);
|
|
3759
|
+
});
|
|
3760
|
+
};
|
|
3761
|
+
return new Proxy(sock, {
|
|
3762
|
+
get(target, prop) {
|
|
3763
|
+
if (prop === 'readMessages') {
|
|
3764
|
+
return wrappedReadMessages;
|
|
3765
|
+
}
|
|
3766
|
+
return target[prop];
|
|
3767
|
+
}
|
|
3768
|
+
});
|
|
3769
|
+
}
|
|
3770
|
+
function stop() {
|
|
3771
|
+
for (const timer of pendingTimers) {
|
|
3772
|
+
clearTimeout(timer);
|
|
3773
|
+
}
|
|
3774
|
+
pendingTimers.clear();
|
|
3775
|
+
}
|
|
3776
|
+
return {
|
|
3777
|
+
wrap,
|
|
3778
|
+
delayMs,
|
|
3779
|
+
stop
|
|
3780
|
+
};
|
|
3781
|
+
}
|
|
3782
|
+
var NoopLogger = {
|
|
3783
|
+
info: () => { },
|
|
3784
|
+
warn: () => { },
|
|
3785
|
+
error: () => { }
|
|
3786
|
+
};
|
|
3787
|
+
function proxyRotator(config) {
|
|
3788
|
+
const { pool, strategy = 'round-robin', rotateOn = ['disconnect', 'ban-warning'], scheduledIntervalMs = 0, maxFailures = 3, deadCooldownMs = 6e5, logger = NoopLogger } = config;
|
|
3789
|
+
if (!pool || pool.length === 0) {
|
|
3790
|
+
throw new Error('proxyRotator: pool cannot be empty');
|
|
3791
|
+
}
|
|
3792
|
+
if (pool.length === 1) {
|
|
3793
|
+
logger.warn?.('proxyRotator: pool size is 1. Rotation is a no-op.');
|
|
3794
|
+
}
|
|
3795
|
+
if (scheduledIntervalMs > 0 && scheduledIntervalMs < 6e4) {
|
|
3796
|
+
logger.warn?.(`proxyRotator: scheduledIntervalMs (${scheduledIntervalMs}ms) is < 60s. May hammer proxy provider.`);
|
|
3797
|
+
}
|
|
3798
|
+
const states = pool.map(endpoint => ({
|
|
3799
|
+
endpoint,
|
|
3800
|
+
failures: 0,
|
|
3801
|
+
lastUsedAt: null,
|
|
3802
|
+
isDead: false
|
|
3803
|
+
}));
|
|
3804
|
+
let currentIndex = 0;
|
|
3805
|
+
let totalRotations = 0;
|
|
3806
|
+
const rotationsByTrigger = {};
|
|
3807
|
+
let scheduledTimer = null;
|
|
3808
|
+
const agentCache = new Map();
|
|
3809
|
+
const moduleCache = {};
|
|
3810
|
+
function buildProxyUrl(endpoint) {
|
|
3811
|
+
const { type, host, port, username, password } = endpoint;
|
|
3812
|
+
const auth = username && password ? `${username}:${password}@` : '';
|
|
3813
|
+
return `${type}://${auth}${host}:${port}`;
|
|
3814
|
+
}
|
|
3815
|
+
function createAgentForEndpointSync(endpoint) {
|
|
3816
|
+
if (agentCache.has(endpoint)) {
|
|
3817
|
+
return agentCache.get(endpoint);
|
|
3818
|
+
}
|
|
3819
|
+
const url = buildProxyUrl(endpoint);
|
|
3820
|
+
let agent = null;
|
|
3821
|
+
try {
|
|
3822
|
+
if (endpoint.type === 'socks5' || endpoint.type === 'socks5h') {
|
|
3823
|
+
if (!moduleCache['socks-proxy-agent']) {
|
|
3824
|
+
try {
|
|
3825
|
+
moduleCache['socks-proxy-agent'] = require2('socks-proxy-agent');
|
|
3826
|
+
}
|
|
3827
|
+
catch {
|
|
3828
|
+
logger.error?.('socks-proxy-agent not installed. Run: npm install socks-proxy-agent');
|
|
3829
|
+
return null;
|
|
3830
|
+
}
|
|
3831
|
+
}
|
|
3832
|
+
agent = new moduleCache['socks-proxy-agent'].SocksProxyAgent(url);
|
|
3833
|
+
}
|
|
3834
|
+
else if (endpoint.type === 'http') {
|
|
3835
|
+
if (!moduleCache['http-proxy-agent']) {
|
|
3836
|
+
try {
|
|
3837
|
+
moduleCache['http-proxy-agent'] = require2('http-proxy-agent');
|
|
3838
|
+
}
|
|
3839
|
+
catch {
|
|
3840
|
+
logger.error?.('http-proxy-agent not installed. Run: npm install http-proxy-agent');
|
|
3841
|
+
return null;
|
|
3842
|
+
}
|
|
3843
|
+
}
|
|
3844
|
+
agent = new moduleCache['http-proxy-agent'].HttpProxyAgent(url);
|
|
3845
|
+
}
|
|
3846
|
+
else if (endpoint.type === 'https') {
|
|
3847
|
+
if (!moduleCache['https-proxy-agent']) {
|
|
3848
|
+
try {
|
|
3849
|
+
moduleCache['https-proxy-agent'] = require2('https-proxy-agent');
|
|
3850
|
+
}
|
|
3851
|
+
catch {
|
|
3852
|
+
logger.error?.('https-proxy-agent not installed. Run: npm install https-proxy-agent');
|
|
3853
|
+
return null;
|
|
3854
|
+
}
|
|
3855
|
+
}
|
|
3856
|
+
agent = new moduleCache['https-proxy-agent'].HttpsProxyAgent(url);
|
|
3857
|
+
}
|
|
3858
|
+
else {
|
|
3859
|
+
logger.error?.(`Unknown proxy type: ${endpoint.type}`);
|
|
3860
|
+
return null;
|
|
3861
|
+
}
|
|
3862
|
+
if (agent) {
|
|
3863
|
+
agentCache.set(endpoint, agent);
|
|
3864
|
+
}
|
|
3865
|
+
return agent;
|
|
3866
|
+
}
|
|
3867
|
+
catch (err) {
|
|
3868
|
+
logger.error?.(`Failed to create agent for ${endpoint.label || endpoint.host}: ${err}`);
|
|
3869
|
+
return null;
|
|
3870
|
+
}
|
|
3871
|
+
}
|
|
3872
|
+
function getAliveEndpoints() {
|
|
3873
|
+
const now = Date.now();
|
|
3874
|
+
return states
|
|
3875
|
+
.map((s, idx) => {
|
|
3876
|
+
if (s.isDead && s.lastUsedAt) {
|
|
3877
|
+
if (now - s.lastUsedAt.getTime() >= deadCooldownMs) {
|
|
3878
|
+
s.isDead = false;
|
|
3879
|
+
s.failures = 0;
|
|
3880
|
+
logger.info?.(`Resurrected endpoint ${s.endpoint.label || s.endpoint.host} after cooldown`);
|
|
3881
|
+
}
|
|
3882
|
+
}
|
|
3883
|
+
const cooldown = s.endpoint.cooldownMs || 0;
|
|
3884
|
+
if (cooldown > 0 && s.lastUsedAt) {
|
|
3885
|
+
if (now - s.lastUsedAt.getTime() < cooldown) {
|
|
3886
|
+
return -1;
|
|
3887
|
+
}
|
|
3888
|
+
}
|
|
3889
|
+
return !s.isDead ? idx : -1;
|
|
3890
|
+
})
|
|
3891
|
+
.filter(idx => idx !== -1);
|
|
3892
|
+
}
|
|
3893
|
+
function selectNextIndex(alive) {
|
|
3894
|
+
if (alive.length === 0)
|
|
3895
|
+
return currentIndex;
|
|
3896
|
+
if (strategy === 'round-robin') {
|
|
3897
|
+
const afterCurrent = alive.filter(idx => idx > currentIndex);
|
|
3898
|
+
if (afterCurrent.length > 0)
|
|
3899
|
+
return afterCurrent[0];
|
|
3900
|
+
return alive[0];
|
|
3901
|
+
}
|
|
3902
|
+
if (strategy === 'random') {
|
|
3903
|
+
return alive[Math.floor(Math.random() * alive.length)];
|
|
3904
|
+
}
|
|
3905
|
+
if (strategy === 'least-recently-used') {
|
|
3906
|
+
const neverUsed = alive.filter(idx => states[idx].lastUsedAt === null);
|
|
3907
|
+
if (neverUsed.length > 0) {
|
|
3908
|
+
return neverUsed[0];
|
|
3909
|
+
}
|
|
3910
|
+
let oldestIdx = alive[0];
|
|
3911
|
+
let oldestTime = states[oldestIdx].lastUsedAt.getTime();
|
|
3912
|
+
for (const idx of alive) {
|
|
3913
|
+
const time = states[idx].lastUsedAt.getTime();
|
|
3914
|
+
if (time < oldestTime) {
|
|
3915
|
+
oldestTime = time;
|
|
3916
|
+
oldestIdx = idx;
|
|
3917
|
+
}
|
|
3918
|
+
}
|
|
3919
|
+
return oldestIdx;
|
|
3920
|
+
}
|
|
3921
|
+
if (strategy === 'weighted') {
|
|
3922
|
+
const weights = alive.map(idx => {
|
|
3923
|
+
const failures = states[idx].failures;
|
|
3924
|
+
return 1 / (failures + 1);
|
|
3925
|
+
});
|
|
3926
|
+
const totalWeight = weights.reduce((a, b) => a + b, 0);
|
|
3927
|
+
let rand = Math.random() * totalWeight;
|
|
3928
|
+
for (let i = 0; i < alive.length; i++) {
|
|
3929
|
+
rand -= weights[i];
|
|
3930
|
+
if (rand <= 0)
|
|
3931
|
+
return alive[i];
|
|
3932
|
+
}
|
|
3933
|
+
return alive[alive.length - 1];
|
|
3934
|
+
}
|
|
3935
|
+
return alive[0];
|
|
3936
|
+
}
|
|
3937
|
+
function rotateImpl(reason = 'manual') {
|
|
3938
|
+
if (pool.length === 1) {
|
|
3939
|
+
return states[0].endpoint;
|
|
3940
|
+
}
|
|
3941
|
+
const alive = getAliveEndpoints();
|
|
3942
|
+
if (alive.length === 0) {
|
|
3943
|
+
logger.warn?.('All endpoints are dead. Cannot rotate.');
|
|
3944
|
+
return states[currentIndex].endpoint;
|
|
3945
|
+
}
|
|
3946
|
+
const nextIdx = selectNextIndex(alive);
|
|
3947
|
+
if (nextIdx === currentIndex && alive.length > 1) {
|
|
3948
|
+
const others = alive.filter(idx => idx !== currentIndex);
|
|
3949
|
+
if (others.length > 0) {
|
|
3950
|
+
currentIndex = others[0];
|
|
3951
|
+
}
|
|
3952
|
+
else {
|
|
3953
|
+
currentIndex = nextIdx;
|
|
3954
|
+
}
|
|
3955
|
+
}
|
|
3956
|
+
else {
|
|
3957
|
+
currentIndex = nextIdx;
|
|
3958
|
+
}
|
|
3959
|
+
states[currentIndex].lastUsedAt = new Date();
|
|
3960
|
+
totalRotations++;
|
|
3961
|
+
rotationsByTrigger[reason] = (rotationsByTrigger[reason] || 0) + 1;
|
|
3962
|
+
const label = states[currentIndex].endpoint.label || states[currentIndex].endpoint.host;
|
|
3963
|
+
logger.info?.(`Rotated to endpoint ${label} (reason: ${reason})`);
|
|
3964
|
+
return states[currentIndex].endpoint;
|
|
3965
|
+
}
|
|
3966
|
+
function markFailureImpl() {
|
|
3967
|
+
const state = states[currentIndex];
|
|
3968
|
+
state.failures++;
|
|
3969
|
+
const label = state.endpoint.label || state.endpoint.host;
|
|
3970
|
+
logger.warn?.(`Endpoint ${label} failed (${state.failures}/${maxFailures})`);
|
|
3971
|
+
if (state.failures >= maxFailures) {
|
|
3972
|
+
state.isDead = true;
|
|
3973
|
+
logger.error?.(`Endpoint ${label} marked DEAD after ${maxFailures} failures`);
|
|
3974
|
+
const alive = getAliveEndpoints();
|
|
3975
|
+
if (alive.length > 0) {
|
|
3976
|
+
rotateImpl('manual');
|
|
3977
|
+
}
|
|
3978
|
+
}
|
|
3979
|
+
}
|
|
3980
|
+
function resurrectAllImpl() {
|
|
3981
|
+
let count = 0;
|
|
3982
|
+
for (const state of states) {
|
|
3983
|
+
if (state.isDead) {
|
|
3984
|
+
state.isDead = false;
|
|
3985
|
+
state.failures = 0;
|
|
3986
|
+
count++;
|
|
3987
|
+
}
|
|
3988
|
+
}
|
|
3989
|
+
if (count > 0) {
|
|
3990
|
+
logger.info?.(`Resurrected ${count} dead endpoint(s)`);
|
|
3991
|
+
}
|
|
3992
|
+
}
|
|
3993
|
+
function stopImpl() {
|
|
3994
|
+
if (scheduledTimer) {
|
|
3995
|
+
clearInterval(scheduledTimer);
|
|
3996
|
+
scheduledTimer = null;
|
|
3997
|
+
logger.info?.('Stopped scheduled rotation timer');
|
|
3998
|
+
}
|
|
3999
|
+
}
|
|
4000
|
+
function getStatsImpl() {
|
|
4001
|
+
return {
|
|
4002
|
+
totalRotations,
|
|
4003
|
+
rotationsByTrigger: { ...rotationsByTrigger },
|
|
4004
|
+
endpointHealth: states.map(s => ({
|
|
4005
|
+
label: s.endpoint.label || s.endpoint.host,
|
|
4006
|
+
inUse: states[currentIndex] === s,
|
|
4007
|
+
failures: s.failures,
|
|
4008
|
+
lastUsedAt: s.lastUsedAt,
|
|
4009
|
+
isDead: s.isDead
|
|
4010
|
+
})),
|
|
4011
|
+
currentEndpoint: states[currentIndex].endpoint.label || states[currentIndex].endpoint.host
|
|
4012
|
+
};
|
|
4013
|
+
}
|
|
4014
|
+
function currentAgentImpl() {
|
|
4015
|
+
const endpoint = states[currentIndex].endpoint;
|
|
4016
|
+
return createAgentForEndpointSync(endpoint);
|
|
4017
|
+
}
|
|
4018
|
+
function currentImpl() {
|
|
4019
|
+
return states[currentIndex].endpoint;
|
|
4020
|
+
}
|
|
4021
|
+
if (rotateOn.includes('scheduled') && scheduledIntervalMs > 0) {
|
|
4022
|
+
scheduledTimer = setInterval(() => {
|
|
4023
|
+
rotateImpl('scheduled');
|
|
4024
|
+
}, scheduledIntervalMs);
|
|
4025
|
+
logger.info?.(`Scheduled rotation enabled (every ${scheduledIntervalMs}ms)`);
|
|
4026
|
+
}
|
|
4027
|
+
states[0].lastUsedAt = new Date();
|
|
4028
|
+
return {
|
|
4029
|
+
currentAgent: currentAgentImpl,
|
|
4030
|
+
current: currentImpl,
|
|
4031
|
+
rotate: rotateImpl,
|
|
4032
|
+
markFailure: markFailureImpl,
|
|
4033
|
+
resurrectAll: resurrectAllImpl,
|
|
4034
|
+
stop: stopImpl,
|
|
4035
|
+
getStats: getStatsImpl
|
|
4036
|
+
};
|
|
4037
|
+
}
|
|
4038
|
+
|
|
4039
|
+
export {
|
|
4040
|
+
AntiBan,
|
|
4041
|
+
ContactGraphWarmer,
|
|
4042
|
+
ContentVariator,
|
|
4043
|
+
FileStateAdapter,
|
|
4044
|
+
HealthMonitor,
|
|
4045
|
+
JidCanonicalizer,
|
|
4046
|
+
LidFirstResolver,
|
|
4047
|
+
LidResolver,
|
|
4048
|
+
MAC_ERROR_CODES,
|
|
4049
|
+
MessageQueue,
|
|
4050
|
+
MessageRetryReason,
|
|
4051
|
+
PRESETS,
|
|
4052
|
+
PostReconnectThrottle,
|
|
4053
|
+
PresenceChoreographer,
|
|
4054
|
+
RateLimiter,
|
|
4055
|
+
ReplyRatioGuard,
|
|
4056
|
+
RetryReasonTracker,
|
|
4057
|
+
Scheduler,
|
|
4058
|
+
SessionHealthMonitor,
|
|
4059
|
+
StateManager,
|
|
4060
|
+
TimelockGuard,
|
|
4061
|
+
WarmUp,
|
|
4062
|
+
WebhookAlerts,
|
|
4063
|
+
applyFingerprint,
|
|
4064
|
+
applyGroupMultiplier,
|
|
4065
|
+
classifyDisconnect,
|
|
4066
|
+
createLidFirstResolver,
|
|
4067
|
+
credsSnapshot,
|
|
4068
|
+
generateFingerprint,
|
|
4069
|
+
getCircadianMultiplier,
|
|
4070
|
+
getRetryReasonDescription,
|
|
4071
|
+
isBroadcast,
|
|
4072
|
+
isGroup,
|
|
4073
|
+
isMacError,
|
|
4074
|
+
isNewsletter,
|
|
4075
|
+
messageRecovery,
|
|
4076
|
+
parseRetryReason,
|
|
4077
|
+
proxyRotator,
|
|
4078
|
+
readReceiptVariance,
|
|
4079
|
+
resolveConfig,
|
|
4080
|
+
shouldUseGroupProfile,
|
|
4081
|
+
wrapSocket,
|
|
4082
|
+
wrapWithSessionStability
|
|
4083
|
+
};
|