baileys-antiban 4.6.0 → 4.7.0

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -8,9 +8,9 @@
8
8
 
9
9
  **Drop-in anti-ban middleware for Baileys WhatsApp bots. Free, self-hosted, TypeScript-first. Whapi.Cloud alternative — zero monthly fees.**
10
10
 
11
- > Rate limiting with Gaussian jitter, 7-day warmup, session health monitoring, LID resolver, disconnect classification, contact graph enforcement — all in one `npm install`. Works with [Baileys](https://github.com/WhiskeySockets/Baileys) and [@oxidezap/baileyrs](https://github.com/oxidezap/baileyrs) (Rust/WASM).
11
+ > Rate limiting with Gaussian jitter, 7-day warmup, session health monitoring, LID resolver, disconnect classification, contact graph enforcement, device fingerprinting, group operation guards, recovery orchestration, cross-instance coordination — all in one `npm install`. Works with [Baileys](https://github.com/WhiskeySockets/Baileys) and [@oxidezap/baileyrs](https://github.com/oxidezap/baileyrs) (Rust/WASM).
12
12
 
13
- > **New in v3.3:** [LID Migration Guide](./docs/lid-migration.md) survive Baileys v7's @lid default with stable thread keys.
13
+ > **New in v4.7:** HumanEntropyService background human-like activity (typing indicators, delayed read receipts, presence cycles) to prevent "too perfect bot" detection. Works with WaSP and any session manager, no socket access needed.
14
14
 
15
15
  ## Why Trust This Package
16
16
 
@@ -27,6 +27,167 @@ The npm WhatsApp ecosystem has a malware problem. In April 2026, [`lotusbail`](h
27
27
 
28
28
  If you can't read the code yourself, lean on these signals: signed releases, public audit trail, no telemetry, and a real product behind it. That's the floor. Everything below is the feature set.
29
29
 
30
+ ## v4.x New Features — Production-Grade Ban Prevention
31
+
32
+ v4.0–v4.7 ship seven major anti-ban modules. All are **auto-wired** by default via `wrapSocket()` or `wrapSocketWithFingerprint()`.
33
+
34
+ ### v4.0 — GroupOperationGuard
35
+
36
+ Rate-limits group operations to prevent `account_reachout_restricted` errors. WA limits: ~3 adds/10min, 2 creates/10min.
37
+
38
+ ```typescript
39
+ import { wrapSocket } from 'baileys-antiban';
40
+
41
+ const sock = wrapSocket(makeWASocket({ ... }), {
42
+ groupOpGuard: { limits: { add: { max: 3, windowMs: 600_000 } } },
43
+ });
44
+ ```
45
+
46
+ Classify errors: `classifyGroupOpError(err)` returns `GROUP_OP_ERRORS.REACHOUT_RESTRICTED` | `RATE_OVERLIMIT` | `PRIVACY_BLOCK` | etc.
47
+ Disable: `groupOpGuard: false`
48
+
49
+ ### v4.1 — LegitimacySignalInjector
50
+
51
+ Injects realistic imperfections: typos + corrections (2.5% of messages), read gaps, mid-typing pauses. WhatsApp's ML flags accounts that are "too perfect".
52
+
53
+ ```typescript
54
+ const sock = wrapSocket(makeWASocket({ ... }), {
55
+ legitimacySignals: { typoProbability: 0.03 },
56
+ });
57
+ ```
58
+
59
+ Disable: `legitimacySignals: false`
60
+
61
+ ### v4.2 — BanRecoveryOrchestrator
62
+
63
+ Structured recovery after ban events. Auto-triggers via HealthMonitor at critical risk.
64
+
65
+ ```typescript
66
+ import { BanRecoveryOrchestrator } from 'baileys-antiban';
67
+
68
+ const recovery = new BanRecoveryOrchestrator({
69
+ onPhaseChange: (phase, plan) => console.log(`Phase: ${phase}`),
70
+ onHardBan: () => { /* replace SIM */ },
71
+ });
72
+
73
+ recovery.triggerRecovery('timelock');
74
+ const status = recovery.getStatus();
75
+ console.log(status.rateMultiplier); // 0.1 = 10% speed
76
+ ```
77
+
78
+ **Recovery plans:** timelock: 24h pause, 10% resume, 15%/week ramp | rate_overlimit: 4h, 25%, 25%/week | soft_ban: 48h, 5%, 10%/week | hard_ban: dead
79
+ Access via: `antiban.recoveryOrchestrator`
80
+
81
+ ### v4.3 — wrapSocketWithFingerprint
82
+
83
+ One-call setup with device fingerprint randomization (appVersion, osVersion, deviceModel).
84
+
85
+ ```typescript
86
+ import { wrapSocketWithFingerprint } from 'baileys-antiban';
87
+
88
+ const sock = wrapSocketWithFingerprint(makeWASocket, { auth }, { preset: 'moderate' });
89
+ ```
90
+
91
+ **Preset changes:** All presets default `groupProfiles: true`; `aggressive`/`high-volume` now `autoPauseAt: 'high'`
92
+
93
+ ### v4.4 — Per-Contact Risk Delays + DeliveryTracker
94
+
95
+ Strangers get 2.5× delay, known contacts 1.0×. Active when `contactGraph.enabled: true`.
96
+
97
+ ```typescript
98
+ const sock = wrapSocket(makeWASocket({ ... }), { contactGraph: { enabled: true } });
99
+ // stranger: 2.5×, handshake_sent: 1.8×, handshake_complete: 1.3×, known: 1.0×
100
+ ```
101
+
102
+ **DeliveryTracker** tracks double-tick receipts. <60% delivery = soft-ban signal.
103
+
104
+ ```typescript
105
+ import { DeliveryTracker } from 'baileys-antiban';
106
+
107
+ const tracker = new DeliveryTracker({
108
+ lowRateThreshold: 0.6,
109
+ onLowDeliveryRate: (rate) => console.error(`Delivery: ${rate * 100}%`),
110
+ });
111
+
112
+ sock.ev.on('messages.upsert', ({ messages }) => {
113
+ messages.forEach(m => m.key.fromMe && tracker.onMessageSent(m.key.id));
114
+ });
115
+ sock.ev.on('messages.update', (updates) => {
116
+ updates.forEach(({ key, update }) => {
117
+ if (update.status >= 3) tracker.onDeliveryReceipt(key.id);
118
+ });
119
+ });
120
+ ```
121
+
122
+ ### v4.5 — Adaptive Rate Limiting
123
+
124
+ Auto-adjusts rate based on delivery success: ≥85% → 100% speed, <55% → 25% speed. Auto-wired.
125
+
126
+ ```typescript
127
+ import { RateLimiter } from 'baileys-antiban';
128
+
129
+ const limiter = new RateLimiter({ maxPerMinute: 10 });
130
+ limiter.adaptLimits(0.5); // manual throttle to 50%
131
+ const factor = limiter.getCurrentFactor();
132
+ ```
133
+
134
+ ### v4.6 — Cross-Instance Coordination
135
+
136
+ Shared token bucket across processes. Solves: 5 bots × 8/min = 40/min → flag.
137
+
138
+ ```typescript
139
+ const sock = wrapSocket(makeWASocket({ ... }), {
140
+ instanceCoordinator: '/tmp/wa-pool.json',
141
+ instancePoolMaxPerMinute: 20,
142
+ });
143
+ ```
144
+
145
+ All instances share 20/min IP-level budget. Atomic writes via rename-swap.
146
+
147
+ ### v4.7 — HumanEntropyService
148
+
149
+ Background noise generator that makes a WA session indistinguishable from a real human user during idle periods. Runs independently of your message flow — no socket access required, works with WaSP or any session manager.
150
+
151
+ **The problem it solves:** WhatsApp's ML flags accounts with "too perfect" patterns — instant read receipts, zero typing activity, always-on presence. A listen-only bot that never idles looks like a bot.
152
+
153
+ **What it does every 2-6 hours (randomized):**
154
+ - Sends typing indicator to a recent contact for 3-8 seconds, then stops (mimics "started typing, changed mind")
155
+ - Marks a received message as read with 10-60 min delay (mimics "opened notification, read later")
156
+ - Toggles own presence available → unavailable over 30-120s (mimics "checked phone, put it down")
157
+
158
+ **Safety:** Only contacts people who already messaged you first. Never cold-contacts strangers. All errors caught silently.
159
+
160
+ ```typescript
161
+ import { createHumanEntropyService } from 'baileys-antiban';
162
+
163
+ // Works with WaSP (no direct socket access needed)
164
+ const entropy = createHumanEntropyService(wasp, sessionId, {
165
+ enabled: true,
166
+ minIntervalMs: 2 * 60 * 60 * 1000, // 2 hours
167
+ maxIntervalMs: 6 * 60 * 60 * 1000, // 6 hours
168
+ });
169
+
170
+ entropy.start(); // runs in background
171
+ entropy.stop(); // call on shutdown
172
+
173
+ // Or with direct Baileys socket
174
+ import { HumanEntropyService } from 'baileys-antiban';
175
+ const svc = new HumanEntropyService(socket, { enabled: true });
176
+ svc.start();
177
+ ```
178
+
179
+ **Tracking recent contacts:** Feed incoming messages so the service knows who to interact with:
180
+
181
+ ```typescript
182
+ sock.ev.on('messages.upsert', ({ messages }) => {
183
+ messages.forEach(m => entropy.addRecentContact(m.key.remoteJid, m.key));
184
+ });
185
+ ```
186
+
187
+ Stats: `entropy.getStats()` returns `{ typingActions, readActions, presenceActions, cycles }`.
188
+
189
+ ---
190
+
30
191
  ## v2.0 New Features — Session Stability Module
31
192
 
32
193
  ### What's New in v2.0
@@ -0,0 +1,162 @@
1
+ "use strict";
2
+ /**
3
+ * Fleet Event Store — Multi-instance coordination via shared event log
4
+ *
5
+ * Enables multiple WhatsApp instances to share ban/warn/recovery signals
6
+ * via a pluggable backend (MySQL, in-memory, etc).
7
+ *
8
+ * Architecture:
9
+ * - EventStoreBackend interface — caller provides storage
10
+ * - Two built-in backends:
11
+ * 1. MySQLEventStoreBackend — persistent, multi-instance (peer dep)
12
+ * 2. InMemoryEventStoreBackend — ephemeral, single-instance, testing
13
+ *
14
+ * Usage with MySQL:
15
+ * import mysql from 'mysql2/promise';
16
+ * const pool = mysql.createPool({ ... });
17
+ * const backend = createMySQLEventStoreBackend(pool);
18
+ * const store = createFleetEventStore({
19
+ * connectionId: 'wa-instance-1',
20
+ * backend,
21
+ * pollIntervalMs: 10_000
22
+ * });
23
+ * store.emit('warn', { risk: 'medium' });
24
+ * store.startPolling((events) => console.log('New events:', events));
25
+ *
26
+ * Usage in-memory (testing):
27
+ * const backend = createInMemoryEventStoreBackend();
28
+ * const store = createFleetEventStore({ connectionId: 'test', backend });
29
+ */
30
+ Object.defineProperty(exports, "__esModule", { value: true });
31
+ exports.createFleetEventStore = createFleetEventStore;
32
+ exports.createMySQLEventStoreBackend = createMySQLEventStoreBackend;
33
+ exports.createInMemoryEventStoreBackend = createInMemoryEventStoreBackend;
34
+ function createFleetEventStore(config) {
35
+ const { connectionId, backend, logger } = config;
36
+ const pollIntervalMs = config.pollIntervalMs ?? 10_000;
37
+ let lastSeenEpoch = Date.now();
38
+ let pollTimer = null;
39
+ const emit = async (eventType, payload) => {
40
+ const epoch = Date.now();
41
+ await backend.emit(connectionId, eventType, epoch, payload);
42
+ logger?.info('[fleet-events] emitted', { connectionId, eventType, epoch });
43
+ };
44
+ const startPolling = (onNewEvents) => {
45
+ if (pollTimer)
46
+ return;
47
+ pollTimer = setInterval(() => {
48
+ void (async () => {
49
+ try {
50
+ const events = await backend.poll(connectionId, lastSeenEpoch);
51
+ if (events.length > 0) {
52
+ // Update cursor
53
+ const maxEpoch = Math.max(...events.map((e) => e.epoch));
54
+ lastSeenEpoch = maxEpoch;
55
+ onNewEvents(events);
56
+ }
57
+ }
58
+ catch (error) {
59
+ logger?.warn('[fleet-events] poll failed', { connectionId, err: error });
60
+ }
61
+ })();
62
+ }, pollIntervalMs);
63
+ // @ts-ignore — unref exists on NodeJS.Timeout
64
+ pollTimer.unref?.();
65
+ logger?.info('[fleet-events] polling started', { connectionId, pollIntervalMs });
66
+ };
67
+ const stop = () => {
68
+ if (pollTimer) {
69
+ clearInterval(pollTimer);
70
+ pollTimer = null;
71
+ logger?.info('[fleet-events] polling stopped', { connectionId });
72
+ }
73
+ };
74
+ return { emit, startPolling, stop };
75
+ }
76
+ function createMySQLEventStoreBackend(pool) {
77
+ // Ensure table exists on first emit (idempotent)
78
+ let tableEnsured = false;
79
+ const ensureTable = async () => {
80
+ if (tableEnsured)
81
+ return;
82
+ const createTableSQL = `
83
+ CREATE TABLE IF NOT EXISTS antiban_events (
84
+ id BIGINT AUTO_INCREMENT PRIMARY KEY,
85
+ connection_id VARCHAR(255) NOT NULL,
86
+ event_type VARCHAR(50) NOT NULL,
87
+ epoch BIGINT NOT NULL,
88
+ payload JSON,
89
+ created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
90
+ INDEX idx_conn (connection_id),
91
+ INDEX idx_epoch (epoch)
92
+ ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
93
+ `;
94
+ try {
95
+ await pool.query(createTableSQL);
96
+ tableEnsured = true;
97
+ }
98
+ catch (error) {
99
+ // Log but don't throw — table might already exist
100
+ console.warn('[mysql-backend] table creation failed (may already exist)', error);
101
+ tableEnsured = true; // Assume it exists
102
+ }
103
+ };
104
+ const emit = async (connectionId, eventType, epoch, payload) => {
105
+ try {
106
+ await ensureTable();
107
+ await pool.execute('INSERT INTO antiban_events (connection_id, event_type, epoch, payload) VALUES (?, ?, ?, ?)', [connectionId, eventType, epoch, payload ? JSON.stringify(payload) : null]);
108
+ }
109
+ catch (error) {
110
+ // Never throw — event emission is non-critical
111
+ console.warn('[mysql-backend] emit failed', { connectionId, eventType, error });
112
+ }
113
+ };
114
+ const poll = async (connectionId, sinceEpoch) => {
115
+ try {
116
+ await ensureTable();
117
+ const [rows] = (await pool.execute('SELECT id, connection_id, event_type, epoch, payload, created_at FROM antiban_events WHERE connection_id = ? AND epoch > ? ORDER BY epoch ASC LIMIT 50', [connectionId, sinceEpoch]));
118
+ return rows.map((row) => ({
119
+ id: row.id,
120
+ connectionId: row.connection_id,
121
+ eventType: row.event_type,
122
+ epoch: row.epoch,
123
+ payload: row.payload ? JSON.parse(row.payload) : null,
124
+ createdAt: row.created_at,
125
+ }));
126
+ }
127
+ catch (error) {
128
+ console.warn('[mysql-backend] poll failed', { connectionId, error });
129
+ return [];
130
+ }
131
+ };
132
+ return { emit, poll };
133
+ }
134
+ // ===============================
135
+ // Built-in Backend: In-Memory
136
+ // ===============================
137
+ function createInMemoryEventStoreBackend() {
138
+ const events = [];
139
+ let nextId = 1;
140
+ const emit = async (connectionId, eventType, epoch, payload) => {
141
+ const event = {
142
+ id: nextId++,
143
+ connectionId,
144
+ eventType,
145
+ epoch,
146
+ payload: payload || null,
147
+ createdAt: new Date(),
148
+ };
149
+ events.push(event);
150
+ // Evict oldest if over 1000 entries
151
+ if (events.length > 1000) {
152
+ events.shift();
153
+ }
154
+ };
155
+ const poll = async (connectionId, sinceEpoch) => {
156
+ return events
157
+ .filter((e) => e.connectionId === connectionId && e.epoch > sinceEpoch)
158
+ .sort((a, b) => a.epoch - b.epoch)
159
+ .slice(0, 50);
160
+ };
161
+ return { emit, poll };
162
+ }
@@ -0,0 +1,342 @@
1
+ "use strict";
2
+ /**
3
+ * HumanEntropyService — Background noise for WA sessions
4
+ *
5
+ * Runs periodic human-like actions to make idle sessions appear more realistic:
6
+ * - Random typing presence to recent contacts
7
+ * - Delayed read receipts
8
+ * - Availability status toggles
9
+ *
10
+ * Design:
11
+ * - Works ONLY with WaSP's public API (no direct socket access)
12
+ * - Fail-silent (never crashes wa-pa)
13
+ * - Configurable intervals and probabilities
14
+ * - Only interacts with contacts who messaged first (never strangers)
15
+ */
16
+ Object.defineProperty(exports, "__esModule", { value: true });
17
+ exports.HumanEntropyService = void 0;
18
+ exports.createHumanEntropyService = createHumanEntropyService;
19
+ const DEFAULT_CONFIG = {
20
+ enabled: true,
21
+ minIntervalMs: 2 * 60 * 60 * 1000, // 2 hours
22
+ maxIntervalMs: 6 * 60 * 60 * 1000, // 6 hours
23
+ maxRecentContacts: 30,
24
+ typingProbability: 0.3,
25
+ typingMinMs: 3000,
26
+ typingMaxMs: 8000,
27
+ readReceiptProbability: 0.2,
28
+ readReceiptMinDelayMs: 10 * 60 * 1000, // 10 min
29
+ readReceiptMaxDelayMs: 60 * 60 * 1000, // 60 min
30
+ presenceToggleProbability: 0.15,
31
+ presenceToggleMinMs: 30 * 1000, // 30 sec
32
+ presenceToggleMaxMs: 2 * 60 * 1000, // 2 min
33
+ };
34
+ /**
35
+ * Human entropy service
36
+ *
37
+ * Adds realistic background noise to WhatsApp sessions by performing
38
+ * random human-like actions periodically.
39
+ */
40
+ class HumanEntropyService {
41
+ config;
42
+ wasp; // WaSP instance
43
+ sessionId;
44
+ recentContacts = [];
45
+ unreadMessages = [];
46
+ cycleTimer = null;
47
+ isRunning = false;
48
+ stats = {
49
+ cyclesExecuted: 0,
50
+ typingActionsPerformed: 0,
51
+ readReceiptsMarked: 0,
52
+ presenceToggles: 0,
53
+ errors: 0,
54
+ lastCycleAt: null,
55
+ nextCycleAt: null,
56
+ };
57
+ constructor(wasp, sessionId, config = {}) {
58
+ this.wasp = wasp;
59
+ this.sessionId = sessionId;
60
+ this.config = { ...DEFAULT_CONFIG, ...config };
61
+ // Listen to MESSAGE_RECEIVED events to track recent contacts
62
+ this.wasp.on('MESSAGE_RECEIVED', (event) => {
63
+ if (event.sessionId === this.sessionId) {
64
+ this.trackIncomingMessage(event.data);
65
+ }
66
+ });
67
+ this.log('info', 'HumanEntropyService initialized', {
68
+ enabled: this.config.enabled,
69
+ minInterval: this.config.minIntervalMs,
70
+ maxInterval: this.config.maxIntervalMs,
71
+ });
72
+ }
73
+ /**
74
+ * Start the entropy service
75
+ */
76
+ start() {
77
+ if (!this.config.enabled) {
78
+ this.log('info', 'Entropy service disabled, not starting');
79
+ return;
80
+ }
81
+ if (this.isRunning) {
82
+ this.log('warn', 'Entropy service already running');
83
+ return;
84
+ }
85
+ this.isRunning = true;
86
+ this.scheduleNextCycle();
87
+ this.log('info', 'Entropy service started');
88
+ }
89
+ /**
90
+ * Stop the entropy service
91
+ */
92
+ stop() {
93
+ this.isRunning = false;
94
+ if (this.cycleTimer) {
95
+ clearTimeout(this.cycleTimer);
96
+ this.cycleTimer = null;
97
+ }
98
+ this.stats.nextCycleAt = null;
99
+ this.log('info', 'Entropy service stopped');
100
+ }
101
+ /**
102
+ * Get current statistics
103
+ */
104
+ getStats() {
105
+ return { ...this.stats };
106
+ }
107
+ /**
108
+ * Track incoming message to build recent contacts list
109
+ */
110
+ trackIncomingMessage(message) {
111
+ try {
112
+ // Skip outgoing messages
113
+ if (message.fromMe)
114
+ return;
115
+ const jid = message.from || message.chatId;
116
+ if (!jid)
117
+ return;
118
+ // Update or add to recent contacts
119
+ const existingIndex = this.recentContacts.findIndex(c => c.jid === jid);
120
+ if (existingIndex >= 0) {
121
+ this.recentContacts[existingIndex].lastMessageAt = new Date();
122
+ }
123
+ else {
124
+ this.recentContacts.push({
125
+ jid,
126
+ lastMessageAt: new Date(),
127
+ isGroup: message.isGroup ?? false,
128
+ });
129
+ }
130
+ // Trim to max size (keep most recent)
131
+ if (this.recentContacts.length > this.config.maxRecentContacts) {
132
+ this.recentContacts.sort((a, b) => b.lastMessageAt.getTime() - a.lastMessageAt.getTime());
133
+ this.recentContacts = this.recentContacts.slice(0, this.config.maxRecentContacts);
134
+ }
135
+ // Track for potential read receipt
136
+ if (message.id && message.key) {
137
+ this.unreadMessages.push({
138
+ jid,
139
+ messageKey: message.key,
140
+ receivedAt: new Date(),
141
+ });
142
+ // Trim old unread messages (keep last 50)
143
+ if (this.unreadMessages.length > 50) {
144
+ this.unreadMessages = this.unreadMessages.slice(-50);
145
+ }
146
+ }
147
+ }
148
+ catch (error) {
149
+ this.log('error', 'Error tracking incoming message', { error: error.message });
150
+ }
151
+ }
152
+ /**
153
+ * Schedule next entropy cycle
154
+ */
155
+ scheduleNextCycle() {
156
+ if (!this.isRunning)
157
+ return;
158
+ const delay = this.randomBetween(this.config.minIntervalMs, this.config.maxIntervalMs);
159
+ this.stats.nextCycleAt = new Date(Date.now() + delay);
160
+ this.cycleTimer = setTimeout(() => {
161
+ this.executeCycle().catch(error => {
162
+ this.log('error', 'Entropy cycle failed', { error: error.message });
163
+ this.stats.errors++;
164
+ }).finally(() => {
165
+ this.scheduleNextCycle();
166
+ });
167
+ }, delay);
168
+ this.log('debug', `Next entropy cycle in ${Math.floor(delay / 1000)}s`);
169
+ }
170
+ /**
171
+ * Execute one entropy cycle
172
+ */
173
+ async executeCycle() {
174
+ if (!this.isRunning || this.recentContacts.length === 0) {
175
+ return;
176
+ }
177
+ this.log('info', 'Starting entropy cycle', {
178
+ recentContactsCount: this.recentContacts.length,
179
+ unreadMessagesCount: this.unreadMessages.length,
180
+ });
181
+ this.stats.cyclesExecuted++;
182
+ this.stats.lastCycleAt = new Date();
183
+ const actions = [];
184
+ // Action 1: Random typing presence
185
+ if (Math.random() < this.config.typingProbability) {
186
+ actions.push(this.performTypingPresence());
187
+ }
188
+ // Action 2: Mark random message as read
189
+ if (Math.random() < this.config.readReceiptProbability && this.unreadMessages.length > 0) {
190
+ actions.push(this.performReadReceipt());
191
+ }
192
+ // Action 3: Toggle presence status
193
+ if (Math.random() < this.config.presenceToggleProbability) {
194
+ actions.push(this.performPresenceToggle());
195
+ }
196
+ // Execute all actions in parallel
197
+ await Promise.allSettled(actions);
198
+ }
199
+ /**
200
+ * Send typing presence to a random recent contact
201
+ */
202
+ async performTypingPresence() {
203
+ try {
204
+ // Pick a random non-group contact
205
+ const eligibleContacts = this.recentContacts.filter(c => !c.isGroup);
206
+ if (eligibleContacts.length === 0) {
207
+ this.log('debug', 'No eligible contacts for typing presence');
208
+ return;
209
+ }
210
+ const contact = eligibleContacts[Math.floor(Math.random() * eligibleContacts.length)];
211
+ const duration = this.randomBetween(this.config.typingMinMs, this.config.typingMaxMs);
212
+ this.log('info', `Entropy action: typing to ${this.maskJid(contact.jid)} for ${duration}ms`);
213
+ // Get provider to access socket
214
+ const provider = this.wasp.getProvider(this.sessionId);
215
+ if (!provider || !provider.socket) {
216
+ throw new Error('Provider or socket not available');
217
+ }
218
+ // Send composing presence
219
+ await provider.socket.sendPresenceUpdate('composing', contact.jid);
220
+ // Wait for duration
221
+ await this.sleep(duration);
222
+ // Send paused presence
223
+ await provider.socket.sendPresenceUpdate('paused', contact.jid);
224
+ this.stats.typingActionsPerformed++;
225
+ }
226
+ catch (error) {
227
+ this.log('error', 'Error performing typing presence', { error: error.message });
228
+ this.stats.errors++;
229
+ }
230
+ }
231
+ /**
232
+ * Mark a random recent message as read with delay
233
+ */
234
+ async performReadReceipt() {
235
+ try {
236
+ const message = this.unreadMessages[Math.floor(Math.random() * this.unreadMessages.length)];
237
+ if (!message)
238
+ return;
239
+ const delay = this.randomBetween(this.config.readReceiptMinDelayMs, this.config.readReceiptMaxDelayMs);
240
+ this.log('info', `Entropy action: mark read ${this.maskJid(message.jid)} after ${Math.floor(delay / 1000)}s`);
241
+ // Wait for delay
242
+ await this.sleep(delay);
243
+ // Get provider to access socket
244
+ const provider = this.wasp.getProvider(this.sessionId);
245
+ if (!provider || !provider.socket) {
246
+ throw new Error('Provider or socket not available');
247
+ }
248
+ // Mark as read
249
+ await provider.socket.readMessages([message.messageKey]);
250
+ // Remove from unread list
251
+ this.unreadMessages = this.unreadMessages.filter(m => m !== message);
252
+ this.stats.readReceiptsMarked++;
253
+ }
254
+ catch (error) {
255
+ this.log('error', 'Error performing read receipt', { error: error.message });
256
+ this.stats.errors++;
257
+ }
258
+ }
259
+ /**
260
+ * Toggle presence status (available → unavailable)
261
+ */
262
+ async performPresenceToggle() {
263
+ try {
264
+ const duration = this.randomBetween(this.config.presenceToggleMinMs, this.config.presenceToggleMaxMs);
265
+ this.log('info', `Entropy action: presence toggle for ${Math.floor(duration / 1000)}s`);
266
+ // Get provider to access socket
267
+ const provider = this.wasp.getProvider(this.sessionId);
268
+ if (!provider || !provider.socket) {
269
+ throw new Error('Provider or socket not available');
270
+ }
271
+ // Set available
272
+ await provider.socket.sendPresenceUpdate('available');
273
+ // Wait for duration
274
+ await this.sleep(duration);
275
+ // Set unavailable
276
+ await provider.socket.sendPresenceUpdate('unavailable');
277
+ this.stats.presenceToggles++;
278
+ }
279
+ catch (error) {
280
+ this.log('error', 'Error performing presence toggle', { error: error.message });
281
+ this.stats.errors++;
282
+ }
283
+ }
284
+ /**
285
+ * Random value between min and max (inclusive)
286
+ */
287
+ randomBetween(min, max) {
288
+ return Math.floor(Math.random() * (max - min + 1)) + min;
289
+ }
290
+ /**
291
+ * Sleep for specified milliseconds
292
+ */
293
+ sleep(ms) {
294
+ return new Promise(resolve => setTimeout(resolve, ms));
295
+ }
296
+ /**
297
+ * Mask JID for logging (privacy)
298
+ */
299
+ maskJid(jid) {
300
+ if (jid.length < 8)
301
+ return '***';
302
+ return jid.substring(0, 4) + '***' + jid.substring(jid.length - 4);
303
+ }
304
+ /**
305
+ * Log message
306
+ */
307
+ log(level, message, meta) {
308
+ const prefix = '[entropy]';
309
+ const logData = meta ? `${message} ${JSON.stringify(meta)}` : message;
310
+ switch (level) {
311
+ case 'info':
312
+ console.log(`${prefix} ${logData}`);
313
+ break;
314
+ case 'warn':
315
+ console.warn(`${prefix} ${logData}`);
316
+ break;
317
+ case 'error':
318
+ console.error(`${prefix} ${logData}`);
319
+ break;
320
+ case 'debug':
321
+ // Skip debug logs in production
322
+ break;
323
+ }
324
+ }
325
+ }
326
+ exports.HumanEntropyService = HumanEntropyService;
327
+ /**
328
+ * Factory function to create HumanEntropyService
329
+ *
330
+ * @param wasp WaSP instance
331
+ * @param sessionId Session ID
332
+ * @param config Optional configuration
333
+ * @returns HumanEntropyService instance with start() and stop() methods
334
+ */
335
+ function createHumanEntropyService(wasp, sessionId, config) {
336
+ const service = new HumanEntropyService(wasp, sessionId, config);
337
+ return {
338
+ start: () => service.start(),
339
+ stop: () => service.stop(),
340
+ getStats: () => service.getStats(),
341
+ };
342
+ }