@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
|
@@ -1,73 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* State Adapter — Interface for persisting anti-ban state to disk/DB
|
|
3
|
-
*
|
|
4
|
-
* Implement this interface to save/load state between restarts.
|
|
5
|
-
* Without persistence, warm-up progress is lost on every restart.
|
|
6
|
-
*
|
|
7
|
-
* Example implementations:
|
|
8
|
-
* - File-based: JSON files in a directory
|
|
9
|
-
* - Database: SQLite, PostgreSQL, MongoDB
|
|
10
|
-
* - Redis: For distributed systems
|
|
11
|
-
*
|
|
12
|
-
* Usage:
|
|
13
|
-
* const adapter = new FileStateAdapter('./state');
|
|
14
|
-
* const antiban = new AntiBan({ stateAdapter: adapter });
|
|
15
|
-
* await antiban.loadState(); // On startup
|
|
16
|
-
* await antiban.saveState(); // Periodically or on shutdown
|
|
17
|
-
*/
|
|
18
|
-
/**
|
|
19
|
-
* Example file-based adapter using JSON files
|
|
20
|
-
*/
|
|
21
|
-
export class FileStateAdapter {
|
|
22
|
-
basePath;
|
|
23
|
-
constructor(basePath) {
|
|
24
|
-
this.basePath = basePath;
|
|
25
|
-
}
|
|
26
|
-
async save(key, state) {
|
|
27
|
-
const fs = await import('fs/promises');
|
|
28
|
-
const path = await import('path');
|
|
29
|
-
const filePath = path.join(this.basePath, `${key}.json`);
|
|
30
|
-
await fs.mkdir(this.basePath, { recursive: true });
|
|
31
|
-
await fs.writeFile(filePath, JSON.stringify(state, null, 2), 'utf-8');
|
|
32
|
-
}
|
|
33
|
-
async load(key) {
|
|
34
|
-
const fs = await import('fs/promises');
|
|
35
|
-
const path = await import('path');
|
|
36
|
-
const filePath = path.join(this.basePath, `${key}.json`);
|
|
37
|
-
try {
|
|
38
|
-
const data = await fs.readFile(filePath, 'utf-8');
|
|
39
|
-
return JSON.parse(data);
|
|
40
|
-
}
|
|
41
|
-
catch (err) {
|
|
42
|
-
if (err.code === 'ENOENT')
|
|
43
|
-
return null;
|
|
44
|
-
throw err;
|
|
45
|
-
}
|
|
46
|
-
}
|
|
47
|
-
async delete(key) {
|
|
48
|
-
const fs = await import('fs/promises');
|
|
49
|
-
const path = await import('path');
|
|
50
|
-
const filePath = path.join(this.basePath, `${key}.json`);
|
|
51
|
-
try {
|
|
52
|
-
await fs.unlink(filePath);
|
|
53
|
-
}
|
|
54
|
-
catch (err) {
|
|
55
|
-
if (err.code !== 'ENOENT')
|
|
56
|
-
throw err;
|
|
57
|
-
}
|
|
58
|
-
}
|
|
59
|
-
async list() {
|
|
60
|
-
const fs = await import('fs/promises');
|
|
61
|
-
try {
|
|
62
|
-
const files = await fs.readdir(this.basePath);
|
|
63
|
-
return files
|
|
64
|
-
.filter(f => f.endsWith('.json'))
|
|
65
|
-
.map(f => f.replace(/\.json$/, ''));
|
|
66
|
-
}
|
|
67
|
-
catch (err) {
|
|
68
|
-
if (err.code === 'ENOENT')
|
|
69
|
-
return [];
|
|
70
|
-
throw err;
|
|
71
|
-
}
|
|
72
|
-
}
|
|
73
|
-
}
|
|
@@ -1,152 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Unified State Export — Single-call export/import of ALL antiban state
|
|
3
|
-
*
|
|
4
|
-
* Enables Redis failover and cross-instance state migration.
|
|
5
|
-
* CRDT-safe rate limit counters using increment-only approach.
|
|
6
|
-
*
|
|
7
|
-
* Features:
|
|
8
|
-
* - Single snapshot of all module states
|
|
9
|
-
* - CRDT-safe rate limiter state (never overwrites higher counts)
|
|
10
|
-
* - Version tracking for migration
|
|
11
|
-
* - Instance tracking for debugging
|
|
12
|
-
*/
|
|
13
|
-
/**
|
|
14
|
-
* Export antiban state from individual modules.
|
|
15
|
-
* Call with references to active module instances.
|
|
16
|
-
*/
|
|
17
|
-
export function exportAntibanState(modules) {
|
|
18
|
-
const snapshot = {
|
|
19
|
-
version: 1,
|
|
20
|
-
exportedAt: Date.now(),
|
|
21
|
-
instanceId: modules.instanceId || `instance-${Date.now()}-${Math.random().toString(36).slice(2, 9)}`,
|
|
22
|
-
};
|
|
23
|
-
// Export warmup state
|
|
24
|
-
if (modules.warmup) {
|
|
25
|
-
snapshot.warmup = modules.warmup.exportState();
|
|
26
|
-
}
|
|
27
|
-
// Export health state
|
|
28
|
-
if (modules.health) {
|
|
29
|
-
const status = modules.health.getStatus();
|
|
30
|
-
snapshot.health = {
|
|
31
|
-
riskScore: status.score,
|
|
32
|
-
disconnectEvents: [], // Health module doesn't expose raw events, only aggregate stats
|
|
33
|
-
};
|
|
34
|
-
}
|
|
35
|
-
// Export rate limiter state (CRDT-safe)
|
|
36
|
-
if (modules.rateLimiter) {
|
|
37
|
-
const stats = modules.rateLimiter.getStats();
|
|
38
|
-
snapshot.rateLimiter = {
|
|
39
|
-
messages: [], // We don't expose raw messages, rely on knownChats + counters
|
|
40
|
-
knownChats: Array.from(modules.rateLimiter.getKnownChats()),
|
|
41
|
-
identicalCount: {},
|
|
42
|
-
burstCount: 0,
|
|
43
|
-
lastMessageTime: Date.now(),
|
|
44
|
-
currentFactor: modules.rateLimiter.getCurrentFactor(),
|
|
45
|
-
sentSinceExport: stats.lastDay, // Use last day count as increment baseline
|
|
46
|
-
};
|
|
47
|
-
}
|
|
48
|
-
// Export circuit breaker states (BUG FIX 2)
|
|
49
|
-
if (modules.circuits?.exportState) {
|
|
50
|
-
snapshot.circuits = modules.circuits.exportState();
|
|
51
|
-
}
|
|
52
|
-
// Export timelock guard state
|
|
53
|
-
if (modules.timelockGuard) {
|
|
54
|
-
const state = modules.timelockGuard.getState();
|
|
55
|
-
const knownChats = modules.timelockGuard.getKnownChats ? Array.from(modules.timelockGuard.getKnownChats()) : [];
|
|
56
|
-
snapshot.timelockGuard = {
|
|
57
|
-
active: state.isActive,
|
|
58
|
-
expiresAt: state.expiresAt ? state.expiresAt.getTime() : undefined,
|
|
59
|
-
affectedJids: knownChats,
|
|
60
|
-
enforcementType: state.enforcementType,
|
|
61
|
-
};
|
|
62
|
-
}
|
|
63
|
-
// Export message registry state
|
|
64
|
-
if (modules.messageRegistry) {
|
|
65
|
-
snapshot.messageRegistry = modules.messageRegistry.exportState();
|
|
66
|
-
}
|
|
67
|
-
// Export topology throttler state
|
|
68
|
-
if (modules.topologyThrottler) {
|
|
69
|
-
snapshot.topologyThrottler = modules.topologyThrottler.exportState();
|
|
70
|
-
}
|
|
71
|
-
// Export reputation voucher state
|
|
72
|
-
if (modules.reputationVoucher) {
|
|
73
|
-
snapshot.reputationVoucher = modules.reputationVoucher.exportState();
|
|
74
|
-
}
|
|
75
|
-
// Export engagement scores
|
|
76
|
-
if (modules.engagementScores) {
|
|
77
|
-
snapshot.engagementScores = Object.fromEntries(modules.engagementScores.entries());
|
|
78
|
-
}
|
|
79
|
-
return snapshot;
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Import antiban state into modules.
|
|
83
|
-
* CRDT-safe for rate limiters (never overwrites higher counts).
|
|
84
|
-
*/
|
|
85
|
-
export function importAntibanState(snapshot, modules) {
|
|
86
|
-
// Import warmup state
|
|
87
|
-
if (snapshot.warmup && modules.warmup?.importState) {
|
|
88
|
-
modules.warmup.importState(snapshot.warmup);
|
|
89
|
-
}
|
|
90
|
-
// Import health state (reset to clean state, can't import raw events)
|
|
91
|
-
if (snapshot.health && modules.health?.reset) {
|
|
92
|
-
modules.health.reset();
|
|
93
|
-
}
|
|
94
|
-
// Import rate limiter state (CRDT-safe)
|
|
95
|
-
if (snapshot.rateLimiter && modules.rateLimiter) {
|
|
96
|
-
// Restore known chats
|
|
97
|
-
if (snapshot.rateLimiter.knownChats) {
|
|
98
|
-
modules.rateLimiter.restoreKnownChats(snapshot.rateLimiter.knownChats);
|
|
99
|
-
}
|
|
100
|
-
// CRDT-safe merge: only restore factor if not already higher
|
|
101
|
-
const currentStats = modules.rateLimiter.getStats();
|
|
102
|
-
const currentCount = currentStats.lastDay;
|
|
103
|
-
const snapshotCount = snapshot.rateLimiter.sentSinceExport;
|
|
104
|
-
// If current count is lower, we're likely a fresh instance, so restore factor
|
|
105
|
-
if (currentCount < snapshotCount) {
|
|
106
|
-
modules.rateLimiter.adaptLimits(snapshot.rateLimiter.currentFactor);
|
|
107
|
-
}
|
|
108
|
-
// If current count is higher, keep current state (concurrent instance ahead)
|
|
109
|
-
}
|
|
110
|
-
// Import circuit breaker states (BUG FIX 2)
|
|
111
|
-
if (snapshot.circuits && modules.circuits?.importState) {
|
|
112
|
-
modules.circuits.importState(snapshot.circuits);
|
|
113
|
-
}
|
|
114
|
-
// Import timelock guard state
|
|
115
|
-
if (snapshot.timelockGuard && modules.timelockGuard) {
|
|
116
|
-
if (snapshot.timelockGuard.active && modules.timelockGuard.onTimelockUpdate) {
|
|
117
|
-
const expiresAt = snapshot.timelockGuard.expiresAt
|
|
118
|
-
? new Date(snapshot.timelockGuard.expiresAt)
|
|
119
|
-
: undefined;
|
|
120
|
-
modules.timelockGuard.onTimelockUpdate({
|
|
121
|
-
isActive: snapshot.timelockGuard.active,
|
|
122
|
-
timeEnforcementEnds: expiresAt,
|
|
123
|
-
enforcementType: snapshot.timelockGuard.enforcementType,
|
|
124
|
-
});
|
|
125
|
-
// Restore known chats
|
|
126
|
-
if (snapshot.timelockGuard.affectedJids && modules.timelockGuard.registerKnownChats) {
|
|
127
|
-
modules.timelockGuard.registerKnownChats(snapshot.timelockGuard.affectedJids);
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
else {
|
|
131
|
-
modules.timelockGuard.reset();
|
|
132
|
-
}
|
|
133
|
-
}
|
|
134
|
-
// Import message registry state
|
|
135
|
-
if (snapshot.messageRegistry && modules.messageRegistry) {
|
|
136
|
-
modules.messageRegistry.importState(snapshot.messageRegistry);
|
|
137
|
-
}
|
|
138
|
-
// Import topology throttler state
|
|
139
|
-
if (snapshot.topologyThrottler && modules.topologyThrottler) {
|
|
140
|
-
modules.topologyThrottler.importState(snapshot.topologyThrottler);
|
|
141
|
-
}
|
|
142
|
-
// Import reputation voucher state
|
|
143
|
-
if (snapshot.reputationVoucher && modules.reputationVoucher) {
|
|
144
|
-
modules.reputationVoucher.importState(snapshot.reputationVoucher);
|
|
145
|
-
}
|
|
146
|
-
// Import engagement scores
|
|
147
|
-
if (snapshot.engagementScores && modules.engagementScores) {
|
|
148
|
-
for (const [jid, score] of Object.entries(snapshot.engagementScores)) {
|
|
149
|
-
modules.engagementScores.set(jid, score);
|
|
150
|
-
}
|
|
151
|
-
}
|
|
152
|
-
}
|
|
@@ -1,130 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Stealth Connect — Reduce ban signal on socket connect + presence ramp
|
|
3
|
-
*
|
|
4
|
-
* Inspired by GOWA's --presence-on-connect=unavailable flag. Bots that
|
|
5
|
-
* snap online immediately and start blasting messages look suspicious to
|
|
6
|
-
* WhatsApp's anti-spam classifier. This module ships two helpers:
|
|
7
|
-
*
|
|
8
|
-
* - `getStealthSocketConfig()` returns a partial Baileys socket config
|
|
9
|
-
* that disables `markOnlineOnConnect` and provides a non-default
|
|
10
|
-
* browser fingerprint (random pick from a small pool unless overridden).
|
|
11
|
-
*
|
|
12
|
-
* - `rampPresenceAfterConnect()` waits a randomized delay, then issues
|
|
13
|
-
* `sendPresenceUpdate('available')`. Supports `AbortSignal` so the
|
|
14
|
-
* caller can cancel if the socket dies before the timer fires.
|
|
15
|
-
*
|
|
16
|
-
* Usage:
|
|
17
|
-
* const config = getStealthSocketConfig({ os: 'MyApp' });
|
|
18
|
-
* const sock = makeWASocket({ ...config, auth: state });
|
|
19
|
-
*
|
|
20
|
-
* const ac = new AbortController();
|
|
21
|
-
* sock.ev.on('connection.update', u => {
|
|
22
|
-
* if (u.connection === 'close') ac.abort();
|
|
23
|
-
* });
|
|
24
|
-
* await rampPresenceAfterConnect(sock, { signal: ac.signal });
|
|
25
|
-
*/
|
|
26
|
-
/**
|
|
27
|
-
* Pool of realistic browser fingerprints.
|
|
28
|
-
*
|
|
29
|
-
* Values match formats actually emitted by WhatsApp Web clients in the wild.
|
|
30
|
-
* `getStealthSocketConfig()` picks one at random when caller does not supply
|
|
31
|
-
* an explicit `browser` or `os` option, so multiple consumers of the library
|
|
32
|
-
* do not all advertise an identical fingerprint (which would be trivially
|
|
33
|
-
* cluster-able by WhatsApp).
|
|
34
|
-
*
|
|
35
|
-
* Exported so callers can extend or override the pool if desired.
|
|
36
|
-
*/
|
|
37
|
-
export const STEALTH_BROWSER_POOL = Object.freeze([
|
|
38
|
-
['Mac OS', 'Chrome', '120.0.6099.109'],
|
|
39
|
-
['Mac OS', 'Safari', '17.2.1'],
|
|
40
|
-
['Windows', 'Chrome', '121.0.6167.85'],
|
|
41
|
-
['Windows', 'Firefox', '122.0'],
|
|
42
|
-
['Windows', 'Edge', '120.0.2210.144'],
|
|
43
|
-
['Linux', 'Chrome', '120.0.6099.109'],
|
|
44
|
-
['Linux', 'Firefox', '122.0'],
|
|
45
|
-
['Ubuntu', 'Chrome', '121.0.6167.85'],
|
|
46
|
-
]);
|
|
47
|
-
/**
|
|
48
|
-
* Returns a partial Baileys socket config tuned for stealth connect.
|
|
49
|
-
*
|
|
50
|
-
* - `markOnlineOnConnect` set to `false` so the socket joins without
|
|
51
|
-
* broadcasting `available` (matches GOWA's `presence-on-connect=unavailable`).
|
|
52
|
-
* - `browser` is a randomized realistic tuple from `STEALTH_BROWSER_POOL`
|
|
53
|
-
* unless overridden via `opts.browser` or `opts.os`.
|
|
54
|
-
*
|
|
55
|
-
* Merge the result into your `makeWASocket` options:
|
|
56
|
-
*
|
|
57
|
-
* const sock = makeWASocket({ ...getStealthSocketConfig(), auth: state });
|
|
58
|
-
*/
|
|
59
|
-
export function getStealthSocketConfig(opts) {
|
|
60
|
-
const random = opts?.random ?? Math.random;
|
|
61
|
-
let browser;
|
|
62
|
-
if (opts?.browser) {
|
|
63
|
-
browser = opts.browser;
|
|
64
|
-
}
|
|
65
|
-
else {
|
|
66
|
-
const pick = STEALTH_BROWSER_POOL[Math.floor(random() * STEALTH_BROWSER_POOL.length)];
|
|
67
|
-
// pick is guaranteed defined — pool is non-empty and frozen.
|
|
68
|
-
const tuple = pick;
|
|
69
|
-
if (opts?.os) {
|
|
70
|
-
browser = [opts.os, tuple[1], tuple[2]];
|
|
71
|
-
}
|
|
72
|
-
else {
|
|
73
|
-
browser = [tuple[0], tuple[1], tuple[2]];
|
|
74
|
-
}
|
|
75
|
-
}
|
|
76
|
-
return {
|
|
77
|
-
markOnlineOnConnect: false,
|
|
78
|
-
browser,
|
|
79
|
-
};
|
|
80
|
-
}
|
|
81
|
-
/**
|
|
82
|
-
* Custom error thrown when `rampPresenceAfterConnect` is aborted via signal.
|
|
83
|
-
* Mirrors DOM `AbortError` semantics so consumers can `instanceof` check.
|
|
84
|
-
*/
|
|
85
|
-
export class AbortError extends Error {
|
|
86
|
-
name = 'AbortError';
|
|
87
|
-
constructor(message = 'rampPresenceAfterConnect aborted') {
|
|
88
|
-
super(message);
|
|
89
|
-
}
|
|
90
|
-
}
|
|
91
|
-
/**
|
|
92
|
-
* Waits a randomized delay then calls `sock.sendPresenceUpdate(targetState)`.
|
|
93
|
-
*
|
|
94
|
-
* Supports `AbortSignal` — abort during the delay window cancels the timer
|
|
95
|
-
* and rejects the returned promise with `AbortError`. Aborting after the
|
|
96
|
-
* presence update has already been sent is a no-op.
|
|
97
|
-
*
|
|
98
|
-
* Caller is responsible for invoking abort when the socket disconnects;
|
|
99
|
-
* otherwise the post-delay `sendPresenceUpdate` may run against a dead
|
|
100
|
-
* socket.
|
|
101
|
-
*/
|
|
102
|
-
export async function rampPresenceAfterConnect(sock, opts) {
|
|
103
|
-
const minDelayMs = opts?.minDelayMs ?? 30000;
|
|
104
|
-
const maxDelayMs = opts?.maxDelayMs ?? 90000;
|
|
105
|
-
const targetState = opts?.targetState ?? 'available';
|
|
106
|
-
const random = opts?.random ?? Math.random;
|
|
107
|
-
const signal = opts?.signal;
|
|
108
|
-
if (signal?.aborted) {
|
|
109
|
-
throw new AbortError();
|
|
110
|
-
}
|
|
111
|
-
const range = Math.max(0, maxDelayMs - minDelayMs);
|
|
112
|
-
const delayMs = Math.floor(random() * (range + 1)) + minDelayMs;
|
|
113
|
-
await new Promise((resolve, reject) => {
|
|
114
|
-
const timer = setTimeout(() => {
|
|
115
|
-
if (signal) {
|
|
116
|
-
signal.removeEventListener('abort', onAbort);
|
|
117
|
-
}
|
|
118
|
-
resolve();
|
|
119
|
-
}, delayMs);
|
|
120
|
-
const onAbort = () => {
|
|
121
|
-
clearTimeout(timer);
|
|
122
|
-
reject(new AbortError());
|
|
123
|
-
};
|
|
124
|
-
if (signal) {
|
|
125
|
-
signal.addEventListener('abort', onAbort, { once: true });
|
|
126
|
-
}
|
|
127
|
-
});
|
|
128
|
-
// jid undefined => broadcast presence to all conversations
|
|
129
|
-
await sock.sendPresenceUpdate(targetState, undefined);
|
|
130
|
-
}
|
|
@@ -1,181 +0,0 @@
|
|
|
1
|
-
/**
|
|
2
|
-
* Timelock Guard — Manages reachout timelock state and routing decisions
|
|
3
|
-
*
|
|
4
|
-
* When WhatsApp timelocks an account (463 error), this guard:
|
|
5
|
-
* - Tracks the timelock state (active, expiry, enforcement type)
|
|
6
|
-
* - Blocks messages to NEW contacts (no tctoken / no prior chat)
|
|
7
|
-
* - Allows messages to EXISTING contacts (have tctoken / prior chat history)
|
|
8
|
-
* - Auto-resumes when the timelock expires
|
|
9
|
-
* - Fires callbacks for alerting (Telegram/Discord/webhook)
|
|
10
|
-
*/
|
|
11
|
-
const DEFAULT_CONFIG = {
|
|
12
|
-
resumeBufferMs: 10_000,
|
|
13
|
-
};
|
|
14
|
-
export class TimelockGuard {
|
|
15
|
-
config;
|
|
16
|
-
state = {
|
|
17
|
-
isActive: false,
|
|
18
|
-
errorCount: 0,
|
|
19
|
-
};
|
|
20
|
-
knownChats = new Set();
|
|
21
|
-
resumeTimer = null;
|
|
22
|
-
timerGeneration = 0; // BUG FIX 4: Track timer validity to prevent race conditions
|
|
23
|
-
constructor(config = {}) {
|
|
24
|
-
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
25
|
-
}
|
|
26
|
-
/**
|
|
27
|
-
* Update timelock state from Baileys connection.update event
|
|
28
|
-
*/
|
|
29
|
-
onTimelockUpdate(data) {
|
|
30
|
-
const wasActive = this.state.isActive;
|
|
31
|
-
this.state.isActive = !!data.isActive;
|
|
32
|
-
this.state.enforcementType = data.enforcementType;
|
|
33
|
-
this.state.expiresAt = data.timeEnforcementEnds;
|
|
34
|
-
if (this.state.isActive && !wasActive) {
|
|
35
|
-
this.state.detectedAt = new Date();
|
|
36
|
-
this.state.errorCount = 0;
|
|
37
|
-
this.config.onTimelockDetected?.(this.getState());
|
|
38
|
-
this.scheduleResume();
|
|
39
|
-
}
|
|
40
|
-
else if (this.state.isActive && wasActive) {
|
|
41
|
-
// Already locked but expiry updated — reschedule timer with new expiry
|
|
42
|
-
this.scheduleResume();
|
|
43
|
-
}
|
|
44
|
-
if (!this.state.isActive && wasActive) {
|
|
45
|
-
this.clearResumeTimer();
|
|
46
|
-
this.config.onTimelockLifted?.(this.getState());
|
|
47
|
-
}
|
|
48
|
-
}
|
|
49
|
-
/**
|
|
50
|
-
* Record a 463 error from a failed send
|
|
51
|
-
*/
|
|
52
|
-
record463Error() {
|
|
53
|
-
this.state.errorCount++;
|
|
54
|
-
if (!this.state.isActive) {
|
|
55
|
-
// First 463 before MEX query returns — assume locked for 60s
|
|
56
|
-
this.state.isActive = true;
|
|
57
|
-
this.state.detectedAt = new Date();
|
|
58
|
-
this.state.expiresAt = new Date(Date.now() + 60_000);
|
|
59
|
-
this.config.onTimelockDetected?.(this.getState());
|
|
60
|
-
this.scheduleResume();
|
|
61
|
-
}
|
|
62
|
-
}
|
|
63
|
-
/**
|
|
64
|
-
* Register a JID as a known/existing chat (has tctoken / prior history)
|
|
65
|
-
*/
|
|
66
|
-
registerKnownChat(jid) {
|
|
67
|
-
this.knownChats.add(jid);
|
|
68
|
-
}
|
|
69
|
-
/**
|
|
70
|
-
* Register multiple known chats at once (e.g. from chat list on connect)
|
|
71
|
-
*/
|
|
72
|
-
registerKnownChats(jids) {
|
|
73
|
-
for (const jid of jids) {
|
|
74
|
-
this.knownChats.add(jid);
|
|
75
|
-
}
|
|
76
|
-
}
|
|
77
|
-
/**
|
|
78
|
-
* Check if a message to this recipient should be allowed
|
|
79
|
-
*/
|
|
80
|
-
canSend(jid) {
|
|
81
|
-
if (!this.state.isActive) {
|
|
82
|
-
return { allowed: true };
|
|
83
|
-
}
|
|
84
|
-
// Check if expiry has passed (with buffer)
|
|
85
|
-
if (this.state.expiresAt) {
|
|
86
|
-
const expiryWithBuffer = this.state.expiresAt.getTime() + this.config.resumeBufferMs;
|
|
87
|
-
if (Date.now() >= expiryWithBuffer) {
|
|
88
|
-
this.lift();
|
|
89
|
-
return { allowed: true };
|
|
90
|
-
}
|
|
91
|
-
}
|
|
92
|
-
// Groups always flow (tctokens not required for group msgs)
|
|
93
|
-
if (jid.endsWith('@g.us') || jid.endsWith('@newsletter')) {
|
|
94
|
-
return { allowed: true };
|
|
95
|
-
}
|
|
96
|
-
// Known chats (have tctoken) can proceed
|
|
97
|
-
if (this.knownChats.has(jid)) {
|
|
98
|
-
return { allowed: true };
|
|
99
|
-
}
|
|
100
|
-
// New contact while timelocked — block
|
|
101
|
-
const expiresIn = this.state.expiresAt
|
|
102
|
-
? Math.max(0, this.state.expiresAt.getTime() - Date.now())
|
|
103
|
-
: 60_000;
|
|
104
|
-
return {
|
|
105
|
-
allowed: false,
|
|
106
|
-
reason: `Reachout timelocked (${this.state.enforcementType || 'unknown'}). `
|
|
107
|
-
+ `New contacts blocked. Expires in ${Math.ceil(expiresIn / 1000)}s.`,
|
|
108
|
-
};
|
|
109
|
-
}
|
|
110
|
-
/**
|
|
111
|
-
* Get current timelock state
|
|
112
|
-
*/
|
|
113
|
-
getState() {
|
|
114
|
-
return { ...this.state };
|
|
115
|
-
}
|
|
116
|
-
/**
|
|
117
|
-
* Check if currently timelocked
|
|
118
|
-
*/
|
|
119
|
-
isTimelocked() {
|
|
120
|
-
if (!this.state.isActive)
|
|
121
|
-
return false;
|
|
122
|
-
// Auto-lift if expired
|
|
123
|
-
if (this.state.expiresAt) {
|
|
124
|
-
const expiryWithBuffer = this.state.expiresAt.getTime() + this.config.resumeBufferMs;
|
|
125
|
-
if (Date.now() >= expiryWithBuffer) {
|
|
126
|
-
this.lift();
|
|
127
|
-
return false;
|
|
128
|
-
}
|
|
129
|
-
}
|
|
130
|
-
return true;
|
|
131
|
-
}
|
|
132
|
-
/**
|
|
133
|
-
* Get the set of known chat JIDs
|
|
134
|
-
*/
|
|
135
|
-
getKnownChats() {
|
|
136
|
-
return new Set(this.knownChats);
|
|
137
|
-
}
|
|
138
|
-
/**
|
|
139
|
-
* Manually lift the timelock
|
|
140
|
-
*/
|
|
141
|
-
lift() {
|
|
142
|
-
if (this.state.isActive) {
|
|
143
|
-
this.state.isActive = false;
|
|
144
|
-
this.clearResumeTimer();
|
|
145
|
-
this.config.onTimelockLifted?.(this.getState());
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
/**
|
|
149
|
-
* Reset all state
|
|
150
|
-
*/
|
|
151
|
-
reset() {
|
|
152
|
-
this.state = { isActive: false, errorCount: 0 };
|
|
153
|
-
this.knownChats.clear();
|
|
154
|
-
this.clearResumeTimer();
|
|
155
|
-
}
|
|
156
|
-
scheduleResume() {
|
|
157
|
-
this.clearResumeTimer();
|
|
158
|
-
if (this.state.expiresAt) {
|
|
159
|
-
const delay = this.state.expiresAt.getTime() - Date.now() + this.config.resumeBufferMs;
|
|
160
|
-
if (delay > 0) {
|
|
161
|
-
// BUG FIX 4: Increment generation to invalidate any pending timers
|
|
162
|
-
this.timerGeneration++;
|
|
163
|
-
const currentGeneration = this.timerGeneration;
|
|
164
|
-
this.resumeTimer = setTimeout(() => {
|
|
165
|
-
// Only lift if this timer is still valid (not superseded by a newer scheduleResume call)
|
|
166
|
-
if (currentGeneration === this.timerGeneration) {
|
|
167
|
-
this.lift();
|
|
168
|
-
}
|
|
169
|
-
}, delay);
|
|
170
|
-
}
|
|
171
|
-
}
|
|
172
|
-
}
|
|
173
|
-
clearResumeTimer() {
|
|
174
|
-
if (this.resumeTimer) {
|
|
175
|
-
clearTimeout(this.resumeTimer);
|
|
176
|
-
this.resumeTimer = null;
|
|
177
|
-
// Increment generation to invalidate any pending timer callbacks
|
|
178
|
-
this.timerGeneration++;
|
|
179
|
-
}
|
|
180
|
-
}
|
|
181
|
-
}
|