@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,116 +0,0 @@
|
|
|
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
|
-
}
|
package/lib/AntiBan/health.js
DELETED
|
@@ -1,207 +0,0 @@
|
|
|
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
|
-
}
|
|
@@ -1,337 +0,0 @@
|
|
|
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
|
-
}
|