@prototypevip/baileys 0.0.9 → 0.1.1

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 (50) hide show
  1. package/lib/Socket/index.js +12 -135
  2. package/lib/Socket/messages-send.js +27 -44
  3. package/package.json +2 -2
  4. package/lib/AntiBan/antiban.js +0 -922
  5. package/lib/AntiBan/banRecoveryOrchestrator.js +0 -284
  6. package/lib/AntiBan/cli.js +0 -177
  7. package/lib/AntiBan/contactGraph.js +0 -236
  8. package/lib/AntiBan/contentVariator.js +0 -150
  9. package/lib/AntiBan/credsSnapshot.js +0 -121
  10. package/lib/AntiBan/deliveryTracker.js +0 -107
  11. package/lib/AntiBan/deviceFingerprint.js +0 -106
  12. package/lib/AntiBan/fleetEventStore.js +0 -157
  13. package/lib/AntiBan/groupOperationGuard.js +0 -116
  14. package/lib/AntiBan/health.js +0 -207
  15. package/lib/AntiBan/humanEntropy.js +0 -337
  16. package/lib/AntiBan/index.js +0 -77
  17. package/lib/AntiBan/instanceCoordinator.js +0 -175
  18. package/lib/AntiBan/jidCanonicalizer.js +0 -256
  19. package/lib/AntiBan/jidCircuitBreaker.js +0 -186
  20. package/lib/AntiBan/legitimacySignalInjector.js +0 -231
  21. package/lib/AntiBan/lidFirstResolver.js +0 -174
  22. package/lib/AntiBan/lidResolver.js +0 -324
  23. package/lib/AntiBan/messageQueue.js +0 -187
  24. package/lib/AntiBan/messageRecovery.js +0 -283
  25. package/lib/AntiBan/messageTypeRegistry.js +0 -369
  26. package/lib/AntiBan/observability.js +0 -145
  27. package/lib/AntiBan/patch.js +0 -302
  28. package/lib/AntiBan/persist.js +0 -87
  29. package/lib/AntiBan/presenceChoreographer.js +0 -430
  30. package/lib/AntiBan/presets.js +0 -105
  31. package/lib/AntiBan/profiles.js +0 -31
  32. package/lib/AntiBan/proxyRotator.js +0 -306
  33. package/lib/AntiBan/rateLimiter.js +0 -261
  34. package/lib/AntiBan/readReceiptVariance.js +0 -88
  35. package/lib/AntiBan/reconnectThrottle.js +0 -180
  36. package/lib/AntiBan/replyRatio.js +0 -161
  37. package/lib/AntiBan/reputationVoucher.js +0 -373
  38. package/lib/AntiBan/retryReason.js +0 -91
  39. package/lib/AntiBan/retryTracker.js +0 -172
  40. package/lib/AntiBan/scheduler.js +0 -111
  41. package/lib/AntiBan/sessionFingerprint.js +0 -248
  42. package/lib/AntiBan/sessionStability.js +0 -330
  43. package/lib/AntiBan/stateAdapter.js +0 -73
  44. package/lib/AntiBan/stateExport.js +0 -152
  45. package/lib/AntiBan/stealthConnect.js +0 -130
  46. package/lib/AntiBan/timelockGuard.js +0 -181
  47. package/lib/AntiBan/topologyThrottler.js +0 -339
  48. package/lib/AntiBan/warmup.js +0 -140
  49. package/lib/AntiBan/webhooks.js +0 -80
  50. package/lib/AntiBan/wrapper.js +0 -456
