@prototypevip/baileys 0.0.8 → 0.0.9

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
Files changed (54) hide show
  1. package/lib/AntiBan/antiban.js +922 -0
  2. package/lib/AntiBan/banRecoveryOrchestrator.js +284 -0
  3. package/lib/AntiBan/cli.js +177 -0
  4. package/lib/AntiBan/contactGraph.js +236 -0
  5. package/lib/AntiBan/contentVariator.js +150 -0
  6. package/lib/AntiBan/credsSnapshot.js +121 -0
  7. package/lib/AntiBan/deliveryTracker.js +107 -0
  8. package/lib/AntiBan/deviceFingerprint.js +106 -0
  9. package/lib/AntiBan/fleetEventStore.js +157 -0
  10. package/lib/AntiBan/groupOperationGuard.js +116 -0
  11. package/lib/AntiBan/health.js +207 -0
  12. package/lib/AntiBan/humanEntropy.js +337 -0
  13. package/lib/AntiBan/index.js +77 -0
  14. package/lib/AntiBan/instanceCoordinator.js +175 -0
  15. package/lib/AntiBan/jidCanonicalizer.js +256 -0
  16. package/lib/AntiBan/jidCircuitBreaker.js +186 -0
  17. package/lib/AntiBan/legitimacySignalInjector.js +231 -0
  18. package/lib/AntiBan/lidFirstResolver.js +174 -0
  19. package/lib/AntiBan/lidResolver.js +324 -0
  20. package/lib/AntiBan/messageQueue.js +187 -0
  21. package/lib/AntiBan/messageRecovery.js +283 -0
  22. package/lib/AntiBan/messageTypeRegistry.js +369 -0
  23. package/lib/AntiBan/observability.js +145 -0
  24. package/lib/AntiBan/patch.js +302 -0
  25. package/lib/AntiBan/persist.js +87 -0
  26. package/lib/AntiBan/presenceChoreographer.js +430 -0
  27. package/lib/AntiBan/presets.js +105 -0
  28. package/lib/AntiBan/profiles.js +31 -0
  29. package/lib/AntiBan/proxyRotator.js +306 -0
  30. package/lib/AntiBan/rateLimiter.js +261 -0
  31. package/lib/AntiBan/readReceiptVariance.js +88 -0
  32. package/lib/AntiBan/reconnectThrottle.js +180 -0
  33. package/lib/AntiBan/replyRatio.js +161 -0
  34. package/lib/AntiBan/reputationVoucher.js +373 -0
  35. package/lib/AntiBan/retryReason.js +91 -0
  36. package/lib/AntiBan/retryTracker.js +172 -0
  37. package/lib/AntiBan/scheduler.js +111 -0
  38. package/lib/AntiBan/sessionFingerprint.js +248 -0
  39. package/lib/AntiBan/sessionStability.js +330 -0
  40. package/lib/AntiBan/stateAdapter.js +73 -0
  41. package/lib/AntiBan/stateExport.js +152 -0
  42. package/lib/AntiBan/stealthConnect.js +130 -0
  43. package/lib/AntiBan/timelockGuard.js +181 -0
  44. package/lib/AntiBan/topologyThrottler.js +339 -0
  45. package/lib/AntiBan/warmup.js +140 -0
  46. package/lib/AntiBan/webhooks.js +80 -0
  47. package/lib/AntiBan/wrapper.js +456 -0
  48. package/lib/Socket/groups.js +26 -21
  49. package/lib/Socket/index.js +139 -7
  50. package/lib/Socket/messages-send.js +68 -5
  51. package/lib/Utils/ai-rich-formatter.js +256 -0
  52. package/lib/Utils/index.js +1 -0
  53. package/lib/index.js +1 -1
  54. package/package.json +1 -1
