@prototypevip/baileys 0.1.0 → 0.1.2
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/lib/Defaults/index.js +3 -3
- package/lib/Defaults/index.js.bak-1788476907801 +187 -0
- package/lib/Defaults/index.js.bak-1788479166288 +187 -0
- package/lib/Socket/groups.js.bak-1788476973954 +475 -0
- package/lib/Socket/index.js +42 -126
- package/lib/Socket/index.js.bak-1788479281092 +38 -0
- package/lib/Socket/messages-send.js +88 -51
- package/lib/Store/prototype-store.js +34 -22
- package/lib/Store/prototype-store.js.bak-1788477006895 +983 -0
- package/lib/Utils/index.js +2 -0
- package/lib/Utils/integrity-guard.js +281 -0
- package/lib/Utils/interactive-message-handler.js +28 -8
- package/lib/Utils/interactive-message-handler.js.bak-1788476898661 +478 -0
- package/lib/Utils/socket-guard.js +154 -0
- package/package.json +2 -2
- package/lib/AntiBan/antiban.js +0 -922
- package/lib/AntiBan/banRecoveryOrchestrator.js +0 -284
- package/lib/AntiBan/cli.js +0 -177
- package/lib/AntiBan/contactGraph.js +0 -236
- package/lib/AntiBan/contentVariator.js +0 -150
- package/lib/AntiBan/credsSnapshot.js +0 -121
- package/lib/AntiBan/deliveryTracker.js +0 -107
- package/lib/AntiBan/deviceFingerprint.js +0 -106
- package/lib/AntiBan/fleetEventStore.js +0 -157
- package/lib/AntiBan/groupOperationGuard.js +0 -116
- package/lib/AntiBan/health.js +0 -207
- package/lib/AntiBan/humanEntropy.js +0 -337
- package/lib/AntiBan/index.js +0 -77
- package/lib/AntiBan/instanceCoordinator.js +0 -175
- package/lib/AntiBan/jidCanonicalizer.js +0 -256
- package/lib/AntiBan/jidCircuitBreaker.js +0 -186
- package/lib/AntiBan/legitimacySignalInjector.js +0 -231
- package/lib/AntiBan/lidFirstResolver.js +0 -174
- package/lib/AntiBan/lidResolver.js +0 -324
- package/lib/AntiBan/messageQueue.js +0 -187
- package/lib/AntiBan/messageRecovery.js +0 -283
- package/lib/AntiBan/messageTypeRegistry.js +0 -369
- package/lib/AntiBan/observability.js +0 -145
- package/lib/AntiBan/patch.js +0 -302
- package/lib/AntiBan/persist.js +0 -87
- package/lib/AntiBan/presenceChoreographer.js +0 -430
- package/lib/AntiBan/presets.js +0 -105
- package/lib/AntiBan/profiles.js +0 -31
- package/lib/AntiBan/proxyRotator.js +0 -306
- package/lib/AntiBan/rateLimiter.js +0 -261
- package/lib/AntiBan/readReceiptVariance.js +0 -88
- package/lib/AntiBan/reconnectThrottle.js +0 -180
- package/lib/AntiBan/replyRatio.js +0 -161
- package/lib/AntiBan/reputationVoucher.js +0 -373
- package/lib/AntiBan/retryReason.js +0 -91
- package/lib/AntiBan/retryTracker.js +0 -172
- package/lib/AntiBan/scheduler.js +0 -111
- package/lib/AntiBan/sessionFingerprint.js +0 -248
- package/lib/AntiBan/sessionStability.js +0 -330
- package/lib/AntiBan/stateAdapter.js +0 -73
- package/lib/AntiBan/stateExport.js +0 -152
- package/lib/AntiBan/stealthConnect.js +0 -130
- package/lib/AntiBan/timelockGuard.js +0 -181
- package/lib/AntiBan/topologyThrottler.js +0 -339
- package/lib/AntiBan/warmup.js +0 -140
- package/lib/AntiBan/webhooks.js +0 -80
- package/lib/AntiBan/wrapper.js +0 -456
package/lib/AntiBan/scheduler.js
DELETED
|
@@ -1,111 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Smart Scheduler — Send messages during safe hours only
|
|
3
|
-
*
|
|
4
|
-
* WhatsApp is more suspicious of messages sent at 3 AM.
|
|
5
|
-
* This module ensures messages go out during "business hours"
|
|
6
|
-
* and adds realistic daily activity patterns.
|
|
7
|
-
*/
|
|
8
|
-
const DEFAULT_CONFIG = {
|
|
9
|
-
timezone: 'UTC',
|
|
10
|
-
activeHours: [8, 21],
|
|
11
|
-
weekendFactor: 0.5,
|
|
12
|
-
peakHours: [10, 14],
|
|
13
|
-
peakFactor: 1.3,
|
|
14
|
-
lunchBreak: [12, 13],
|
|
15
|
-
lunchFactor: 0.5,
|
|
16
|
-
};
|
|
17
|
-
export class Scheduler {
|
|
18
|
-
config;
|
|
19
|
-
constructor(config = {}) {
|
|
20
|
-
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
21
|
-
}
|
|
22
|
-
/**
|
|
23
|
-
* Check if now is within active hours
|
|
24
|
-
*/
|
|
25
|
-
isActiveTime() {
|
|
26
|
-
const hour = this.getCurrentHour();
|
|
27
|
-
const [start, end] = this.config.activeHours;
|
|
28
|
-
return hour >= start && hour < end;
|
|
29
|
-
}
|
|
30
|
-
/**
|
|
31
|
-
* Get the speed multiplier for current time
|
|
32
|
-
* > 1 = faster, < 1 = slower, 0 = don't send
|
|
33
|
-
*/
|
|
34
|
-
getSpeedFactor() {
|
|
35
|
-
if (!this.isActiveTime())
|
|
36
|
-
return 0;
|
|
37
|
-
const hour = this.getCurrentHour();
|
|
38
|
-
const day = this.getCurrentDay();
|
|
39
|
-
let factor = 1.0;
|
|
40
|
-
// Weekend reduction
|
|
41
|
-
if (day === 0 || day === 6) {
|
|
42
|
-
factor *= this.config.weekendFactor;
|
|
43
|
-
}
|
|
44
|
-
// Peak hours boost
|
|
45
|
-
const [peakStart, peakEnd] = this.config.peakHours;
|
|
46
|
-
if (hour >= peakStart && hour < peakEnd) {
|
|
47
|
-
factor *= this.config.peakFactor;
|
|
48
|
-
}
|
|
49
|
-
// Lunch slowdown
|
|
50
|
-
const [lunchStart, lunchEnd] = this.config.lunchBreak;
|
|
51
|
-
if (hour >= lunchStart && hour < lunchEnd) {
|
|
52
|
-
factor *= this.config.lunchFactor;
|
|
53
|
-
}
|
|
54
|
-
return factor;
|
|
55
|
-
}
|
|
56
|
-
/**
|
|
57
|
-
* Get ms until next active window
|
|
58
|
-
*/
|
|
59
|
-
msUntilActive() {
|
|
60
|
-
if (this.isActiveTime())
|
|
61
|
-
return 0;
|
|
62
|
-
const now = new Date();
|
|
63
|
-
const hour = now.getHours();
|
|
64
|
-
const [start] = this.config.activeHours;
|
|
65
|
-
let nextActive;
|
|
66
|
-
if (hour >= this.config.activeHours[1]) {
|
|
67
|
-
// After hours — next active is tomorrow
|
|
68
|
-
nextActive = new Date(now);
|
|
69
|
-
nextActive.setDate(nextActive.getDate() + 1);
|
|
70
|
-
nextActive.setHours(start, 0, 0, 0);
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
// Before hours — next active is today
|
|
74
|
-
nextActive = new Date(now);
|
|
75
|
-
nextActive.setHours(start, 0, 0, 0);
|
|
76
|
-
}
|
|
77
|
-
return nextActive.getTime() - now.getTime();
|
|
78
|
-
}
|
|
79
|
-
/**
|
|
80
|
-
* Adjust a delay based on current time factors
|
|
81
|
-
*/
|
|
82
|
-
adjustDelay(baseDelayMs) {
|
|
83
|
-
const factor = this.getSpeedFactor();
|
|
84
|
-
if (factor === 0)
|
|
85
|
-
return -1; // Don't send
|
|
86
|
-
return Math.round(baseDelayMs / factor);
|
|
87
|
-
}
|
|
88
|
-
/**
|
|
89
|
-
* Get current schedule status
|
|
90
|
-
*/
|
|
91
|
-
getStatus() {
|
|
92
|
-
const hour = this.getCurrentHour();
|
|
93
|
-
const day = this.getCurrentDay();
|
|
94
|
-
const dayNames = ['Sunday', 'Monday', 'Tuesday', 'Wednesday', 'Thursday', 'Friday', 'Saturday'];
|
|
95
|
-
return {
|
|
96
|
-
active: this.isActiveTime(),
|
|
97
|
-
currentHour: hour,
|
|
98
|
-
day: dayNames[day],
|
|
99
|
-
isWeekend: day === 0 || day === 6,
|
|
100
|
-
speedFactor: this.getSpeedFactor(),
|
|
101
|
-
msUntilActive: this.msUntilActive(),
|
|
102
|
-
activeWindow: `${this.config.activeHours[0]}:00 - ${this.config.activeHours[1]}:00`,
|
|
103
|
-
};
|
|
104
|
-
}
|
|
105
|
-
getCurrentHour() {
|
|
106
|
-
return new Date().getHours();
|
|
107
|
-
}
|
|
108
|
-
getCurrentDay() {
|
|
109
|
-
return new Date().getDay();
|
|
110
|
-
}
|
|
111
|
-
}
|
|
@@ -1,248 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session Fingerprint Randomization (Obscura-inspired)
|
|
3
|
-
*
|
|
4
|
-
* Per-session fingerprint randomization to prevent device tracking.
|
|
5
|
-
* Scavenged patterns from Obscura headless browser's stealth mode.
|
|
6
|
-
*
|
|
7
|
-
* Key principles from Obscura:
|
|
8
|
-
* 1. Per-session randomization (not per-request)
|
|
9
|
-
* 2. Consistent within session (same session = same fingerprint)
|
|
10
|
-
* 3. Feature-flag pattern for optional anti-detection
|
|
11
|
-
* 4. Emulation of real device profiles (not synthetic values)
|
|
12
|
-
*
|
|
13
|
-
* Browser fingerprint → WhatsApp signal mapping:
|
|
14
|
-
* - TLS fingerprint → WA protocol version
|
|
15
|
-
* - Canvas noise → message timing jitter
|
|
16
|
-
* - Audio fingerprint → voice note metadata
|
|
17
|
-
* - GPU info → device model/brand
|
|
18
|
-
* - Battery → connection state variation
|
|
19
|
-
* - User agent → WA client version
|
|
20
|
-
*
|
|
21
|
-
* @author Kobus Wentzel <kobie@pop.co.za>
|
|
22
|
-
* @license MIT
|
|
23
|
-
*/
|
|
24
|
-
import { generateFingerprint } from './deviceFingerprint.js';
|
|
25
|
-
/**
|
|
26
|
-
* Simple deterministic PRNG using mulberry32
|
|
27
|
-
* Same as deviceFingerprint.ts for consistency
|
|
28
|
-
*/
|
|
29
|
-
class SeededRandom {
|
|
30
|
-
state;
|
|
31
|
-
constructor(seed) {
|
|
32
|
-
let hash = 0;
|
|
33
|
-
for (let i = 0; i < seed.length; i++) {
|
|
34
|
-
hash = (hash << 5) - hash + seed.charCodeAt(i);
|
|
35
|
-
hash = hash & hash;
|
|
36
|
-
}
|
|
37
|
-
this.state = Math.abs(hash) || 1;
|
|
38
|
-
}
|
|
39
|
-
next() {
|
|
40
|
-
let t = (this.state += 0x6d2b79f5);
|
|
41
|
-
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
42
|
-
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
43
|
-
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
44
|
-
}
|
|
45
|
-
range(min, max) {
|
|
46
|
-
return Math.floor(this.next() * (max - min + 1)) + min;
|
|
47
|
-
}
|
|
48
|
-
rangeFloat(min, max) {
|
|
49
|
-
return this.next() * (max - min) + min;
|
|
50
|
-
}
|
|
51
|
-
pick(array) {
|
|
52
|
-
return array[Math.floor(this.next() * array.length)];
|
|
53
|
-
}
|
|
54
|
-
boolean(probability = 0.5) {
|
|
55
|
-
return this.next() < probability;
|
|
56
|
-
}
|
|
57
|
-
}
|
|
58
|
-
// Default configuration values (conservative, realistic)
|
|
59
|
-
const DEFAULT_SEND_JITTER_MS = [50, 300];
|
|
60
|
-
const DEFAULT_TYPING_JITTER_MS = [30, 150];
|
|
61
|
-
const DEFAULT_RETRY_JITTER_MS = [100, 500];
|
|
62
|
-
const DEFAULT_DURATION_JITTER_MS = 200;
|
|
63
|
-
const DEFAULT_SAMPLE_RATE_POOL = [8000, 16000, 44100, 48000];
|
|
64
|
-
const DEFAULT_IDLE_TIMEOUT_JITTER_MS = [25000, 35000];
|
|
65
|
-
const DEFAULT_KEEPALIVE_JITTER_MS = [15000, 25000];
|
|
66
|
-
const DEFAULT_BATTERY_LEVEL_POOL = [20, 35, 50, 65, 80, 95, 100];
|
|
67
|
-
const DEFAULT_PROTOCOL_VERSION_POOL = ['2.24.5', '2.24.4', '2.24.3'];
|
|
68
|
-
/**
|
|
69
|
-
* Generate a comprehensive session fingerprint.
|
|
70
|
-
* Call once per session (socket initialization).
|
|
71
|
-
*
|
|
72
|
-
* Obscura pattern: consistent per session, randomized across sessions.
|
|
73
|
-
*/
|
|
74
|
-
export function generateSessionFingerprint(config = {}, sessionId) {
|
|
75
|
-
const { enabled = true, deviceProfile = {}, networkTiming = {}, voiceNote = {}, connectionState = {}, protocolVersion = {}, seed, } = config;
|
|
76
|
-
const finalSessionId = sessionId || `session-${Date.now()}-${Math.random()}`;
|
|
77
|
-
const rng = new SeededRandom(seed || finalSessionId);
|
|
78
|
-
// Generate base device fingerprint (delegates to deviceFingerprint.ts)
|
|
79
|
-
const device = generateFingerprint({
|
|
80
|
-
enabled,
|
|
81
|
-
randomizeAppVersion: deviceProfile.randomizeAppVersion ?? true,
|
|
82
|
-
randomizeOsVersion: deviceProfile.randomizeOsVersion ?? true,
|
|
83
|
-
randomizeDeviceModel: deviceProfile.randomizeDeviceModel ?? true,
|
|
84
|
-
seed: seed || finalSessionId,
|
|
85
|
-
appVersionPool: deviceProfile.appVersionPool,
|
|
86
|
-
osVersionPool: deviceProfile.osVersionPool,
|
|
87
|
-
deviceModelPool: deviceProfile.deviceModelPool,
|
|
88
|
-
}, finalSessionId);
|
|
89
|
-
// Network timing variances (Obscura: prevent timing pattern detection)
|
|
90
|
-
const sendJitterRange = networkTiming.sendJitterMs || DEFAULT_SEND_JITTER_MS;
|
|
91
|
-
const typingJitterRange = networkTiming.typingJitterMs || DEFAULT_TYPING_JITTER_MS;
|
|
92
|
-
const retryJitterRange = networkTiming.retryJitterMs || DEFAULT_RETRY_JITTER_MS;
|
|
93
|
-
const networkTimingProfile = enabled
|
|
94
|
-
? {
|
|
95
|
-
sendJitterMs: rng.range(sendJitterRange[0], sendJitterRange[1]),
|
|
96
|
-
typingJitterMs: rng.range(typingJitterRange[0], typingJitterRange[1]),
|
|
97
|
-
retryJitterMs: rng.range(retryJitterRange[0], retryJitterRange[1]),
|
|
98
|
-
}
|
|
99
|
-
: {
|
|
100
|
-
sendJitterMs: 0,
|
|
101
|
-
typingJitterMs: 0,
|
|
102
|
-
retryJitterMs: 0,
|
|
103
|
-
};
|
|
104
|
-
// Voice note profile (Obscura: audio fingerprint variance)
|
|
105
|
-
const sampleRatePool = voiceNote.sampleRatePool || DEFAULT_SAMPLE_RATE_POOL;
|
|
106
|
-
const voiceNoteProfile = {
|
|
107
|
-
waveformSeed: enabled ? rng.range(0, 2147483647) : 0,
|
|
108
|
-
durationJitterMs: enabled && voiceNote.randomizeWaveform !== false
|
|
109
|
-
? rng.range(0, voiceNote.durationJitterMs || DEFAULT_DURATION_JITTER_MS)
|
|
110
|
-
: 0,
|
|
111
|
-
sampleRate: enabled ? rng.pick(sampleRatePool) : sampleRatePool[0],
|
|
112
|
-
};
|
|
113
|
-
// Connection state profile (Obscura: battery/network state variance)
|
|
114
|
-
const idleTimeoutRange = connectionState.idleTimeoutJitterMs || DEFAULT_IDLE_TIMEOUT_JITTER_MS;
|
|
115
|
-
const keepaliveRange = connectionState.keepaliveJitterMs || DEFAULT_KEEPALIVE_JITTER_MS;
|
|
116
|
-
const batteryLevelPool = connectionState.batteryLevelPool || DEFAULT_BATTERY_LEVEL_POOL;
|
|
117
|
-
const connectionStateProfile = {
|
|
118
|
-
idleTimeoutMs: enabled
|
|
119
|
-
? rng.range(idleTimeoutRange[0], idleTimeoutRange[1])
|
|
120
|
-
: 30000,
|
|
121
|
-
keepaliveMs: enabled
|
|
122
|
-
? rng.range(keepaliveRange[0], keepaliveRange[1])
|
|
123
|
-
: 20000,
|
|
124
|
-
batteryLevel: enabled && connectionState.randomizeBattery !== false
|
|
125
|
-
? rng.pick(batteryLevelPool)
|
|
126
|
-
: 100,
|
|
127
|
-
batteryCharging: enabled ? rng.boolean(0.3) : false, // 30% charging probability
|
|
128
|
-
};
|
|
129
|
-
// Protocol version (Obscura: TLS fingerprint variance → WA protocol variance)
|
|
130
|
-
const versionPool = protocolVersion.versionPool || DEFAULT_PROTOCOL_VERSION_POOL;
|
|
131
|
-
const protocolVersionStr = enabled && protocolVersion.randomizeSubVersion !== false
|
|
132
|
-
? rng.pick(versionPool)
|
|
133
|
-
: versionPool[0];
|
|
134
|
-
return {
|
|
135
|
-
device,
|
|
136
|
-
networkTiming: networkTimingProfile,
|
|
137
|
-
voiceNote: voiceNoteProfile,
|
|
138
|
-
connectionState: connectionStateProfile,
|
|
139
|
-
protocolVersion: protocolVersionStr,
|
|
140
|
-
sessionId: finalSessionId,
|
|
141
|
-
createdAt: Date.now(),
|
|
142
|
-
};
|
|
143
|
-
}
|
|
144
|
-
/**
|
|
145
|
-
* Apply session fingerprint to Baileys socket config.
|
|
146
|
-
*
|
|
147
|
-
* Usage:
|
|
148
|
-
* const fingerprint = generateSessionFingerprint({ enabled: true });
|
|
149
|
-
* const sock = makeWASocket(applySessionFingerprint(config, fingerprint));
|
|
150
|
-
*/
|
|
151
|
-
export function applySessionFingerprint(socketConfig, fingerprint) {
|
|
152
|
-
const config = { ...socketConfig };
|
|
153
|
-
// Apply device profile (appVersion, browser tuple)
|
|
154
|
-
config.version = fingerprint.device.appVersion;
|
|
155
|
-
config.browser = [
|
|
156
|
-
fingerprint.device.deviceModel,
|
|
157
|
-
fingerprint.device.osVersion,
|
|
158
|
-
`WhatsApp/${fingerprint.device.appVersion.join('.')}`,
|
|
159
|
-
];
|
|
160
|
-
// Apply connection timeouts if fields exist
|
|
161
|
-
if ('connectTimeoutMs' in config || config.connectTimeoutMs !== undefined) {
|
|
162
|
-
// Add idle timeout jitter to connection config
|
|
163
|
-
config.connectTimeoutMs = fingerprint.connectionState.idleTimeoutMs;
|
|
164
|
-
}
|
|
165
|
-
// Apply keepalive if supported
|
|
166
|
-
if ('keepAliveIntervalMs' in config || config.keepAliveIntervalMs !== undefined) {
|
|
167
|
-
config.keepAliveIntervalMs = fingerprint.connectionState.keepaliveMs;
|
|
168
|
-
}
|
|
169
|
-
// Store fingerprint for runtime access (helpers can read this)
|
|
170
|
-
config.__sessionFingerprint = fingerprint;
|
|
171
|
-
return config;
|
|
172
|
-
}
|
|
173
|
-
/**
|
|
174
|
-
* Get timing jitter for message send (helper for presenceChoreographer/rateLimiter)
|
|
175
|
-
*
|
|
176
|
-
* Usage in beforeSend():
|
|
177
|
-
* const jitter = getMessageSendJitter(fingerprint);
|
|
178
|
-
* await sleep(baseDelay + jitter);
|
|
179
|
-
*/
|
|
180
|
-
export function getMessageSendJitter(fingerprint) {
|
|
181
|
-
// Return a random value within ±50% of the session's base jitter
|
|
182
|
-
// This adds per-message variance while staying within session profile
|
|
183
|
-
const base = fingerprint.networkTiming.sendJitterMs;
|
|
184
|
-
return Math.floor(base * 0.5 + Math.random() * base * 0.5);
|
|
185
|
-
}
|
|
186
|
-
/**
|
|
187
|
-
* Get typing indicator jitter (helper for presenceChoreographer)
|
|
188
|
-
*/
|
|
189
|
-
export function getTypingJitter(fingerprint) {
|
|
190
|
-
const base = fingerprint.networkTiming.typingJitterMs;
|
|
191
|
-
return Math.floor(base * 0.5 + Math.random() * base * 0.5);
|
|
192
|
-
}
|
|
193
|
-
/**
|
|
194
|
-
* Get retry backoff jitter (helper for reconnectThrottle)
|
|
195
|
-
*/
|
|
196
|
-
export function getRetryJitter(fingerprint) {
|
|
197
|
-
const base = fingerprint.networkTiming.retryJitterMs;
|
|
198
|
-
return Math.floor(base * 0.5 + Math.random() * base * 0.5);
|
|
199
|
-
}
|
|
200
|
-
/**
|
|
201
|
-
* Get voice note metadata (helper for voice message encoding)
|
|
202
|
-
*
|
|
203
|
-
* Returns suggested sample rate and duration adjustment based on session fingerprint.
|
|
204
|
-
*/
|
|
205
|
-
export function getVoiceNoteMetadata(fingerprint) {
|
|
206
|
-
return {
|
|
207
|
-
sampleRate: fingerprint.voiceNote.sampleRate,
|
|
208
|
-
durationJitterMs: fingerprint.voiceNote.durationJitterMs,
|
|
209
|
-
waveformSeed: fingerprint.voiceNote.waveformSeed,
|
|
210
|
-
};
|
|
211
|
-
}
|
|
212
|
-
/**
|
|
213
|
-
* Get battery state (helper for presence/connection state signals)
|
|
214
|
-
*/
|
|
215
|
-
export function getBatteryState(fingerprint) {
|
|
216
|
-
return {
|
|
217
|
-
level: fingerprint.connectionState.batteryLevel,
|
|
218
|
-
charging: fingerprint.connectionState.batteryCharging,
|
|
219
|
-
};
|
|
220
|
-
}
|
|
221
|
-
/**
|
|
222
|
-
* Create a session fingerprint preset (Obscura-inspired feature flag pattern)
|
|
223
|
-
*/
|
|
224
|
-
export function createStealthFingerprint(sessionId) {
|
|
225
|
-
return generateSessionFingerprint({
|
|
226
|
-
enabled: true,
|
|
227
|
-
deviceProfile: {
|
|
228
|
-
randomizeAppVersion: true,
|
|
229
|
-
randomizeOsVersion: true,
|
|
230
|
-
randomizeDeviceModel: true,
|
|
231
|
-
},
|
|
232
|
-
networkTiming: {
|
|
233
|
-
sendJitterMs: [100, 500],
|
|
234
|
-
typingJitterMs: [50, 200],
|
|
235
|
-
retryJitterMs: [200, 800],
|
|
236
|
-
},
|
|
237
|
-
voiceNote: {
|
|
238
|
-
randomizeWaveform: true,
|
|
239
|
-
durationJitterMs: 300,
|
|
240
|
-
},
|
|
241
|
-
connectionState: {
|
|
242
|
-
randomizeBattery: true,
|
|
243
|
-
},
|
|
244
|
-
protocolVersion: {
|
|
245
|
-
randomizeSubVersion: true,
|
|
246
|
-
},
|
|
247
|
-
}, sessionId);
|
|
248
|
-
}
|
|
@@ -1,330 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Session Stability Module — Middleware layer for Baileys socket stability
|
|
3
|
-
*
|
|
4
|
-
* Wraps Baileys socket to provide:
|
|
5
|
-
* 1. Canonical JID normalization before sendMessage (reduces mutex race triggers)
|
|
6
|
-
* 2. Typed disconnect reason classification with recovery recommendations
|
|
7
|
-
* 3. Session health monitoring (Bad MAC detection and degradation alerts)
|
|
8
|
-
*
|
|
9
|
-
* This is a pure middleware layer — cannot modify Baileys internals, but can wrap
|
|
10
|
-
* the socket interface to provide stability improvements.
|
|
11
|
-
*
|
|
12
|
-
* @author Kobus Wentzel <kobie@pop.co.za>
|
|
13
|
-
* @license MIT
|
|
14
|
-
*/
|
|
15
|
-
/**
|
|
16
|
-
* Classify Baileys DisconnectReason codes into typed categories.
|
|
17
|
-
* Based on PR #2367 and observed behavior from production bots.
|
|
18
|
-
*/
|
|
19
|
-
export function classifyDisconnect(statusCode) {
|
|
20
|
-
// Fatal errors — logged out or banned, need QR restart
|
|
21
|
-
if (statusCode === 401 || statusCode === 440) {
|
|
22
|
-
return {
|
|
23
|
-
category: 'fatal',
|
|
24
|
-
shouldReconnect: false,
|
|
25
|
-
message: 'Logged out — restart with QR code required',
|
|
26
|
-
code: statusCode,
|
|
27
|
-
};
|
|
28
|
-
}
|
|
29
|
-
if (statusCode === 515) {
|
|
30
|
-
return {
|
|
31
|
-
category: 'fatal',
|
|
32
|
-
shouldReconnect: false,
|
|
33
|
-
message: 'Restart required by WhatsApp — client too old or protocol mismatch',
|
|
34
|
-
code: statusCode,
|
|
35
|
-
};
|
|
36
|
-
}
|
|
37
|
-
// Method not allowed — server rejecting connection method
|
|
38
|
-
if (statusCode === 405) {
|
|
39
|
-
return {
|
|
40
|
-
category: 'fatal',
|
|
41
|
-
shouldReconnect: false,
|
|
42
|
-
message: 'Method not allowed — server rejected connection method',
|
|
43
|
-
code: statusCode,
|
|
44
|
-
};
|
|
45
|
-
}
|
|
46
|
-
// Conflict / Connection replaced — user logged in elsewhere or multi-device conflict
|
|
47
|
-
if (statusCode === 409 || statusCode === 428) {
|
|
48
|
-
return {
|
|
49
|
-
category: 'fatal',
|
|
50
|
-
shouldReconnect: false,
|
|
51
|
-
message: 'Connection replaced — another device took over',
|
|
52
|
-
code: statusCode,
|
|
53
|
-
};
|
|
54
|
-
}
|
|
55
|
-
// Precondition failed — auth state mismatch, retry after delay
|
|
56
|
-
if (statusCode === 412) {
|
|
57
|
-
return {
|
|
58
|
-
category: 'recoverable',
|
|
59
|
-
shouldReconnect: true,
|
|
60
|
-
backoffMs: 30_000, // 30 seconds
|
|
61
|
-
message: 'Precondition failed — auth state mismatch, retry after delay',
|
|
62
|
-
code: statusCode,
|
|
63
|
-
};
|
|
64
|
-
}
|
|
65
|
-
// Rate limited — back off before reconnecting
|
|
66
|
-
if (statusCode === 429) {
|
|
67
|
-
return {
|
|
68
|
-
category: 'rate-limited',
|
|
69
|
-
shouldReconnect: true,
|
|
70
|
-
backoffMs: 300_000, // 5 minutes
|
|
71
|
-
message: 'Rate limited by WhatsApp — cool-off period required',
|
|
72
|
-
code: statusCode,
|
|
73
|
-
};
|
|
74
|
-
}
|
|
75
|
-
if (statusCode === 503) {
|
|
76
|
-
return {
|
|
77
|
-
category: 'rate-limited',
|
|
78
|
-
shouldReconnect: true,
|
|
79
|
-
backoffMs: 60_000, // 1 minute
|
|
80
|
-
message: 'WhatsApp service unavailable — temporary outage',
|
|
81
|
-
code: statusCode,
|
|
82
|
-
};
|
|
83
|
-
}
|
|
84
|
-
// Timeout — transient network issue
|
|
85
|
-
if (statusCode === 408) {
|
|
86
|
-
return {
|
|
87
|
-
category: 'recoverable',
|
|
88
|
-
shouldReconnect: true,
|
|
89
|
-
backoffMs: 5_000, // 5 seconds
|
|
90
|
-
message: 'Connection timeout — network issue, safe to retry',
|
|
91
|
-
code: statusCode,
|
|
92
|
-
};
|
|
93
|
-
}
|
|
94
|
-
// Internal server error — WhatsApp hiccup
|
|
95
|
-
if (statusCode === 500) {
|
|
96
|
-
return {
|
|
97
|
-
category: 'recoverable',
|
|
98
|
-
shouldReconnect: true,
|
|
99
|
-
backoffMs: 10_000, // 10 seconds
|
|
100
|
-
message: 'WhatsApp internal error — temporary server issue',
|
|
101
|
-
code: statusCode,
|
|
102
|
-
};
|
|
103
|
-
}
|
|
104
|
-
// Connection closed gracefully
|
|
105
|
-
if (statusCode === 1000) {
|
|
106
|
-
return {
|
|
107
|
-
category: 'recoverable',
|
|
108
|
-
shouldReconnect: true,
|
|
109
|
-
backoffMs: 2_000, // 2 seconds
|
|
110
|
-
message: 'Connection closed gracefully — safe to reconnect',
|
|
111
|
-
code: statusCode,
|
|
112
|
-
};
|
|
113
|
-
}
|
|
114
|
-
// Unknown code — conservative approach
|
|
115
|
-
return {
|
|
116
|
-
category: 'unknown',
|
|
117
|
-
shouldReconnect: true,
|
|
118
|
-
backoffMs: 15_000, // 15 seconds
|
|
119
|
-
message: `Unknown disconnect reason (code ${statusCode}) — reconnect with caution`,
|
|
120
|
-
code: statusCode,
|
|
121
|
-
};
|
|
122
|
-
}
|
|
123
|
-
const DEFAULT_HEALTH_CONFIG = {
|
|
124
|
-
badMacThreshold: 3,
|
|
125
|
-
badMacWindowMs: 60_000,
|
|
126
|
-
};
|
|
127
|
-
/**
|
|
128
|
-
* Track session health via decrypt success/failure ratio.
|
|
129
|
-
* Emits 'session:degraded' event when Bad MAC rate exceeds threshold.
|
|
130
|
-
*/
|
|
131
|
-
export class SessionHealthMonitor {
|
|
132
|
-
config;
|
|
133
|
-
onDegraded;
|
|
134
|
-
onRecovered;
|
|
135
|
-
stats = {
|
|
136
|
-
decryptSuccess: 0,
|
|
137
|
-
decryptFail: 0,
|
|
138
|
-
badMacCount: 0,
|
|
139
|
-
isDegraded: false,
|
|
140
|
-
};
|
|
141
|
-
badMacTimestamps = [];
|
|
142
|
-
constructor(config = {}) {
|
|
143
|
-
this.config = { ...DEFAULT_HEALTH_CONFIG, ...config };
|
|
144
|
-
this.onDegraded = config.onDegraded;
|
|
145
|
-
this.onRecovered = config.onRecovered;
|
|
146
|
-
}
|
|
147
|
-
/**
|
|
148
|
-
* Record successful decrypt
|
|
149
|
-
*/
|
|
150
|
-
recordDecryptSuccess() {
|
|
151
|
-
this.stats.decryptSuccess++;
|
|
152
|
-
this.checkRecovery();
|
|
153
|
-
}
|
|
154
|
-
/**
|
|
155
|
-
* Record failed decrypt (Bad MAC or similar)
|
|
156
|
-
*/
|
|
157
|
-
recordDecryptFail(isBadMac = false) {
|
|
158
|
-
this.stats.decryptFail++;
|
|
159
|
-
if (isBadMac) {
|
|
160
|
-
const now = Date.now();
|
|
161
|
-
this.stats.badMacCount++;
|
|
162
|
-
this.stats.lastBadMac = new Date(now);
|
|
163
|
-
this.badMacTimestamps.push(now);
|
|
164
|
-
// Clean up old timestamps outside window
|
|
165
|
-
const cutoff = now - this.config.badMacWindowMs;
|
|
166
|
-
this.badMacTimestamps = this.badMacTimestamps.filter(ts => ts > cutoff);
|
|
167
|
-
// Check if we've crossed threshold
|
|
168
|
-
if (!this.stats.isDegraded && this.badMacTimestamps.length >= this.config.badMacThreshold) {
|
|
169
|
-
this.stats.isDegraded = true;
|
|
170
|
-
this.stats.degradedSince = new Date(now);
|
|
171
|
-
this.onDegraded?.(this.getStats());
|
|
172
|
-
}
|
|
173
|
-
}
|
|
174
|
-
}
|
|
175
|
-
/**
|
|
176
|
-
* Check if session has recovered from degraded state
|
|
177
|
-
*/
|
|
178
|
-
checkRecovery() {
|
|
179
|
-
if (!this.stats.isDegraded)
|
|
180
|
-
return;
|
|
181
|
-
const now = Date.now();
|
|
182
|
-
const cutoff = now - this.config.badMacWindowMs;
|
|
183
|
-
this.badMacTimestamps = this.badMacTimestamps.filter(ts => ts > cutoff);
|
|
184
|
-
// Recovered if Bad MAC count dropped below threshold
|
|
185
|
-
if (this.badMacTimestamps.length < this.config.badMacThreshold) {
|
|
186
|
-
this.stats.isDegraded = false;
|
|
187
|
-
this.stats.degradedSince = undefined;
|
|
188
|
-
this.onRecovered?.(this.getStats());
|
|
189
|
-
}
|
|
190
|
-
}
|
|
191
|
-
/**
|
|
192
|
-
* Get current health stats
|
|
193
|
-
*/
|
|
194
|
-
getStats() {
|
|
195
|
-
return { ...this.stats };
|
|
196
|
-
}
|
|
197
|
-
/**
|
|
198
|
-
* Reset all counters
|
|
199
|
-
*/
|
|
200
|
-
reset() {
|
|
201
|
-
this.stats = {
|
|
202
|
-
decryptSuccess: 0,
|
|
203
|
-
decryptFail: 0,
|
|
204
|
-
badMacCount: 0,
|
|
205
|
-
isDegraded: false,
|
|
206
|
-
};
|
|
207
|
-
this.badMacTimestamps = [];
|
|
208
|
-
}
|
|
209
|
-
}
|
|
210
|
-
/**
|
|
211
|
-
* Detects "deaf sessions" — WebSocket connections that stay open but stop
|
|
212
|
-
* delivering messages.upsert / messages.update events.
|
|
213
|
-
*
|
|
214
|
-
* Root cause (Baileys issue #2491): messageMutex holding ACKs hostage under
|
|
215
|
-
* Redis latency spikes causes WhatsApp's server-side flow control to stop
|
|
216
|
-
* delivering messages to that client, while keepAlive pings still succeed.
|
|
217
|
-
*
|
|
218
|
-
* Usage: call onConnect() / onDisconnect() from connection.update events,
|
|
219
|
-
* onMessageActivity() from messages.upsert and messages.update events.
|
|
220
|
-
* Pass a sock reference via attach() so auto-reconnect can call sock.end().
|
|
221
|
-
*/
|
|
222
|
-
export class DeafSessionDetector {
|
|
223
|
-
timeoutMs;
|
|
224
|
-
minUptimeMs;
|
|
225
|
-
autoReconnect;
|
|
226
|
-
onDeafSessionCb;
|
|
227
|
-
lastMessageAt = null;
|
|
228
|
-
connectedAt = null;
|
|
229
|
-
timer = null;
|
|
230
|
-
sockRef = null;
|
|
231
|
-
constructor(config = {}) {
|
|
232
|
-
this.timeoutMs = config.timeoutMs ?? 5 * 60_000;
|
|
233
|
-
this.minUptimeMs = config.minUptimeMs ?? 2 * 60_000;
|
|
234
|
-
this.autoReconnect = config.autoReconnect ?? true;
|
|
235
|
-
this.onDeafSessionCb = config.onDeafSession;
|
|
236
|
-
}
|
|
237
|
-
/** Attach a socket so auto-reconnect can call sock.end() */
|
|
238
|
-
attach(sock) {
|
|
239
|
-
this.sockRef = sock;
|
|
240
|
-
}
|
|
241
|
-
/** Call when connection.update → connection === 'open' */
|
|
242
|
-
onConnect() {
|
|
243
|
-
this.connectedAt = Date.now();
|
|
244
|
-
this.lastMessageAt = Date.now(); // reset — fresh connect is not silence
|
|
245
|
-
this.startTimer();
|
|
246
|
-
}
|
|
247
|
-
/** Call when connection.update → connection === 'close' */
|
|
248
|
-
onDisconnect() {
|
|
249
|
-
this.connectedAt = null;
|
|
250
|
-
this.stopTimer();
|
|
251
|
-
}
|
|
252
|
-
/** Call on every messages.upsert and messages.update event */
|
|
253
|
-
onMessageActivity() {
|
|
254
|
-
this.lastMessageAt = Date.now();
|
|
255
|
-
}
|
|
256
|
-
/** Release the interval — call when discarding the socket */
|
|
257
|
-
destroy() {
|
|
258
|
-
this.stopTimer();
|
|
259
|
-
this.sockRef = null;
|
|
260
|
-
}
|
|
261
|
-
startTimer() {
|
|
262
|
-
this.stopTimer();
|
|
263
|
-
this.timer = setInterval(() => this.check(), 30_000);
|
|
264
|
-
}
|
|
265
|
-
stopTimer() {
|
|
266
|
-
if (this.timer !== null) {
|
|
267
|
-
clearInterval(this.timer);
|
|
268
|
-
this.timer = null;
|
|
269
|
-
}
|
|
270
|
-
}
|
|
271
|
-
check() {
|
|
272
|
-
if (this.connectedAt === null)
|
|
273
|
-
return;
|
|
274
|
-
const now = Date.now();
|
|
275
|
-
const connectedSinceMs = now - this.connectedAt;
|
|
276
|
-
if (connectedSinceMs < this.minUptimeMs)
|
|
277
|
-
return;
|
|
278
|
-
const silenceDurationMs = now - (this.lastMessageAt ?? this.connectedAt);
|
|
279
|
-
if (silenceDurationMs < this.timeoutMs)
|
|
280
|
-
return;
|
|
281
|
-
const info = {
|
|
282
|
-
lastMessageAt: this.lastMessageAt !== null ? new Date(this.lastMessageAt) : null,
|
|
283
|
-
silenceDurationMs,
|
|
284
|
-
connectedSinceMs,
|
|
285
|
-
};
|
|
286
|
-
this.onDeafSessionCb?.(info);
|
|
287
|
-
if (this.autoReconnect && this.sockRef) {
|
|
288
|
-
try {
|
|
289
|
-
this.sockRef.end(new Error('deaf-session'));
|
|
290
|
-
}
|
|
291
|
-
catch {
|
|
292
|
-
// socket may already be closing
|
|
293
|
-
}
|
|
294
|
-
}
|
|
295
|
-
// Stop checking after triggering — let onConnect reset on next reconnect
|
|
296
|
-
this.stopTimer();
|
|
297
|
-
}
|
|
298
|
-
}
|
|
299
|
-
/**
|
|
300
|
-
* Wrap a Baileys socket with session stability features.
|
|
301
|
-
* Returns a Proxy that intercepts sendMessage to canonicalize JIDs.
|
|
302
|
-
*/
|
|
303
|
-
export function wrapWithSessionStability(sock, config = {}) {
|
|
304
|
-
const { canonicalJidNormalization = true, healthMonitoring = true, health: healthConfig, lidResolver, } = config;
|
|
305
|
-
// Initialize health monitor if enabled
|
|
306
|
-
const healthMonitor = healthMonitoring ? new SessionHealthMonitor(healthConfig) : null;
|
|
307
|
-
// Return a Proxy that intercepts method calls
|
|
308
|
-
return new Proxy(sock, {
|
|
309
|
-
get(target, prop) {
|
|
310
|
-
// Intercept sendMessage for JID canonicalization
|
|
311
|
-
if (prop === 'sendMessage' && canonicalJidNormalization && lidResolver) {
|
|
312
|
-
return async (jid, content, options) => {
|
|
313
|
-
// Canonicalize JID using LID resolver
|
|
314
|
-
const canonical = lidResolver.resolveCanonical(jid);
|
|
315
|
-
return target.sendMessage(canonical, content, options);
|
|
316
|
-
};
|
|
317
|
-
}
|
|
318
|
-
// Expose health monitor stats via a getter
|
|
319
|
-
if (prop === 'sessionHealthStats' && healthMonitor) {
|
|
320
|
-
return healthMonitor.getStats();
|
|
321
|
-
}
|
|
322
|
-
// Expose health monitor instance
|
|
323
|
-
if (prop === 'sessionHealthMonitor' && healthMonitor) {
|
|
324
|
-
return healthMonitor;
|
|
325
|
-
}
|
|
326
|
-
// Pass through everything else
|
|
327
|
-
return target[prop];
|
|
328
|
-
},
|
|
329
|
-
});
|
|
330
|
-
}
|