@@ -1,284 +0,0 @@
1
- /**
2
- * BanRecoveryOrchestrator — Structured recovery after ban/restriction events
3
- *
4
- * When WhatsApp restricts your account, the worst thing to do is immediately
5
- * resume normal activity. This module provides a evidence-based recovery protocol:
6
- *
7
- * - Timelocked (reachout_restricted): 24h pause, resume at 10% rate, ramp 15%/week
8
- * - Rate-overlimit (429): 4h pause, resume at 25% rate, ramp 25%/week
9
- * - Soft-ban (repeated disconnects): 48h pause, resume at 5% rate, ramp 10%/week
10
- * - Hard-ban (loggedOut): account is dead, signal for replacement
11
- *
12
- * Based on observed recovery times from community reports. Not guaranteed —
13
- * WA's enforcement is non-deterministic. Treat as best-effort guidance.
14
- */
15
- const DEFAULT_PLANS = {
16
- timelock: {
17
- eventType: 'timelock',
18
- pauseDurationMs: 24 * 60 * 60 * 1000, // 24 hours
19
- resumeRateMultiplier: 0.10, // 10% of normal
20
- weeklyRampPercent: 15, // +15% per week
21
- estimatedRecoveryDays: 14,
22
- description: 'WA reachout timelock — 24h pause then slow ramp',
23
- },
24
- rate_overlimit: {
25
- eventType: 'rate_overlimit',
26
- pauseDurationMs: 4 * 60 * 60 * 1000, // 4 hours
27
- resumeRateMultiplier: 0.25, // 25% of normal
28
- weeklyRampPercent: 25, // +25% per week
29
- estimatedRecoveryDays: 7,
30
- description: 'Rate limit hit — 4h pause then moderate ramp',
31
- },
32
- soft_ban: {
33
- eventType: 'soft_ban',
34
- pauseDurationMs: 48 * 60 * 60 * 1000, // 48 hours
35
- resumeRateMultiplier: 0.05, // 5% of normal
36
- weeklyRampPercent: 10, // +10% per week
37
- estimatedRecoveryDays: 21,
38
- description: 'Soft ban detected — 48h pause then very slow ramp',
39
- },
40
- hard_ban: {
41
- eventType: 'hard_ban',
42
- pauseDurationMs: Infinity,
43
- resumeRateMultiplier: 0,
44
- weeklyRampPercent: 0,
45
- estimatedRecoveryDays: Infinity,
46
- description: 'Hard ban — number is dead, replace SIM',
47
- },
48
- };
49
- export class BanRecoveryOrchestrator {
50
- config;
51
- state;
52
- plans;
53
- constructor(config = {}) {
54
- this.config = {
55
- plans: config.plans || {},
56
- onPhaseChange: config.onPhaseChange || (() => { }),
57
- onHardBan: config.onHardBan || (() => { }),
58
- maxRecoveryWeeks: config.maxRecoveryWeeks ?? 8,
59
- };
60
- // Merge custom plans with defaults
61
- this.plans = { ...DEFAULT_PLANS };
62
- if (config.plans) {
63
- for (const eventType in config.plans) {
64
- const customPlan = config.plans[eventType];
65
- if (customPlan) {
66
- this.plans[eventType] = {
67
- ...this.plans[eventType],
68
- ...customPlan,
69
- };
70
- }
71
- }
72
- }
73
- this.state = {
74
- active: false,
75
- phase: 'graduated',
76
- currentRateMultiplier: 1.0,
77
- weeksSinceResume: 0,
78
- banCount30d: 0,
79
- };
80
- }
81
- /**
82
- * Record a ban event and start recovery protocol
83
- */
84
- recordBanEvent(eventType) {
85
- const now = Date.now();
86
- const plan = this.plans[eventType];
87
- // Update ban count (reset if >30 days since last ban)
88
- if (this.state.lastBanAt) {
89
- const daysSinceLastBan = (now - this.state.lastBanAt) / (86400000);
90
- if (daysSinceLastBan > 30) {
91
- this.state.banCount30d = 0;
92
- }
93
- }
94
- this.state.banCount30d++;
95
- this.state.lastBanAt = now;
96
- // Upgrade to hard_ban if 3+ bans in 30 days (unless already hard_ban)
97
- if (this.state.banCount30d >= 3 && eventType !== 'hard_ban') {
98
- eventType = 'hard_ban';
99
- }
100
- this.state.active = true;
101
- this.state.eventType = eventType;
102
- this.state.banDetectedAt = now;
103
- this.state.pauseUntil = plan.pauseDurationMs === Infinity ? Infinity : now + plan.pauseDurationMs;
104
- this.state.currentRateMultiplier = plan.resumeRateMultiplier;
105
- this.state.weeksSinceResume = 0;
106
- this.state.phase = eventType === 'hard_ban' ? 'dead' : 'paused';
107
- this.config.onPhaseChange(this.state.phase, plan);
108
- if (eventType === 'hard_ban') {
109
- this.config.onHardBan();
110
- }
111
- return this.getStatus();
112
- }
113
- /**
114
- * Get current recovery status (call before sending to check rate)
115
- */
116
- getStatus() {
117
- if (!this.state.active || !this.state.eventType) {
118
- return {
119
- phase: 'graduated',
120
- rateMultiplier: 1.0,
121
- recommendation: 'No active recovery — operating normally',
122
- shouldReplaceNumber: false,
123
- };
124
- }
125
- const now = Date.now();
126
- const plan = this.plans[this.state.eventType];
127
- // Check if pause period has ended
128
- if (this.state.phase === 'paused' && this.state.pauseUntil !== undefined && this.state.pauseUntil !== Infinity && now >= this.state.pauseUntil) {
129
- this.state.phase = 'recovering';
130
- this.config.onPhaseChange(this.state.phase, plan);
131
- }
132
- // Check if we've exceeded max recovery weeks (give up)
133
- if (this.state.phase === 'recovering' || this.state.phase === 'ramping') {
134
- if (this.state.weeksSinceResume >= this.config.maxRecoveryWeeks) {
135
- this.state.phase = 'dead';
136
- this.config.onPhaseChange(this.state.phase, plan);
137
- this.config.onHardBan();
138
- }
139
- }
140
- const shouldReplaceNumber = this.state.phase === 'dead' || this.state.banCount30d >= 3;
141
- let recommendation;
142
- switch (this.state.phase) {
143
- case 'dead':
144
- recommendation = 'Account is permanently restricted. Replace number and start fresh.';
145
- break;
146
- case 'paused':
147
- if (this.state.pauseUntil === Infinity) {
148
- recommendation = 'Account is dead. Do not attempt to send messages.';
149
- }
150
- else {
151
- const remainingMs = this.state.pauseUntil - now;
152
- const remainingHours = Math.ceil(remainingMs / 3600000);
153
- recommendation = `Pause period active. Wait ${remainingHours}h before resuming. ${plan.description}`;
154
- }
155
- break;
156
- case 'recovering':
157
- recommendation = `Recovery phase. Operating at ${Math.round(this.state.currentRateMultiplier * 100)}% capacity. Ramp: ${plan.weeklyRampPercent}%/week.`;
158
- break;
159
- case 'ramping':
160
- recommendation = `Ramping phase. Operating at ${Math.round(this.state.currentRateMultiplier * 100)}% capacity. Ramp: ${plan.weeklyRampPercent}%/week.`;
161
- break;
162
- case 'graduated':
163
- recommendation = 'Recovery complete. Operating at full capacity.';
164
- break;
165
- }
166
- const pauseRemainingMs = this.state.pauseUntil !== undefined && this.state.pauseUntil !== Infinity && now < this.state.pauseUntil
167
- ? this.state.pauseUntil - now
168
- : undefined;
169
- let estimatedFullRecoveryDate;
170
- if (this.state.phase === 'recovering' || this.state.phase === 'ramping') {
171
- const rateToGain = 1.0 - this.state.currentRateMultiplier;
172
- const weeksNeeded = Math.ceil((rateToGain / (plan.weeklyRampPercent / 100)) * (1 / this.state.currentRateMultiplier));
173
- estimatedFullRecoveryDate = now + weeksNeeded * 7 * 86400000;
174
- }
175
- return {
176
- phase: this.state.phase,
177
- rateMultiplier: this.state.currentRateMultiplier,
178
- pauseRemainingMs,
179
- estimatedFullRecoveryDate,
180
- recommendation,
181
- shouldReplaceNumber,
182
- };
183
- }
184
- /**
185
- * Current rate multiplier — multiply your normal limits by this
186
- */
187
- getRateMultiplier() {
188
- return this.state.currentRateMultiplier;
189
- }
190
- /**
191
- * Should be called daily/weekly to advance the ramp
192
- */
193
- tick() {
194
- if (!this.state.active || !this.state.eventType) {
195
- return;
196
- }
197
- const now = Date.now();
198
- const plan = this.plans[this.state.eventType];
199
- // Transition from paused to recovering
200
- if (this.state.phase === 'paused' && this.state.pauseUntil !== undefined && this.state.pauseUntil !== Infinity && now >= this.state.pauseUntil) {
201
- this.state.phase = 'recovering';
202
- this.config.onPhaseChange(this.state.phase, plan);
203
- }
204
- // Advance the ramp if in recovering/ramping phase
205
- if (this.state.phase === 'recovering' || this.state.phase === 'ramping') {
206
- this.state.weeksSinceResume++;
207
- // Check if exceeded max recovery weeks
208
- if (this.state.weeksSinceResume >= this.config.maxRecoveryWeeks) {
209
- this.state.phase = 'dead';
210
- this.state.currentRateMultiplier = 0;
211
- this.config.onPhaseChange(this.state.phase, plan);
212
- this.config.onHardBan();
213
- return;
214
- }
215
- // Apply weekly ramp
216
- const rampMultiplier = 1 + (plan.weeklyRampPercent / 100);
217
- const newMultiplier = this.state.currentRateMultiplier * rampMultiplier;
218
- if (newMultiplier >= 1.0) {
219
- // Graduated
220
- this.state.currentRateMultiplier = 1.0;
221
- this.state.phase = 'graduated';
222
- this.state.active = false;
223
- this.config.onPhaseChange(this.state.phase, plan);
224
- }
225
- else {
226
- // Still ramping
227
- this.state.currentRateMultiplier = newMultiplier;
228
- this.state.phase = 'ramping';
229
- }
230
- }
231
- }
232
- /**
233
- * Classify a raw error into a BanEventType
234
- */
235
- static classifyError(err) {
236
- if (!err)
237
- return null;
238
- const errorStr = String(err).toLowerCase();
239
- const errorMsg = err.message?.toLowerCase() || '';
240
- const errorCode = err.code || '';
241
- // Timelock patterns
242
- if (errorStr.includes('reachout') ||
243
- errorStr.includes('account_reachout_restricted') ||
244
- errorMsg.includes('reachout') ||
245
- errorCode === 463 ||
246
- errorCode === '463') {
247
- return 'timelock';
248
- }
249
- // Rate overlimit patterns
250
- if (errorStr.includes('rate-overlimit') ||
251
- errorStr.includes('rate_overlimit') ||
252
- errorStr.includes('429') ||
253
- errorCode === 429 ||
254
- errorCode === '429') {
255
- return 'rate_overlimit';
256
- }
257
- // Hard ban patterns
258
- if (errorStr.includes('loggedout') ||
259
- errorStr.includes('logged_out') ||
260
- errorStr.includes('logged out') ||
261
- errorMsg.includes('loggedout') ||
262
- errorCode === 401 ||
263
- errorCode === '401') {
264
- return 'hard_ban';
265
- }
266
- // Note: soft_ban detection requires context (multiple disconnects)
267
- // and should be determined by the caller using HealthMonitor
268
- return null;
269
- }
270
- /**
271
- * Serializable state for persistence
272
- */
273
- getState() {
274
- return { ...this.state };
275
- }
276
- /**
277
- * Restore from persisted state
278
- */
279
- static fromState(state, config) {
280
- const orchestrator = new BanRecoveryOrchestrator(config);
281
- orchestrator.state = { ...state };
282
- return orchestrator;
283
- }
284
- }
@@ -1,177 +0,0 @@
1
- #!/usr/bin/env node
2
- /**
3
- * baileys-antiban CLI
4
- * Usage: npx baileys-antiban <command> [options]
5
- */
6
- import * as fs from 'fs';
7
- import { StateManager } from './persist.js';
8
- import { resolveConfig } from './presets.js';
9
- import { applyPatch, unpatchFile } from './patch.js';
10
- const args = process.argv.slice(2);
11
- const command = args[0];
12
- function parseArgs(argv) {
13
- const result = {};
14
- for (let i = 1; i < argv.length; i++) {
15
- const arg = argv[i];
16
- if (arg.startsWith('--')) {
17
- const key = arg.slice(2);
18
- const next = argv[i + 1];
19
- if (next && !next.startsWith('--')) {
20
- result[key] = next;
21
- i++;
22
- }
23
- else {
24
- result[key] = true;
25
- }
26
- }
27
- }
28
- return result;
29
- }
30
- function cmdStatus(opts) {
31
- const statePath = opts['state'];
32
- let warmupInfo = 'No state file (in-memory mode)';
33
- let savedAt = 'N/A';
34
- if (statePath) {
35
- const mgr = new StateManager(statePath);
36
- const state = mgr.load();
37
- if (state) {
38
- const now = Date.now();
39
- const dayMs = 86400000;
40
- const currentDay = Math.floor((now - state.warmup.startedAt) / dayMs);
41
- warmupInfo = state.warmup.graduated
42
- ? 'Graduated (warmup complete)'
43
- : `Day ${currentDay + 1}, sent today: ${state.warmup.dailyCounts[currentDay] || 0}`;
44
- savedAt = new Date(state.savedAt).toISOString();
45
- }
46
- else {
47
- warmupInfo = 'State file missing or corrupt';
48
- }
49
- }
50
- const output = {
51
- warmup: warmupInfo,
52
- savedAt,
53
- statePath: statePath || null,
54
- };
55
- if (opts['json']) {
56
- console.log(JSON.stringify(output, null, 2));
57
- }
58
- else {
59
- console.log('═══ baileys-antiban status ═══');
60
- console.log(`Warmup: ${output.warmup}`);
61
- console.log(`Saved: ${output.savedAt}`);
62
- console.log(`State: ${output.statePath || 'none'}`);
63
- }
64
- }
65
- function cmdReset(opts) {
66
- const statePath = opts['state'];
67
- if (!statePath) {
68
- console.error('Error: --state <path> required for reset');
69
- process.exit(1);
70
- }
71
- if (!fs.existsSync(statePath)) {
72
- console.log('State file does not exist — nothing to reset');
73
- return;
74
- }
75
- fs.unlinkSync(statePath);
76
- console.log(`✅ State file deleted: ${statePath}`);
77
- }
78
- function cmdWarmupSimulate(opts) {
79
- const days = parseInt(opts['simulate'] || '7', 10);
80
- const presetName = opts['preset'] || 'conservative';
81
- const cfg = resolveConfig(presetName);
82
- console.log(`\nWarmup simulation — preset: ${presetName}, days: ${days}`);
83
- console.log('─'.repeat(50));
84
- const startDate = new Date();
85
- for (let day = 0; day < days; day++) {
86
- const date = new Date(startDate);
87
- date.setDate(date.getDate() + day);
88
- const dayName = date.toLocaleDateString('en-US', { weekday: 'short', month: 'short', day: 'numeric' });
89
- const limit = Math.round(cfg.day1Limit * Math.pow(cfg.growthFactor, day));
90
- const bar = '█'.repeat(Math.min(30, Math.round(limit / 10)));
91
- console.log(`Day ${String(day + 1).padStart(2)} ${dayName.padEnd(15)} ${String(limit).padStart(5)} msgs/day ${bar}`);
92
- }
93
- console.log('─'.repeat(50));
94
- console.log(`Day ${days + 1}+: graduated (unlimited by warmup)\n`);
95
- }
96
- function cmdPatch(opts) {
97
- const searchPaths = opts['path'] ? [opts['path']] : undefined;
98
- const preset = opts['preset'] || 'conservative';
99
- const minDelay = opts['min-delay'] ? parseInt(opts['min-delay']) : 1500;
100
- const maxDelay = opts['max-delay'] ? parseInt(opts['max-delay']) : 4000;
101
- const typingIndicator = opts['no-typing'] ? false : true;
102
- const dryRun = !!opts['dry-run'];
103
- const force = !!opts['force'];
104
- const result = applyPatch({
105
- baileysPaths: searchPaths,
106
- preset,
107
- minDelay,
108
- maxDelay,
109
- typingIndicator,
110
- dryRun,
111
- });
112
- if (result.alreadyPatched && !force) {
113
- console.log(result.message);
114
- process.exit(0);
115
- }
116
- if (!result.success) {
117
- console.error('✗ ' + result.message);
118
- process.exit(1);
119
- }
120
- console.log(result.message);
121
- }
122
- function cmdUnpatch(opts) {
123
- const targetFile = opts['file'];
124
- if (!targetFile) {
125
- console.error('Usage: npx baileys-antiban unpatch --file <path-to-patched-file>');
126
- process.exit(1);
127
- }
128
- const result = unpatchFile(targetFile);
129
- if (!result.success) {
130
- console.error('✗ ' + result.message);
131
- process.exit(1);
132
- }
133
- console.log(result.message);
134
- }
135
- // Main
136
- const opts = parseArgs(args);
137
- switch (command) {
138
- case 'status':
139
- cmdStatus(opts);
140
- break;
141
- case 'reset':
142
- cmdReset(opts);
143
- break;
144
- case 'warmup':
145
- if (opts['simulate']) {
146
- cmdWarmupSimulate(opts);
147
- }
148
- else {
149
- console.error('Usage: npx baileys-antiban warmup --simulate <days> [--preset conservative|moderate|aggressive]');
150
- process.exit(1);
151
- }
152
- break;
153
- case 'patch':
154
- cmdPatch(opts);
155
- break;
156
- case 'unpatch':
157
- cmdUnpatch(opts);
158
- break;
159
- default:
160
- console.log('baileys-antiban v3.0');
161
- console.log('');
162
- console.log('Commands:');
163
- console.log(' status [--state <path>] [--json] Show warmup and health status');
164
- console.log(' reset --state <path> Delete state file');
165
- console.log(' warmup --simulate <days> [--preset] Show warmup schedule');
166
- console.log(' patch [--path <baileys-dir>] [--preset] [--min-delay] [--max-delay] [--no-typing] [--dry-run]');
167
- console.log(' unpatch --file <patched-file> Restore file from backup');
168
- console.log('');
169
- console.log('Examples:');
170
- console.log(' npx baileys-antiban status --state ./antiban-state.json');
171
- console.log(' npx baileys-antiban warmup --simulate 7 --preset moderate');
172
- console.log(' npx baileys-antiban reset --state ./antiban-state.json');
173
- console.log(' npx baileys-antiban patch');
174
- console.log(' npx baileys-antiban patch --path ./node_modules/@openclaw/whatsapp/node_modules/baileys');
175
- console.log(' npx baileys-antiban patch --preset moderate --min-delay 1000 --max-delay 3000');
176
- console.log(' npx baileys-antiban unpatch --file ./node_modules/baileys/lib/socket/index.js');
177
- }
@@ -1,236 +0,0 @@
1
- /**
2
- * Contact Graph Warmer — Requires 1:1 handshake before group/bulk sends
3
- *
4
- * WhatsApp's ML models weight "social graph distance" heavily. Accounts that
5
- * message strangers (contacts who never replied) have higher ban risk.
6
- *
7
- * This module:
8
- * - Tracks contact state: stranger → handshake_sent → handshake_complete → known
9
- * - Blocks sends to strangers unless handshake completed
10
- * - Enforces group lurk period (don't send immediately after joining)
11
- * - Caps daily new-contact messaging (prevent spray-and-pray patterns)
12
- * - Auto-registers inbound senders as "known contacts"
13
- *
14
- * Research: 2025 ban waves correlated with accounts joining groups + spamming
15
- * instantly. 12-24h lurk period significantly reduced bans.
16
- */
17
- const DEFAULT_CONFIG = {
18
- enabled: false,
19
- requireHandshakeBeforeGroupSend: true,
20
- handshakeMinDelayMs: 3600000, // 1 hour
21
- groupLurkPeriodMs: 43200000, // 12 hours
22
- maxStrangerMessagesPerDay: 5,
23
- autoRegisterOnIncoming: true,
24
- };
25
- export class ContactGraphWarmer {
26
- config;
27
- contacts = new Map();
28
- groups = new Map();
29
- strangerMessagesToday = 0;
30
- lastStrangerResetDay = this.getCurrentDay();
31
- constructor(config = {}) {
32
- this.config = { ...DEFAULT_CONFIG, ...config };
33
- }
34
- /**
35
- * Check if message can be sent to this contact/group.
36
- * Returns { allowed: false, needsHandshake: true } if handshake required.
37
- */
38
- canMessage(jid) {
39
- if (!this.config.enabled) {
40
- return { allowed: true };
41
- }
42
- // Reset daily stranger counter at UTC midnight
43
- const currentDay = this.getCurrentDay();
44
- if (currentDay !== this.lastStrangerResetDay) {
45
- this.strangerMessagesToday = 0;
46
- this.lastStrangerResetDay = currentDay;
47
- }
48
- // Handle groups
49
- if (this.isGroup(jid)) {
50
- return this.checkGroupMessage(jid);
51
- }
52
- // Handle individual contacts
53
- return this.checkIndividualMessage(jid);
54
- }
55
- /**
56
- * Mark handshake as sent to this contact.
57
- */
58
- markHandshakeSent(jid) {
59
- if (!this.config.enabled)
60
- return;
61
- if (this.isGroup(jid))
62
- return;
63
- const record = this.contacts.get(jid) || { state: 'stranger' };
64
- record.state = 'handshake_sent';
65
- record.handshakeSentAt = Date.now();
66
- this.contacts.set(jid, record);
67
- }
68
- /**
69
- * Mark handshake as complete with this contact.
70
- */
71
- markHandshakeComplete(jid) {
72
- if (!this.config.enabled)
73
- return;
74
- if (this.isGroup(jid))
75
- return;
76
- const record = this.contacts.get(jid) || { state: 'stranger' };
77
- record.state = 'handshake_complete';
78
- this.contacts.set(jid, record);
79
- }
80
- /**
81
- * Register a contact as known (skip handshake requirement).
82
- */
83
- registerKnownContact(jid) {
84
- if (!this.config.enabled)
85
- return;
86
- if (this.isGroup(jid))
87
- return;
88
- const record = this.contacts.get(jid) || { state: 'stranger' };
89
- record.state = 'known';
90
- this.contacts.set(jid, record);
91
- }
92
- /**
93
- * Register a group join event.
94
- */
95
- registerGroupJoin(groupJid) {
96
- if (!this.config.enabled)
97
- return;
98
- if (!this.isGroup(groupJid))
99
- return;
100
- this.groups.set(groupJid, { joinedAt: Date.now() });
101
- }
102
- /**
103
- * Get contact state.
104
- */
105
- getContactState(jid) {
106
- if (this.isGroup(jid))
107
- return 'known'; // Groups don't have handshake states
108
- return this.contacts.get(jid)?.state || 'stranger';
109
- }
110
- /**
111
- * Handle incoming message — auto-register if enabled.
112
- */
113
- onIncomingMessage(jid) {
114
- if (!this.config.enabled)
115
- return;
116
- if (this.isGroup(jid))
117
- return;
118
- if (this.config.autoRegisterOnIncoming) {
119
- this.registerKnownContact(jid);
120
- }
121
- }
122
- /**
123
- * Get statistics.
124
- */
125
- getStats() {
126
- const knownContacts = Array.from(this.contacts.values()).filter(c => c.state === 'known').length;
127
- const pendingHandshakes = Array.from(this.contacts.values()).filter(c => c.state === 'handshake_sent').length;
128
- const groupsJoined = Array.from(this.groups.entries()).map(([groupJid, record]) => ({
129
- groupJid,
130
- joinedAt: record.joinedAt,
131
- firstSendUnlocksAt: record.joinedAt + this.config.groupLurkPeriodMs,
132
- }));
133
- return {
134
- knownContacts,
135
- pendingHandshakes,
136
- strangersToday: this.strangerMessagesToday,
137
- groupsJoined,
138
- };
139
- }
140
- /**
141
- * Reset all state.
142
- */
143
- reset() {
144
- this.contacts.clear();
145
- this.groups.clear();
146
- this.strangerMessagesToday = 0;
147
- this.lastStrangerResetDay = this.getCurrentDay();
148
- }
149
- /**
150
- * Export state for persistence.
151
- */
152
- exportState() {
153
- return {
154
- contacts: Array.from(this.contacts.entries()),
155
- groups: Array.from(this.groups.entries()),
156
- strangerMessagesToday: this.strangerMessagesToday,
157
- lastStrangerResetDay: this.lastStrangerResetDay,
158
- };
159
- }
160
- /**
161
- * Restore state from persistence.
162
- */
163
- restoreState(state) {
164
- if (state?.contacts && Array.isArray(state.contacts)) {
165
- this.contacts = new Map(state.contacts);
166
- }
167
- if (state?.groups && Array.isArray(state.groups)) {
168
- this.groups = new Map(state.groups);
169
- }
170
- if (typeof state?.strangerMessagesToday === 'number') {
171
- this.strangerMessagesToday = state.strangerMessagesToday;
172
- }
173
- if (typeof state?.lastStrangerResetDay === 'number') {
174
- this.lastStrangerResetDay = state.lastStrangerResetDay;
175
- }
176
- }
177
- // Private helpers
178
- isGroup(jid) {
179
- return jid.endsWith('@g.us');
180
- }
181
- getCurrentDay() {
182
- return Math.floor(Date.now() / 86400000);
183
- }
184
- checkGroupMessage(groupJid) {
185
- const record = this.groups.get(groupJid);
186
- if (!record) {
187
- // Group not registered — allow (assume old membership)
188
- return { allowed: true };
189
- }
190
- const lurkEndsAt = record.joinedAt + this.config.groupLurkPeriodMs;
191
- if (Date.now() < lurkEndsAt) {
192
- const minutesLeft = Math.ceil((lurkEndsAt - Date.now()) / 60000);
193
- return {
194
- allowed: false,
195
- reason: `Group lurk period not elapsed — wait ${minutesLeft} minutes`,
196
- };
197
- }
198
- return { allowed: true };
199
- }
200
- checkIndividualMessage(jid) {
201
- const record = this.contacts.get(jid);
202
- // Unknown contact (stranger)
203
- if (!record || record.state === 'stranger') {
204
- if (this.config.requireHandshakeBeforeGroupSend) {
205
- // Check daily stranger quota
206
- if (this.strangerMessagesToday >= this.config.maxStrangerMessagesPerDay) {
207
- return {
208
- allowed: false,
209
- reason: `Daily new-contact limit reached (${this.config.maxStrangerMessagesPerDay})`,
210
- needsHandshake: true,
211
- };
212
- }
213
- // Allow but increment counter
214
- this.strangerMessagesToday++;
215
- }
216
- return { allowed: true, needsHandshake: true };
217
- }
218
- // Handshake sent — check delay
219
- if (record.state === 'handshake_sent') {
220
- if (!record.handshakeSentAt) {
221
- // No timestamp — shouldn't happen, but allow
222
- return { allowed: true };
223
- }
224
- const elapsed = Date.now() - record.handshakeSentAt;
225
- if (elapsed < this.config.handshakeMinDelayMs) {
226
- const minutesLeft = Math.ceil((this.config.handshakeMinDelayMs - elapsed) / 60000);
227
- return {
228
- allowed: false,
229
- reason: `Handshake too recent — wait ${minutesLeft} minutes`,
230
- };
231
- }
232
- }
233
- // handshake_complete or known — allow
234
- return { allowed: true };
235
- }
236
- }