@@ -0,0 +1,116 @@
1
+ /**
2
+ * GroupOperationGuard — Rate limiting for WhatsApp group operations
3
+ *
4
+ * Prevents account_reachout_restricted and rate-overlimit errors
5
+ * by enforcing per-operation windows on group adds, removes, and creates.
6
+ *
7
+ * WA unofficial limits (observed):
8
+ * - groupParticipantsUpdate (add): ~3 new contacts per 10 min
9
+ * - groupCreate: ~2 per 10 min
10
+ * - Rapid retries after 403: triggers account_reachout_restricted
11
+ */
12
+ const DEFAULT_LIMITS = {
13
+ add: { max: 3, windowMs: 10 * 60 * 1000 }, // 3 adds / 10 min
14
+ remove: { max: 5, windowMs: 10 * 60 * 1000 }, // 5 removes / 10 min
15
+ create: { max: 2, windowMs: 10 * 60 * 1000 }, // 2 creates / 10 min
16
+ invite: { max: 10, windowMs: 10 * 60 * 1000 }, // 10 invite fetches / 10 min
17
+ };
18
+ /**
19
+ * Known WA error patterns for group operations.
20
+ * Use with classifyGroupOpError() to classify errors before retrying.
21
+ */
22
+ export const GROUP_OP_ERRORS = {
23
+ REACHOUT_RESTRICTED: 'account_reachout_restricted',
24
+ RATE_OVERLIMIT: 'rate-overlimit',
25
+ PRIVACY_BLOCK: '403', // groupParticipantsUpdate returned status '403'
26
+ INVITE_EXPIRED: 'gone', // groupInviteCode returned 'gone' — code expired
27
+ GROUP_LOCKED: 'locked', // groupRevokeInvite returned 'locked'
28
+ };
29
+ /**
30
+ * Classify a caught error from a group operation.
31
+ */
32
+ export function classifyGroupOpError(err) {
33
+ const msg = err instanceof Error ? err.message : String(err);
34
+ if (msg.includes('account_reachout_restricted') || msg.includes('reachout'))
35
+ return GROUP_OP_ERRORS.REACHOUT_RESTRICTED;
36
+ if (msg.includes('rate-overlimit') || msg.includes('429'))
37
+ return GROUP_OP_ERRORS.RATE_OVERLIMIT;
38
+ if (msg.includes('locked'))
39
+ return GROUP_OP_ERRORS.GROUP_LOCKED;
40
+ if (msg.includes('gone'))
41
+ return GROUP_OP_ERRORS.INVITE_EXPIRED;
42
+ return null;
43
+ }
44
+ export function extractPrivacyBlock(result) {
45
+ if (!Array.isArray(result))
46
+ return { blocked: false };
47
+ for (const r of result) {
48
+ const entry = r;
49
+ if (entry.status !== '403')
50
+ continue;
51
+ // Traverse content to find add_request with code
52
+ const content = Array.isArray(entry.content) ? entry.content : [];
53
+ for (const c of content) {
54
+ if (c.tag === 'add_request' && typeof c.attrs === 'object') {
55
+ const attrs = c.attrs;
56
+ if (attrs.code) {
57
+ return {
58
+ blocked: true,
59
+ inviteCode: attrs.code,
60
+ inviteLink: `https://chat.whatsapp.com/${attrs.code}`,
61
+ };
62
+ }
63
+ }
64
+ }
65
+ // 403 without invite code — privacy block but WA didn't provide link
66
+ return { blocked: true };
67
+ }
68
+ return { blocked: false };
69
+ }
70
+ export class GroupOperationGuard {
71
+ limits;
72
+ windows = new Map();
73
+ constructor(config = {}) {
74
+ this.limits = { ...DEFAULT_LIMITS, ...config.limits };
75
+ }
76
+ /**
77
+ * Check whether an operation is allowed under the current rate limits.
78
+ * @param op Operation type
79
+ * @param key Unique key scoping the limit (e.g. groupJid, clientId, or composite)
80
+ */
81
+ check(op, key) {
82
+ const limit = this.limits[op];
83
+ const windowKey = `${op}:${key}`;
84
+ const now = Date.now();
85
+ const entry = this.windows.get(windowKey);
86
+ if (!entry || now > entry.resetAt) {
87
+ this.windows.set(windowKey, { count: 1, resetAt: now + limit.windowMs });
88
+ return { allowed: true };
89
+ }
90
+ if (entry.count >= limit.max) {
91
+ const retryAfterSec = Math.ceil((entry.resetAt - now) / 1000);
92
+ return {
93
+ allowed: false,
94
+ reason: `Too many ${op} attempts. WhatsApp rate-limits group operations — wait ${Math.ceil(retryAfterSec / 60)} min before trying again.`,
95
+ retryAfterSec,
96
+ };
97
+ }
98
+ entry.count++;
99
+ return { allowed: true };
100
+ }
101
+ /**
102
+ * Reset the counter for a specific operation + key.
103
+ * Call this if you want to allow immediate retry after a successful operation.
104
+ */
105
+ reset(op, key) {
106
+ this.windows.delete(`${op}:${key}`);
107
+ }
108
+ /** Snapshot of all active windows (for observability). */
109
+ getStats() {
110
+ const stats = {};
111
+ for (const [key, val] of this.windows) {
112
+ stats[key] = { ...val };
113
+ }
114
+ return stats;
115
+ }
116
+ }
@@ -0,0 +1,207 @@
1
+ /**
2
+ * Health Monitor — Detect ban warning signs early
3
+ *
4
+ * Tracks connection patterns to identify when WhatsApp is
5
+ * getting suspicious. Gives you time to back off before a ban.
6
+ *
7
+ * Warning signs:
8
+ * - Frequent disconnections (connection.update → close)
9
+ * - 403 Forbidden errors
10
+ * - 401 Logged Out (possible temp ban)
11
+ * - Messages failing silently (sent but not delivered)
12
+ * - QR re-request loops
13
+ * - Rate limit responses (429-like behavior)
14
+ */
15
+ const DEFAULT_CONFIG = {
16
+ disconnectWarningThreshold: 3,
17
+ disconnectCriticalThreshold: 5,
18
+ failedMessageThreshold: 5,
19
+ autoPauseAt: 'high',
20
+ };
21
+ export class HealthMonitor {
22
+ config;
23
+ events = [];
24
+ startTime = Date.now();
25
+ paused = false;
26
+ lastRisk = 'low';
27
+ lastBadEventTime = Date.now();
28
+ lastEventWasSevere = false;
29
+ constructor(config = {}) {
30
+ this.config = { ...DEFAULT_CONFIG, ...config };
31
+ }
32
+ /**
33
+ * Record a disconnection event
34
+ */
35
+ recordDisconnect(reason) {
36
+ const reasonStr = String(reason);
37
+ if (reasonStr === '403' || reasonStr === 'forbidden') {
38
+ this.events.push({ type: 'forbidden', timestamp: Date.now(), detail: reasonStr });
39
+ this.lastBadEventTime = Date.now();
40
+ this.lastEventWasSevere = true;
41
+ }
42
+ else if (reasonStr === '401' || reasonStr === 'loggedOut') {
43
+ this.events.push({ type: 'loggedOut', timestamp: Date.now(), detail: reasonStr });
44
+ this.lastBadEventTime = Date.now();
45
+ this.lastEventWasSevere = true;
46
+ }
47
+ else {
48
+ this.events.push({ type: 'disconnect', timestamp: Date.now(), detail: reasonStr });
49
+ this.lastBadEventTime = Date.now();
50
+ this.lastEventWasSevere = false;
51
+ }
52
+ this.checkAndNotify();
53
+ }
54
+ /**
55
+ * Record a successful reconnection
56
+ */
57
+ recordReconnect() {
58
+ this.events.push({ type: 'reconnect', timestamp: Date.now() });
59
+ }
60
+ /**
61
+ * Record a failed message send
62
+ */
63
+ recordMessageFailed(error) {
64
+ this.events.push({ type: 'messageFailed', timestamp: Date.now(), detail: error });
65
+ this.lastBadEventTime = Date.now();
66
+ this.lastEventWasSevere = false;
67
+ this.checkAndNotify();
68
+ }
69
+ /**
70
+ * Record a 463 reachout timelock error
71
+ */
72
+ recordReachoutTimelock(detail) {
73
+ this.events.push({ type: 'reachoutTimelocked', timestamp: Date.now(), detail });
74
+ this.lastBadEventTime = Date.now();
75
+ this.lastEventWasSevere = false;
76
+ this.checkAndNotify();
77
+ }
78
+ /**
79
+ * Get current health status
80
+ */
81
+ getStatus() {
82
+ const now = Date.now();
83
+ this.cleanup(now);
84
+ const hourEvents = this.events.filter(e => now - e.timestamp < 3600000);
85
+ const disconnects = hourEvents.filter(e => e.type === 'disconnect').length;
86
+ const forbidden = hourEvents.filter(e => e.type === 'forbidden').length;
87
+ const loggedOut = hourEvents.filter(e => e.type === 'loggedOut').length;
88
+ const failedMessages = hourEvents.filter(e => e.type === 'messageFailed').length;
89
+ let score = 0;
90
+ const reasons = [];
91
+ // Forbidden errors are serious
92
+ if (forbidden > 0) {
93
+ score += 40 * forbidden;
94
+ reasons.push(`${forbidden} forbidden (403) error${forbidden > 1 ? 's' : ''} in last hour`);
95
+ }
96
+ // Logged out is very serious
97
+ if (loggedOut > 0) {
98
+ score += 60;
99
+ reasons.push('Logged out by WhatsApp — possible temporary ban');
100
+ }
101
+ // Reachout timelocked (463)
102
+ const timelocked = hourEvents.filter(e => e.type === 'reachoutTimelocked').length;
103
+ if (timelocked > 0) {
104
+ score += 25;
105
+ reasons.push(`${timelocked} reachout timelock (463) error${timelocked > 1 ? 's' : ''} in last hour`);
106
+ }
107
+ // Frequent disconnects
108
+ if (disconnects >= this.config.disconnectCriticalThreshold) {
109
+ score += 30;
110
+ reasons.push(`${disconnects} disconnects in last hour (critical threshold)`);
111
+ }
112
+ else if (disconnects >= this.config.disconnectWarningThreshold) {
113
+ score += 30;
114
+ reasons.push(`${disconnects} disconnects in last hour`);
115
+ }
116
+ // Failed messages
117
+ if (failedMessages >= this.config.failedMessageThreshold) {
118
+ score += 20;
119
+ reasons.push(`${failedMessages} failed messages in last hour`);
120
+ }
121
+ // Determine risk level
122
+ score = Math.min(100, score);
123
+ // Tiered decay: recover based on time since last bad event
124
+ // Severe (403/401): 2pts/min — ~50min to clear 100pts
125
+ // Normal: 5pts/min — ~20min to clear 100pts
126
+ const minutesSinceLastBad = (now - this.lastBadEventTime) / 60000;
127
+ const decayRate = this.lastEventWasSevere ? 2 : 5;
128
+ score = Math.max(0, score - Math.floor(minutesSinceLastBad * decayRate));
129
+ let risk;
130
+ if (score >= 80)
131
+ risk = 'critical';
132
+ else if (score >= 40)
133
+ risk = 'high';
134
+ else if (score >= 15)
135
+ risk = 'medium';
136
+ else
137
+ risk = 'low';
138
+ // Determine recommendation
139
+ let recommendation;
140
+ switch (risk) {
141
+ case 'critical':
142
+ recommendation = 'STOP ALL MESSAGING IMMEDIATELY. Disconnect and wait 24-48 hours before reconnecting.';
143
+ break;
144
+ case 'high':
145
+ recommendation = 'Reduce messaging rate by 80%. Consider pausing for 1-2 hours.';
146
+ break;
147
+ case 'medium':
148
+ recommendation = 'Reduce messaging rate by 50%. Increase delays between messages.';
149
+ break;
150
+ default:
151
+ recommendation = 'Operating normally. Continue monitoring.';
152
+ }
153
+ const lastDisconnect = [...this.events].reverse().find(e => e.type === 'disconnect' || e.type === 'forbidden' || e.type === 'loggedOut');
154
+ return {
155
+ risk,
156
+ score,
157
+ reasons: reasons.length ? reasons : ['No issues detected'],
158
+ recommendation,
159
+ stats: {
160
+ disconnectsLastHour: disconnects,
161
+ failedMessagesLastHour: failedMessages,
162
+ forbiddenErrors: forbidden,
163
+ timelockErrors: timelocked,
164
+ uptimeMs: now - this.startTime,
165
+ lastDisconnectReason: lastDisconnect?.detail,
166
+ },
167
+ };
168
+ }
169
+ /**
170
+ * Check if sending should be paused
171
+ */
172
+ isPaused() {
173
+ if (this.paused)
174
+ return true;
175
+ const status = this.getStatus();
176
+ const riskOrder = ['low', 'medium', 'high', 'critical'];
177
+ return riskOrder.indexOf(status.risk) >= riskOrder.indexOf(this.config.autoPauseAt);
178
+ }
179
+ /**
180
+ * Manually pause/resume
181
+ */
182
+ setPaused(paused) {
183
+ this.paused = paused;
184
+ }
185
+ /**
186
+ * Reset all tracked events
187
+ */
188
+ reset() {
189
+ this.events = [];
190
+ this.startTime = Date.now();
191
+ this.paused = false;
192
+ this.lastRisk = 'low';
193
+ this.lastBadEventTime = Date.now();
194
+ this.lastEventWasSevere = false;
195
+ }
196
+ cleanup(now) {
197
+ // Keep last 6 hours of events
198
+ this.events = this.events.filter(e => now - e.timestamp < 21600000);
199
+ }
200
+ checkAndNotify() {
201
+ const status = this.getStatus();
202
+ if (status.risk !== this.lastRisk) {
203
+ this.lastRisk = status.risk;
204
+ this.config.onRiskChange?.(status);
205
+ }
206
+ }
207
+ }
@@ -0,0 +1,337 @@
1
+ /**
2
+ * HumanEntropyService — Background noise for WA sessions
3
+ *
4
+ * Runs periodic human-like actions to make idle sessions appear more realistic:
5
+ * - Random typing presence to recent contacts
6
+ * - Delayed read receipts
7
+ * - Availability status toggles
8
+ *
9
+ * Design:
10
+ * - Works ONLY with WaSP's public API (no direct socket access)
11
+ * - Fail-silent (never crashes wa-pa)
12
+ * - Configurable intervals and probabilities
13
+ * - Only interacts with contacts who messaged first (never strangers)
14
+ */
15
+ const DEFAULT_CONFIG = {
16
+ enabled: true,
17
+ minIntervalMs: 2 * 60 * 60 * 1000, // 2 hours
18
+ maxIntervalMs: 6 * 60 * 60 * 1000, // 6 hours
19
+ maxRecentContacts: 30,
20
+ typingProbability: 0.3,
21
+ typingMinMs: 3000,
22
+ typingMaxMs: 8000,
23
+ readReceiptProbability: 0.2,
24
+ readReceiptMinDelayMs: 10 * 60 * 1000, // 10 min
25
+ readReceiptMaxDelayMs: 60 * 60 * 1000, // 60 min
26
+ presenceToggleProbability: 0.15,
27
+ presenceToggleMinMs: 30 * 1000, // 30 sec
28
+ presenceToggleMaxMs: 2 * 60 * 1000, // 2 min
29
+ };
30
+ /**
31
+ * Human entropy service
32
+ *
33
+ * Adds realistic background noise to WhatsApp sessions by performing
34
+ * random human-like actions periodically.
35
+ */
36
+ export class HumanEntropyService {
37
+ config;
38
+ wasp; // WaSP instance
39
+ sessionId;
40
+ recentContacts = [];
41
+ unreadMessages = [];
42
+ cycleTimer = null;
43
+ isRunning = false;
44
+ stats = {
45
+ cyclesExecuted: 0,
46
+ typingActionsPerformed: 0,
47
+ readReceiptsMarked: 0,
48
+ presenceToggles: 0,
49
+ errors: 0,
50
+ lastCycleAt: null,
51
+ nextCycleAt: null,
52
+ };
53
+ constructor(wasp, sessionId, config = {}) {
54
+ this.wasp = wasp;
55
+ this.sessionId = sessionId;
56
+ this.config = { ...DEFAULT_CONFIG, ...config };
57
+ // Listen to MESSAGE_RECEIVED events to track recent contacts
58
+ this.wasp.on('MESSAGE_RECEIVED', (event) => {
59
+ if (event.sessionId === this.sessionId) {
60
+ this.trackIncomingMessage(event.data);
61
+ }
62
+ });
63
+ this.log('info', 'HumanEntropyService initialized', {
64
+ enabled: this.config.enabled,
65
+ minInterval: this.config.minIntervalMs,
66
+ maxInterval: this.config.maxIntervalMs,
67
+ });
68
+ }
69
+ /**
70
+ * Start the entropy service
71
+ */
72
+ start() {
73
+ if (!this.config.enabled) {
74
+ this.log('info', 'Entropy service disabled, not starting');
75
+ return;
76
+ }
77
+ if (this.isRunning) {
78
+ this.log('warn', 'Entropy service already running');
79
+ return;
80
+ }
81
+ this.isRunning = true;
82
+ this.scheduleNextCycle();
83
+ this.log('info', 'Entropy service started');
84
+ }
85
+ /**
86
+ * Stop the entropy service
87
+ */
88
+ stop() {
89
+ this.isRunning = false;
90
+ if (this.cycleTimer) {
91
+ clearTimeout(this.cycleTimer);
92
+ this.cycleTimer = null;
93
+ }
94
+ this.stats.nextCycleAt = null;
95
+ this.log('info', 'Entropy service stopped');
96
+ }
97
+ /**
98
+ * Get current statistics
99
+ */
100
+ getStats() {
101
+ return { ...this.stats };
102
+ }
103
+ /**
104
+ * Track incoming message to build recent contacts list
105
+ */
106
+ trackIncomingMessage(message) {
107
+ try {
108
+ // Skip outgoing messages
109
+ if (message.fromMe)
110
+ return;
111
+ const jid = message.from || message.chatId;
112
+ if (!jid)
113
+ return;
114
+ // Update or add to recent contacts
115
+ const existingIndex = this.recentContacts.findIndex(c => c.jid === jid);
116
+ if (existingIndex >= 0) {
117
+ this.recentContacts[existingIndex].lastMessageAt = new Date();
118
+ }
119
+ else {
120
+ this.recentContacts.push({
121
+ jid,
122
+ lastMessageAt: new Date(),
123
+ isGroup: message.isGroup ?? false,
124
+ });
125
+ }
126
+ // Trim to max size (keep most recent)
127
+ if (this.recentContacts.length > this.config.maxRecentContacts) {
128
+ this.recentContacts.sort((a, b) => b.lastMessageAt.getTime() - a.lastMessageAt.getTime());
129
+ this.recentContacts = this.recentContacts.slice(0, this.config.maxRecentContacts);
130
+ }
131
+ // Track for potential read receipt
132
+ if (message.id && message.key) {
133
+ this.unreadMessages.push({
134
+ jid,
135
+ messageKey: message.key,
136
+ receivedAt: new Date(),
137
+ });
138
+ // Trim old unread messages (keep last 50)
139
+ if (this.unreadMessages.length > 50) {
140
+ this.unreadMessages = this.unreadMessages.slice(-50);
141
+ }
142
+ }
143
+ }
144
+ catch (error) {
145
+ this.log('error', 'Error tracking incoming message', { error: error.message });
146
+ }
147
+ }
148
+ /**
149
+ * Schedule next entropy cycle
150
+ */
151
+ scheduleNextCycle() {
152
+ if (!this.isRunning)
153
+ return;
154
+ const delay = this.randomBetween(this.config.minIntervalMs, this.config.maxIntervalMs);
155
+ this.stats.nextCycleAt = new Date(Date.now() + delay);
156
+ this.cycleTimer = setTimeout(() => {
157
+ this.executeCycle().catch(error => {
158
+ this.log('error', 'Entropy cycle failed', { error: error.message });
159
+ this.stats.errors++;
160
+ }).finally(() => {
161
+ this.scheduleNextCycle();
162
+ });
163
+ }, delay);
164
+ this.log('debug', `Next entropy cycle in ${Math.floor(delay / 1000)}s`);
165
+ }
166
+ /**
167
+ * Execute one entropy cycle
168
+ */
169
+ async executeCycle() {
170
+ if (!this.isRunning || this.recentContacts.length === 0) {
171
+ return;
172
+ }
173
+ this.log('info', 'Starting entropy cycle', {
174
+ recentContactsCount: this.recentContacts.length,
175
+ unreadMessagesCount: this.unreadMessages.length,
176
+ });
177
+ this.stats.cyclesExecuted++;
178
+ this.stats.lastCycleAt = new Date();
179
+ const actions = [];
180
+ // Action 1: Random typing presence
181
+ if (Math.random() < this.config.typingProbability) {
182
+ actions.push(this.performTypingPresence());
183
+ }
184
+ // Action 2: Mark random message as read
185
+ if (Math.random() < this.config.readReceiptProbability && this.unreadMessages.length > 0) {
186
+ actions.push(this.performReadReceipt());
187
+ }
188
+ // Action 3: Toggle presence status
189
+ if (Math.random() < this.config.presenceToggleProbability) {
190
+ actions.push(this.performPresenceToggle());
191
+ }
192
+ // Execute all actions in parallel
193
+ await Promise.allSettled(actions);
194
+ }
195
+ /**
196
+ * Send typing presence to a random recent contact
197
+ */
198
+ async performTypingPresence() {
199
+ try {
200
+ // Pick a random non-group contact
201
+ const eligibleContacts = this.recentContacts.filter(c => !c.isGroup);
202
+ if (eligibleContacts.length === 0) {
203
+ this.log('debug', 'No eligible contacts for typing presence');
204
+ return;
205
+ }
206
+ const contact = eligibleContacts[Math.floor(Math.random() * eligibleContacts.length)];
207
+ const duration = this.randomBetween(this.config.typingMinMs, this.config.typingMaxMs);
208
+ this.log('info', `Entropy action: typing to ${this.maskJid(contact.jid)} for ${duration}ms`);
209
+ // Get provider to access socket
210
+ const provider = this.wasp.getProvider(this.sessionId);
211
+ if (!provider || !provider.socket) {
212
+ throw new Error('Provider or socket not available');
213
+ }
214
+ // Send composing presence
215
+ await provider.socket.sendPresenceUpdate('composing', contact.jid);
216
+ // Wait for duration
217
+ await this.sleep(duration);
218
+ // Send paused presence
219
+ await provider.socket.sendPresenceUpdate('paused', contact.jid);
220
+ this.stats.typingActionsPerformed++;
221
+ }
222
+ catch (error) {
223
+ this.log('error', 'Error performing typing presence', { error: error.message });
224
+ this.stats.errors++;
225
+ }
226
+ }
227
+ /**
228
+ * Mark a random recent message as read with delay
229
+ */
230
+ async performReadReceipt() {
231
+ try {
232
+ const message = this.unreadMessages[Math.floor(Math.random() * this.unreadMessages.length)];
233
+ if (!message)
234
+ return;
235
+ const delay = this.randomBetween(this.config.readReceiptMinDelayMs, this.config.readReceiptMaxDelayMs);
236
+ this.log('info', `Entropy action: mark read ${this.maskJid(message.jid)} after ${Math.floor(delay / 1000)}s`);
237
+ // Wait for delay
238
+ await this.sleep(delay);
239
+ // Get provider to access socket
240
+ const provider = this.wasp.getProvider(this.sessionId);
241
+ if (!provider || !provider.socket) {
242
+ throw new Error('Provider or socket not available');
243
+ }
244
+ // Mark as read
245
+ await provider.socket.readMessages([message.messageKey]);
246
+ // Remove from unread list
247
+ this.unreadMessages = this.unreadMessages.filter(m => m !== message);
248
+ this.stats.readReceiptsMarked++;
249
+ }
250
+ catch (error) {
251
+ this.log('error', 'Error performing read receipt', { error: error.message });
252
+ this.stats.errors++;
253
+ }
254
+ }
255
+ /**
256
+ * Toggle presence status (available → unavailable)
257
+ */
258
+ async performPresenceToggle() {
259
+ try {
260
+ const duration = this.randomBetween(this.config.presenceToggleMinMs, this.config.presenceToggleMaxMs);
261
+ this.log('info', `Entropy action: presence toggle for ${Math.floor(duration / 1000)}s`);
262
+ // Get provider to access socket
263
+ const provider = this.wasp.getProvider(this.sessionId);
264
+ if (!provider || !provider.socket) {
265
+ throw new Error('Provider or socket not available');
266
+ }
267
+ // Set available
268
+ await provider.socket.sendPresenceUpdate('available');
269
+ // Wait for duration
270
+ await this.sleep(duration);
271
+ // Set unavailable
272
+ await provider.socket.sendPresenceUpdate('unavailable');
273
+ this.stats.presenceToggles++;
274
+ }
275
+ catch (error) {
276
+ this.log('error', 'Error performing presence toggle', { error: error.message });
277
+ this.stats.errors++;
278
+ }
279
+ }
280
+ /**
281
+ * Random value between min and max (inclusive)
282
+ */
283
+ randomBetween(min, max) {
284
+ return Math.floor(Math.random() * (max - min + 1)) + min;
285
+ }
286
+ /**
287
+ * Sleep for specified milliseconds
288
+ */
289
+ sleep(ms) {
290
+ return new Promise(resolve => setTimeout(resolve, ms));
291
+ }
292
+ /**
293
+ * Mask JID for logging (privacy)
294
+ */
295
+ maskJid(jid) {
296
+ if (jid.length < 8)
297
+ return '***';
298
+ return jid.substring(0, 4) + '***' + jid.substring(jid.length - 4);
299
+ }
300
+ /**
301
+ * Log message
302
+ */
303
+ log(level, message, meta) {
304
+ const prefix = '[entropy]';
305
+ const logData = meta ? `${message} ${JSON.stringify(meta)}` : message;
306
+ switch (level) {
307
+ case 'info':
308
+ console.log(`${prefix} ${logData}`);
309
+ break;
310
+ case 'warn':
311
+ console.warn(`${prefix} ${logData}`);
312
+ break;
313
+ case 'error':
314
+ console.error(`${prefix} ${logData}`);
315
+ break;
316
+ case 'debug':
317
+ // Skip debug logs in production
318
+ break;
319
+ }
320
+ }
321
+ }
322
+ /**
323
+ * Factory function to create HumanEntropyService
324
+ *
325
+ * @param wasp WaSP instance
326
+ * @param sessionId Session ID
327
+ * @param config Optional configuration
328
+ * @returns HumanEntropyService instance with start() and stop() methods
329
+ */
330
+ export function createHumanEntropyService(wasp, sessionId, config) {
331
+ const service = new HumanEntropyService(wasp, sessionId, config);
332
+ return {
333
+ start: () => service.start(),
334
+ stop: () => service.stop(),
335
+ getStats: () => service.getStats(),
336
+ };
337
+ }