baileys-antiban 4.7.0 → 4.8.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/dist/antiban.d.ts +19 -0
- package/dist/antiban.js +49 -0
- package/dist/cjs/antiban.js +49 -0
- package/dist/cjs/index.js +7 -1
- package/dist/cjs/messageTypeRegistry.js +373 -0
- package/dist/cjs/stateExport.js +153 -0
- package/dist/cjs/warmup.js +6 -0
- package/dist/index.d.ts +2 -0
- package/dist/index.js +3 -0
- package/dist/messageTypeRegistry.d.ts +145 -0
- package/dist/messageTypeRegistry.js +369 -0
- package/dist/stateExport.d.ts +145 -0
- package/dist/stateExport.js +149 -0
- package/dist/warmup.d.ts +4 -0
- package/dist/warmup.js +6 -0
- package/package.json +1 -1
package/dist/antiban.d.ts
CHANGED
|
@@ -29,6 +29,8 @@ import { BanRecoveryOrchestrator, type RecoveryStatus } from './banRecoveryOrche
|
|
|
29
29
|
import { type AntiBanInput, type ResolvedConfig } from './presets.js';
|
|
30
30
|
import { type DeliveryTrackerStats } from './deliveryTracker.js';
|
|
31
31
|
import { type InstanceCoordinatorStats } from './instanceCoordinator.js';
|
|
32
|
+
import { MessageTypeRegistry } from './messageTypeRegistry.js';
|
|
33
|
+
import { type AntibanSnapshot } from './stateExport.js';
|
|
32
34
|
export interface AntiBanConfigLegacy {
|
|
33
35
|
rateLimiter?: Partial<RateLimiterConfig>;
|
|
34
36
|
warmUp?: Partial<WarmUpConfig>;
|
|
@@ -76,6 +78,10 @@ export interface AntiBanStats {
|
|
|
76
78
|
banRecovery?: RecoveryStatus | null;
|
|
77
79
|
deliveryTracker: DeliveryTrackerStats;
|
|
78
80
|
instanceCoordinator?: InstanceCoordinatorStats | null;
|
|
81
|
+
messageRegistry?: {
|
|
82
|
+
typeCount: number;
|
|
83
|
+
warningCount: number;
|
|
84
|
+
} | null;
|
|
79
85
|
}
|
|
80
86
|
export declare class AntiBan {
|
|
81
87
|
private rateLimiter;
|
|
@@ -93,6 +99,7 @@ export declare class AntiBan {
|
|
|
93
99
|
private banRecovery;
|
|
94
100
|
private deliveryTracker;
|
|
95
101
|
private instanceCoordinator;
|
|
102
|
+
private messageTypeRegistry;
|
|
96
103
|
private stateManager;
|
|
97
104
|
private resolvedConfig;
|
|
98
105
|
private logging;
|
|
@@ -161,6 +168,8 @@ export declare class AntiBan {
|
|
|
161
168
|
get sessionStability(): SessionHealthMonitor | null;
|
|
162
169
|
/** Get the ban recovery orchestrator for direct access */
|
|
163
170
|
get recoveryOrchestrator(): BanRecoveryOrchestrator;
|
|
171
|
+
/** Get the message type registry for direct access */
|
|
172
|
+
get messageRegistry(): MessageTypeRegistry | null;
|
|
164
173
|
/**
|
|
165
174
|
* Export warm-up state for persistence between restarts
|
|
166
175
|
*/
|
|
@@ -180,6 +189,16 @@ export declare class AntiBan {
|
|
|
180
189
|
private runAdaptiveCheck;
|
|
181
190
|
private persistStateDebounced;
|
|
182
191
|
private persistStateImmediate;
|
|
192
|
+
/**
|
|
193
|
+
* Export unified state snapshot for Redis failover or cross-instance migration.
|
|
194
|
+
* Returns snapshot of all module states (warmup, health, rate limiter, circuits, etc.)
|
|
195
|
+
*/
|
|
196
|
+
exportState(): AntibanSnapshot;
|
|
197
|
+
/**
|
|
198
|
+
* Import unified state snapshot.
|
|
199
|
+
* CRDT-safe for rate limiters (never overwrites higher counts).
|
|
200
|
+
*/
|
|
201
|
+
importState(snapshot: AntibanSnapshot): void;
|
|
183
202
|
/**
|
|
184
203
|
* Clean up all timers and resources.
|
|
185
204
|
* Call this when disposing of the AntiBan instance or when the socket closes.
|
package/dist/antiban.js
CHANGED
|
@@ -31,6 +31,8 @@ import { StateManager } from './persist.js';
|
|
|
31
31
|
import { shouldUseGroupProfile, applyGroupMultiplier } from './profiles.js';
|
|
32
32
|
import { DeliveryTracker } from './deliveryTracker.js';
|
|
33
33
|
import { InstanceCoordinator } from './instanceCoordinator.js';
|
|
34
|
+
import { MessageTypeRegistry } from './messageTypeRegistry.js';
|
|
35
|
+
import { exportAntibanState, importAntibanState } from './stateExport.js';
|
|
34
36
|
function isLegacyConfig(cfg) {
|
|
35
37
|
if (typeof cfg !== 'object' || cfg === null)
|
|
36
38
|
return false;
|
|
@@ -109,6 +111,7 @@ export class AntiBan {
|
|
|
109
111
|
banRecovery;
|
|
110
112
|
deliveryTracker;
|
|
111
113
|
instanceCoordinator = null;
|
|
114
|
+
messageTypeRegistry = null;
|
|
112
115
|
stateManager = null;
|
|
113
116
|
resolvedConfig;
|
|
114
117
|
logging;
|
|
@@ -297,6 +300,13 @@ export class AntiBan {
|
|
|
297
300
|
console.log(`[baileys-antiban] 🌐 Instance coordination enabled: ${cfg.instanceCoordinator}`);
|
|
298
301
|
}
|
|
299
302
|
}
|
|
303
|
+
// Initialize message type registry if configured
|
|
304
|
+
if (cfg.messageTypeRegistry) {
|
|
305
|
+
this.messageTypeRegistry = new MessageTypeRegistry();
|
|
306
|
+
if (this.logging) {
|
|
307
|
+
console.log(`[baileys-antiban] 📝 Message type registry enabled`);
|
|
308
|
+
}
|
|
309
|
+
}
|
|
300
310
|
}
|
|
301
311
|
/**
|
|
302
312
|
* Check if a message can be sent and get required delay.
|
|
@@ -597,6 +607,13 @@ export class AntiBan {
|
|
|
597
607
|
if (this.instanceCoordinator) {
|
|
598
608
|
stats.instanceCoordinator = this.instanceCoordinator.getStats();
|
|
599
609
|
}
|
|
610
|
+
if (this.messageTypeRegistry) {
|
|
611
|
+
const warnings = this.messageTypeRegistry.getWarnings();
|
|
612
|
+
stats.messageRegistry = {
|
|
613
|
+
typeCount: Array.from(this.messageTypeRegistry.types.keys()).length,
|
|
614
|
+
warningCount: warnings.length,
|
|
615
|
+
};
|
|
616
|
+
}
|
|
600
617
|
return stats;
|
|
601
618
|
}
|
|
602
619
|
/** Get the timelock guard for direct access */
|
|
@@ -639,6 +656,10 @@ export class AntiBan {
|
|
|
639
656
|
get recoveryOrchestrator() {
|
|
640
657
|
return this.banRecovery;
|
|
641
658
|
}
|
|
659
|
+
/** Get the message type registry for direct access */
|
|
660
|
+
get messageRegistry() {
|
|
661
|
+
return this.messageTypeRegistry;
|
|
662
|
+
}
|
|
642
663
|
/**
|
|
643
664
|
* Export warm-up state for persistence between restarts
|
|
644
665
|
*/
|
|
@@ -731,6 +752,33 @@ export class AntiBan {
|
|
|
731
752
|
};
|
|
732
753
|
this.stateManager.saveImmediate(state);
|
|
733
754
|
}
|
|
755
|
+
/**
|
|
756
|
+
* Export unified state snapshot for Redis failover or cross-instance migration.
|
|
757
|
+
* Returns snapshot of all module states (warmup, health, rate limiter, circuits, etc.)
|
|
758
|
+
*/
|
|
759
|
+
exportState() {
|
|
760
|
+
return exportAntibanState({
|
|
761
|
+
warmup: this.warmUp,
|
|
762
|
+
health: this.health,
|
|
763
|
+
rateLimiter: this.rateLimiter,
|
|
764
|
+
timelockGuard: this.timelockGuard,
|
|
765
|
+
messageRegistry: this.messageTypeRegistry || undefined,
|
|
766
|
+
instanceId: this.resolvedConfig.instanceId,
|
|
767
|
+
});
|
|
768
|
+
}
|
|
769
|
+
/**
|
|
770
|
+
* Import unified state snapshot.
|
|
771
|
+
* CRDT-safe for rate limiters (never overwrites higher counts).
|
|
772
|
+
*/
|
|
773
|
+
importState(snapshot) {
|
|
774
|
+
importAntibanState(snapshot, {
|
|
775
|
+
warmup: this.warmUp,
|
|
776
|
+
health: this.health,
|
|
777
|
+
rateLimiter: this.rateLimiter,
|
|
778
|
+
timelockGuard: this.timelockGuard,
|
|
779
|
+
messageRegistry: this.messageTypeRegistry || undefined,
|
|
780
|
+
});
|
|
781
|
+
}
|
|
734
782
|
/**
|
|
735
783
|
* Clean up all timers and resources.
|
|
736
784
|
* Call this when disposing of the AntiBan instance or when the socket closes.
|
|
@@ -746,6 +794,7 @@ export class AntiBan {
|
|
|
746
794
|
this.jidCanonicalizerModule?.destroy();
|
|
747
795
|
this.lidResolverModule?.destroy();
|
|
748
796
|
this.sessionStabilityMonitor?.reset();
|
|
797
|
+
this.messageTypeRegistry?.cleanup();
|
|
749
798
|
if (this.logging) {
|
|
750
799
|
console.log('[baileys-antiban] 🧹 Destroyed — all timers cleared');
|
|
751
800
|
}
|
package/dist/cjs/antiban.js
CHANGED
|
@@ -34,6 +34,8 @@ const persist_js_1 = require("./persist.js");
|
|
|
34
34
|
const profiles_js_1 = require("./profiles.js");
|
|
35
35
|
const deliveryTracker_js_1 = require("./deliveryTracker.js");
|
|
36
36
|
const instanceCoordinator_js_1 = require("./instanceCoordinator.js");
|
|
37
|
+
const messageTypeRegistry_js_1 = require("./messageTypeRegistry.js");
|
|
38
|
+
const stateExport_js_1 = require("./stateExport.js");
|
|
37
39
|
function isLegacyConfig(cfg) {
|
|
38
40
|
if (typeof cfg !== 'object' || cfg === null)
|
|
39
41
|
return false;
|
|
@@ -112,6 +114,7 @@ class AntiBan {
|
|
|
112
114
|
banRecovery;
|
|
113
115
|
deliveryTracker;
|
|
114
116
|
instanceCoordinator = null;
|
|
117
|
+
messageTypeRegistry = null;
|
|
115
118
|
stateManager = null;
|
|
116
119
|
resolvedConfig;
|
|
117
120
|
logging;
|
|
@@ -300,6 +303,13 @@ class AntiBan {
|
|
|
300
303
|
console.log(`[baileys-antiban] 🌐 Instance coordination enabled: ${cfg.instanceCoordinator}`);
|
|
301
304
|
}
|
|
302
305
|
}
|
|
306
|
+
// Initialize message type registry if configured
|
|
307
|
+
if (cfg.messageTypeRegistry) {
|
|
308
|
+
this.messageTypeRegistry = new messageTypeRegistry_js_1.MessageTypeRegistry();
|
|
309
|
+
if (this.logging) {
|
|
310
|
+
console.log(`[baileys-antiban] 📝 Message type registry enabled`);
|
|
311
|
+
}
|
|
312
|
+
}
|
|
303
313
|
}
|
|
304
314
|
/**
|
|
305
315
|
* Check if a message can be sent and get required delay.
|
|
@@ -600,6 +610,13 @@ class AntiBan {
|
|
|
600
610
|
if (this.instanceCoordinator) {
|
|
601
611
|
stats.instanceCoordinator = this.instanceCoordinator.getStats();
|
|
602
612
|
}
|
|
613
|
+
if (this.messageTypeRegistry) {
|
|
614
|
+
const warnings = this.messageTypeRegistry.getWarnings();
|
|
615
|
+
stats.messageRegistry = {
|
|
616
|
+
typeCount: Array.from(this.messageTypeRegistry.types.keys()).length,
|
|
617
|
+
warningCount: warnings.length,
|
|
618
|
+
};
|
|
619
|
+
}
|
|
603
620
|
return stats;
|
|
604
621
|
}
|
|
605
622
|
/** Get the timelock guard for direct access */
|
|
@@ -642,6 +659,10 @@ class AntiBan {
|
|
|
642
659
|
get recoveryOrchestrator() {
|
|
643
660
|
return this.banRecovery;
|
|
644
661
|
}
|
|
662
|
+
/** Get the message type registry for direct access */
|
|
663
|
+
get messageRegistry() {
|
|
664
|
+
return this.messageTypeRegistry;
|
|
665
|
+
}
|
|
645
666
|
/**
|
|
646
667
|
* Export warm-up state for persistence between restarts
|
|
647
668
|
*/
|
|
@@ -734,6 +755,33 @@ class AntiBan {
|
|
|
734
755
|
};
|
|
735
756
|
this.stateManager.saveImmediate(state);
|
|
736
757
|
}
|
|
758
|
+
/**
|
|
759
|
+
* Export unified state snapshot for Redis failover or cross-instance migration.
|
|
760
|
+
* Returns snapshot of all module states (warmup, health, rate limiter, circuits, etc.)
|
|
761
|
+
*/
|
|
762
|
+
exportState() {
|
|
763
|
+
return (0, stateExport_js_1.exportAntibanState)({
|
|
764
|
+
warmup: this.warmUp,
|
|
765
|
+
health: this.health,
|
|
766
|
+
rateLimiter: this.rateLimiter,
|
|
767
|
+
timelockGuard: this.timelockGuard,
|
|
768
|
+
messageRegistry: this.messageTypeRegistry || undefined,
|
|
769
|
+
instanceId: this.resolvedConfig.instanceId,
|
|
770
|
+
});
|
|
771
|
+
}
|
|
772
|
+
/**
|
|
773
|
+
* Import unified state snapshot.
|
|
774
|
+
* CRDT-safe for rate limiters (never overwrites higher counts).
|
|
775
|
+
*/
|
|
776
|
+
importState(snapshot) {
|
|
777
|
+
(0, stateExport_js_1.importAntibanState)(snapshot, {
|
|
778
|
+
warmup: this.warmUp,
|
|
779
|
+
health: this.health,
|
|
780
|
+
rateLimiter: this.rateLimiter,
|
|
781
|
+
timelockGuard: this.timelockGuard,
|
|
782
|
+
messageRegistry: this.messageTypeRegistry || undefined,
|
|
783
|
+
});
|
|
784
|
+
}
|
|
737
785
|
/**
|
|
738
786
|
* Clean up all timers and resources.
|
|
739
787
|
* Call this when disposing of the AntiBan instance or when the socket closes.
|
|
@@ -749,6 +797,7 @@ class AntiBan {
|
|
|
749
797
|
this.jidCanonicalizerModule?.destroy();
|
|
750
798
|
this.lidResolverModule?.destroy();
|
|
751
799
|
this.sessionStabilityMonitor?.reset();
|
|
800
|
+
this.messageTypeRegistry?.cleanup();
|
|
752
801
|
if (this.logging) {
|
|
753
802
|
console.log('[baileys-antiban] 🧹 Destroyed — all timers cleared');
|
|
754
803
|
}
|
package/dist/cjs/index.js
CHANGED
|
@@ -10,7 +10,7 @@
|
|
|
10
10
|
*/
|
|
11
11
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
12
12
|
exports.getRetryJitter = exports.getTypingJitter = exports.getMessageSendJitter = exports.applySessionFingerprint = exports.generateSessionFingerprint = exports.proxyRotator = exports.readReceiptVariance = exports.credsSnapshot = exports.applyFingerprint = exports.generateFingerprint = exports.messageRecovery = exports.applyGroupMultiplier = exports.shouldUseGroupProfile = exports.isBroadcast = exports.isNewsletter = exports.isGroup = exports.StateManager = exports.PRESETS = exports.resolveConfig = exports.FileStateAdapter = exports.Scheduler = exports.WebhookAlerts = exports.ContentVariator = exports.MessageQueue = exports.wrapSocketWithFingerprint = exports.wrapSocket = exports.getRetryReasonDescription = exports.isMacError = exports.parseRetryReason = exports.MAC_ERROR_CODES = exports.MessageRetryReason = exports.createLidFirstResolver = exports.LidFirstResolver = exports.DeafSessionDetector = exports.classifyDisconnect = exports.wrapWithSessionStability = exports.SessionHealthMonitor = exports.JidCanonicalizer = exports.LidResolver = exports.PostReconnectThrottle = exports.RetryReasonTracker = exports.getCircadianMultiplier = exports.PresenceChoreographer = exports.ContactGraphWarmer = exports.ReplyRatioGuard = exports.TimelockGuard = exports.HealthMonitor = exports.WarmUp = exports.RateLimiter = exports.AntiBan = void 0;
|
|
13
|
-
exports.createPeriodicExporter = exports.createMetricsHandler = exports.exportPrometheusMetrics = exports.createConsoleLogger = exports.createHumanEntropyService = exports.HumanEntropyService = exports.createInMemoryEventStoreBackend = exports.createMySQLEventStoreBackend = exports.createFleetEventStore = exports.createJidCircuitBreaker = exports.JidCircuitBreaker = exports.InstanceCoordinator = exports.DeliveryTracker = exports.BanRecoveryOrchestrator = exports.LegitimacySignalInjector = exports.GROUP_OP_ERRORS = exports.extractPrivacyBlock = exports.classifyGroupOpError = exports.GroupOperationGuard = exports.AbortError = exports.STEALTH_BROWSER_POOL = exports.rampPresenceAfterConnect = exports.getStealthSocketConfig = exports.createStealthFingerprint = exports.getBatteryState = exports.getVoiceNoteMetadata = void 0;
|
|
13
|
+
exports.createPeriodicExporter = exports.createMetricsHandler = exports.exportPrometheusMetrics = exports.createConsoleLogger = exports.importAntibanState = exports.exportAntibanState = exports.MessageTypeRegistry = exports.createHumanEntropyService = exports.HumanEntropyService = exports.createInMemoryEventStoreBackend = exports.createMySQLEventStoreBackend = exports.createFleetEventStore = exports.createJidCircuitBreaker = exports.JidCircuitBreaker = exports.InstanceCoordinator = exports.DeliveryTracker = exports.BanRecoveryOrchestrator = exports.LegitimacySignalInjector = exports.GROUP_OP_ERRORS = exports.extractPrivacyBlock = exports.classifyGroupOpError = exports.GroupOperationGuard = exports.AbortError = exports.STEALTH_BROWSER_POOL = exports.rampPresenceAfterConnect = exports.getStealthSocketConfig = exports.createStealthFingerprint = exports.getBatteryState = exports.getVoiceNoteMetadata = void 0;
|
|
14
14
|
// Core
|
|
15
15
|
var antiban_js_1 = require("./antiban.js");
|
|
16
16
|
Object.defineProperty(exports, "AntiBan", { enumerable: true, get: function () { return antiban_js_1.AntiBan; } });
|
|
@@ -143,6 +143,12 @@ Object.defineProperty(exports, "createInMemoryEventStoreBackend", { enumerable:
|
|
|
143
143
|
var humanEntropy_js_1 = require("./humanEntropy.js");
|
|
144
144
|
Object.defineProperty(exports, "HumanEntropyService", { enumerable: true, get: function () { return humanEntropy_js_1.HumanEntropyService; } });
|
|
145
145
|
Object.defineProperty(exports, "createHumanEntropyService", { enumerable: true, get: function () { return humanEntropy_js_1.createHumanEntropyService; } });
|
|
146
|
+
// v4.8 new modules
|
|
147
|
+
var messageTypeRegistry_js_1 = require("./messageTypeRegistry.js");
|
|
148
|
+
Object.defineProperty(exports, "MessageTypeRegistry", { enumerable: true, get: function () { return messageTypeRegistry_js_1.MessageTypeRegistry; } });
|
|
149
|
+
var stateExport_js_1 = require("./stateExport.js");
|
|
150
|
+
Object.defineProperty(exports, "exportAntibanState", { enumerable: true, get: function () { return stateExport_js_1.exportAntibanState; } });
|
|
151
|
+
Object.defineProperty(exports, "importAntibanState", { enumerable: true, get: function () { return stateExport_js_1.importAntibanState; } });
|
|
146
152
|
// Observability
|
|
147
153
|
var observability_js_1 = require("./observability.js");
|
|
148
154
|
Object.defineProperty(exports, "createConsoleLogger", { enumerable: true, get: function () { return observability_js_1.createConsoleLogger; } });
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
/**
|
|
3
|
+
* Message Type Registry — Track message types with priority, legitimacy, and engagement
|
|
4
|
+
*
|
|
5
|
+
* Developers register message types upfront with priority and legitimacy requirements.
|
|
6
|
+
* Library tracks engagement metrics per type and enforces provenance on critical sends.
|
|
7
|
+
*
|
|
8
|
+
* Features:
|
|
9
|
+
* - Type registration (immutable after first send)
|
|
10
|
+
* - Provenance validation for critical messages
|
|
11
|
+
* - Per-type engagement tracking (sent/delivered/read/replied/blocked)
|
|
12
|
+
* - Per-pool rate limiting
|
|
13
|
+
* - Warning emission (NO auto-throttling)
|
|
14
|
+
*/
|
|
15
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
16
|
+
exports.MessageTypeRegistry = void 0;
|
|
17
|
+
const rateLimiter_js_1 = require("./rateLimiter.js");
|
|
18
|
+
const DEFAULT_POOL_CONFIG = {
|
|
19
|
+
maxPerMinute: 8,
|
|
20
|
+
maxPerHour: 200,
|
|
21
|
+
maxPerDay: 1500,
|
|
22
|
+
minDelayMs: 1500,
|
|
23
|
+
maxDelayMs: 5000,
|
|
24
|
+
newChatDelayMs: 3000,
|
|
25
|
+
maxIdenticalMessages: 3,
|
|
26
|
+
burstAllowance: 3,
|
|
27
|
+
identicalMessageWindowMs: 3600000,
|
|
28
|
+
};
|
|
29
|
+
class MessageTypeRegistry {
|
|
30
|
+
types = new Map();
|
|
31
|
+
stats = new Map();
|
|
32
|
+
pools = new Map();
|
|
33
|
+
pendingMessages = new Map();
|
|
34
|
+
locked = new Set();
|
|
35
|
+
/**
|
|
36
|
+
* Register a message type with priority and legitimacy requirements.
|
|
37
|
+
* Registration is immutable after first message of that type is sent.
|
|
38
|
+
*/
|
|
39
|
+
registerMessageType(name, definition) {
|
|
40
|
+
if (this.locked.has(name)) {
|
|
41
|
+
throw new Error(`[MessageTypeRegistry] Type '${name}' is locked (messages already sent)`);
|
|
42
|
+
}
|
|
43
|
+
this.types.set(name, { ...definition });
|
|
44
|
+
this.stats.set(name, {
|
|
45
|
+
sent: 0,
|
|
46
|
+
delivered: 0,
|
|
47
|
+
read: 0,
|
|
48
|
+
replied: 0,
|
|
49
|
+
blocked: 0,
|
|
50
|
+
avgActionDeltaMs: 0,
|
|
51
|
+
engagementScore: 100, // Start optimistic
|
|
52
|
+
});
|
|
53
|
+
// Create pool rate limiter if specified
|
|
54
|
+
if (definition.rateLimitPool && !this.pools.has(definition.rateLimitPool)) {
|
|
55
|
+
const poolConfig = this.getPoolConfig(definition.priority);
|
|
56
|
+
this.pools.set(definition.rateLimitPool, {
|
|
57
|
+
limiter: new rateLimiter_js_1.RateLimiter(poolConfig),
|
|
58
|
+
config: poolConfig,
|
|
59
|
+
});
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
/**
|
|
63
|
+
* Send a message through the registry.
|
|
64
|
+
* Validates provenance and enforces rate limiting based on priority pool.
|
|
65
|
+
* Returns delay in ms before message can be sent.
|
|
66
|
+
*/
|
|
67
|
+
async send(sock, jid, content, options) {
|
|
68
|
+
const { type, provenance, engagementScore } = options;
|
|
69
|
+
// Validate type is registered
|
|
70
|
+
const definition = this.types.get(type);
|
|
71
|
+
if (!definition) {
|
|
72
|
+
throw new Error(`[MessageTypeRegistry] Type '${type}' not registered`);
|
|
73
|
+
}
|
|
74
|
+
// Lock type after first send
|
|
75
|
+
this.locked.add(type);
|
|
76
|
+
// Validate provenance for critical messages
|
|
77
|
+
if (definition.requiresProvenance && definition.requiresProvenance.length > 0) {
|
|
78
|
+
if (!provenance) {
|
|
79
|
+
throw new Error(`[MessageTypeRegistry] Type '${type}' requires provenance: ${definition.requiresProvenance.join(', ')}`);
|
|
80
|
+
}
|
|
81
|
+
// Check required fields
|
|
82
|
+
for (const field of definition.requiresProvenance) {
|
|
83
|
+
if (!(field in provenance)) {
|
|
84
|
+
throw new Error(`[MessageTypeRegistry] Type '${type}' requires provenance.${field}`);
|
|
85
|
+
}
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
// Validate legitimacy signals
|
|
89
|
+
if (definition.legitimacySignals) {
|
|
90
|
+
const signals = definition.legitimacySignals;
|
|
91
|
+
// Check action delta for critical messages
|
|
92
|
+
if (signals.maxActionDeltaMs !== undefined && provenance?.action_timestamp) {
|
|
93
|
+
const delta = Date.now() - provenance.action_timestamp;
|
|
94
|
+
if (delta > signals.maxActionDeltaMs) {
|
|
95
|
+
throw new Error(`[MessageTypeRegistry] Type '${type}' maxActionDeltaMs exceeded: ${delta}ms > ${signals.maxActionDeltaMs}ms`);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
// Check engagement score
|
|
99
|
+
if (signals.minEngagementScore !== undefined && engagementScore !== undefined) {
|
|
100
|
+
if (engagementScore < signals.minEngagementScore) {
|
|
101
|
+
throw new Error(`[MessageTypeRegistry] Type '${type}' minEngagementScore not met: ${engagementScore} < ${signals.minEngagementScore}`);
|
|
102
|
+
}
|
|
103
|
+
}
|
|
104
|
+
// Check subscription age
|
|
105
|
+
if (signals.minSubscriptionAgeDays !== undefined && provenance?.subscription_verified_at) {
|
|
106
|
+
const ageDays = (Date.now() - provenance.subscription_verified_at) / (24 * 60 * 60 * 1000);
|
|
107
|
+
if (ageDays < signals.minSubscriptionAgeDays) {
|
|
108
|
+
throw new Error(`[MessageTypeRegistry] Type '${type}' minSubscriptionAgeDays not met: ${ageDays.toFixed(1)} < ${signals.minSubscriptionAgeDays}`);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
// Apply rate limiting based on pool
|
|
113
|
+
if (definition.rateLimitPool) {
|
|
114
|
+
const pool = this.pools.get(definition.rateLimitPool);
|
|
115
|
+
if (pool) {
|
|
116
|
+
const text = content?.text || content?.caption || '';
|
|
117
|
+
const delay = await pool.limiter.getDelay(jid, text);
|
|
118
|
+
if (delay === -1) {
|
|
119
|
+
throw new Error(`[MessageTypeRegistry] Rate limit exceeded for pool '${definition.rateLimitPool}'`);
|
|
120
|
+
}
|
|
121
|
+
if (delay > 0) {
|
|
122
|
+
await new Promise(resolve => setTimeout(resolve, delay));
|
|
123
|
+
}
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
// Send message
|
|
127
|
+
const result = await sock.sendMessage(jid, content);
|
|
128
|
+
const msgId = result?.key?.id;
|
|
129
|
+
// Record send
|
|
130
|
+
const stat = this.stats.get(type);
|
|
131
|
+
stat.sent++;
|
|
132
|
+
// Track action delta
|
|
133
|
+
if (provenance?.action_timestamp) {
|
|
134
|
+
const delta = Date.now() - provenance.action_timestamp;
|
|
135
|
+
// Rolling average
|
|
136
|
+
stat.avgActionDeltaMs = Math.floor((stat.avgActionDeltaMs * (stat.sent - 1) + delta) / stat.sent);
|
|
137
|
+
}
|
|
138
|
+
// Track pending message for delivery/read/reply tracking
|
|
139
|
+
if (msgId) {
|
|
140
|
+
this.pendingMessages.set(msgId, {
|
|
141
|
+
type,
|
|
142
|
+
sentAt: Date.now(),
|
|
143
|
+
provenance,
|
|
144
|
+
});
|
|
145
|
+
}
|
|
146
|
+
// Record in pool
|
|
147
|
+
if (definition.rateLimitPool) {
|
|
148
|
+
const pool = this.pools.get(definition.rateLimitPool);
|
|
149
|
+
if (pool) {
|
|
150
|
+
pool.limiter.record(jid, content?.text || '');
|
|
151
|
+
}
|
|
152
|
+
}
|
|
153
|
+
return result;
|
|
154
|
+
}
|
|
155
|
+
/**
|
|
156
|
+
* Record message delivered (status 3 = DELIVERY_ACK)
|
|
157
|
+
*/
|
|
158
|
+
recordDelivered(messageId) {
|
|
159
|
+
const record = this.pendingMessages.get(messageId);
|
|
160
|
+
if (!record)
|
|
161
|
+
return;
|
|
162
|
+
const stat = this.stats.get(record.type);
|
|
163
|
+
if (stat) {
|
|
164
|
+
stat.delivered++;
|
|
165
|
+
this.updateEngagementScore(record.type);
|
|
166
|
+
}
|
|
167
|
+
}
|
|
168
|
+
/**
|
|
169
|
+
* Record message read (status 4 = READ)
|
|
170
|
+
*/
|
|
171
|
+
recordRead(messageId) {
|
|
172
|
+
const record = this.pendingMessages.get(messageId);
|
|
173
|
+
if (!record)
|
|
174
|
+
return;
|
|
175
|
+
const stat = this.stats.get(record.type);
|
|
176
|
+
if (stat) {
|
|
177
|
+
stat.read++;
|
|
178
|
+
this.updateEngagementScore(record.type);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
/**
|
|
182
|
+
* Record reply received
|
|
183
|
+
*/
|
|
184
|
+
recordReplied(messageId) {
|
|
185
|
+
const record = this.pendingMessages.get(messageId);
|
|
186
|
+
if (!record)
|
|
187
|
+
return;
|
|
188
|
+
const stat = this.stats.get(record.type);
|
|
189
|
+
if (stat) {
|
|
190
|
+
stat.replied++;
|
|
191
|
+
this.updateEngagementScore(record.type);
|
|
192
|
+
}
|
|
193
|
+
// Clean up after reply
|
|
194
|
+
this.pendingMessages.delete(messageId);
|
|
195
|
+
}
|
|
196
|
+
/**
|
|
197
|
+
* Record message blocked (send failed with block error)
|
|
198
|
+
*/
|
|
199
|
+
recordBlocked(_jid) {
|
|
200
|
+
// Find recent messages and mark as blocked
|
|
201
|
+
for (const [_msgId, record] of this.pendingMessages.entries()) {
|
|
202
|
+
// Only count recent blocks (last 5 minutes)
|
|
203
|
+
if (Date.now() - record.sentAt < 5 * 60 * 1000) {
|
|
204
|
+
const stat = this.stats.get(record.type);
|
|
205
|
+
if (stat) {
|
|
206
|
+
stat.blocked++;
|
|
207
|
+
this.updateEngagementScore(record.type);
|
|
208
|
+
}
|
|
209
|
+
}
|
|
210
|
+
}
|
|
211
|
+
}
|
|
212
|
+
/**
|
|
213
|
+
* Get stats for a message type
|
|
214
|
+
*/
|
|
215
|
+
getStats(type) {
|
|
216
|
+
const stat = this.stats.get(type);
|
|
217
|
+
return stat ? { ...stat } : null;
|
|
218
|
+
}
|
|
219
|
+
/**
|
|
220
|
+
* Get warnings for all message types.
|
|
221
|
+
* Returns array of warnings where metrics are below thresholds.
|
|
222
|
+
* NEVER auto-throttles — warnings only.
|
|
223
|
+
*/
|
|
224
|
+
getWarnings() {
|
|
225
|
+
const warnings = [];
|
|
226
|
+
const now = Date.now();
|
|
227
|
+
for (const [type, stat] of this.stats.entries()) {
|
|
228
|
+
// Skip types with insufficient data
|
|
229
|
+
if (stat.sent < 10)
|
|
230
|
+
continue;
|
|
231
|
+
// Engagement score warning (threshold: 50)
|
|
232
|
+
if (stat.engagementScore < 50) {
|
|
233
|
+
warnings.push({
|
|
234
|
+
type,
|
|
235
|
+
metric: 'engagement',
|
|
236
|
+
current: stat.engagementScore,
|
|
237
|
+
threshold: 50,
|
|
238
|
+
message: `Low engagement score for '${type}': ${stat.engagementScore.toFixed(1)}/100`,
|
|
239
|
+
});
|
|
240
|
+
const s = this.stats.get(type);
|
|
241
|
+
s.lastWarningAt = now;
|
|
242
|
+
}
|
|
243
|
+
// Delivery rate warning (threshold: 70%)
|
|
244
|
+
const deliveryRate = stat.sent > 0 ? stat.delivered / stat.sent : 1;
|
|
245
|
+
if (deliveryRate < 0.7 && stat.sent >= 20) {
|
|
246
|
+
warnings.push({
|
|
247
|
+
type,
|
|
248
|
+
metric: 'delivery_rate',
|
|
249
|
+
current: deliveryRate,
|
|
250
|
+
threshold: 0.7,
|
|
251
|
+
message: `Low delivery rate for '${type}': ${(deliveryRate * 100).toFixed(1)}%`,
|
|
252
|
+
});
|
|
253
|
+
const s = this.stats.get(type);
|
|
254
|
+
s.lastWarningAt = now;
|
|
255
|
+
}
|
|
256
|
+
// Blocked rate warning (threshold: 10%)
|
|
257
|
+
const blockedRate = stat.sent > 0 ? stat.blocked / stat.sent : 0;
|
|
258
|
+
if (blockedRate > 0.1 && stat.sent >= 20) {
|
|
259
|
+
warnings.push({
|
|
260
|
+
type,
|
|
261
|
+
metric: 'blocked_rate',
|
|
262
|
+
current: blockedRate,
|
|
263
|
+
threshold: 0.1,
|
|
264
|
+
message: `High blocked rate for '${type}': ${(blockedRate * 100).toFixed(1)}%`,
|
|
265
|
+
});
|
|
266
|
+
const s = this.stats.get(type);
|
|
267
|
+
s.lastWarningAt = now;
|
|
268
|
+
}
|
|
269
|
+
// Action delta warning for critical messages (threshold: 5 seconds)
|
|
270
|
+
const definition = this.types.get(type);
|
|
271
|
+
if (definition?.priority === 'critical' && stat.avgActionDeltaMs > 5000 && stat.sent >= 10) {
|
|
272
|
+
warnings.push({
|
|
273
|
+
type,
|
|
274
|
+
metric: 'action_delta',
|
|
275
|
+
current: stat.avgActionDeltaMs,
|
|
276
|
+
threshold: 5000,
|
|
277
|
+
message: `High action delta for '${type}': ${(stat.avgActionDeltaMs / 1000).toFixed(1)}s`,
|
|
278
|
+
});
|
|
279
|
+
const s = this.stats.get(type);
|
|
280
|
+
s.lastWarningAt = now;
|
|
281
|
+
}
|
|
282
|
+
}
|
|
283
|
+
return warnings;
|
|
284
|
+
}
|
|
285
|
+
/**
|
|
286
|
+
* Export state for persistence
|
|
287
|
+
*/
|
|
288
|
+
exportState() {
|
|
289
|
+
const poolsState = {};
|
|
290
|
+
for (const [name, _pool] of this.pools.entries()) {
|
|
291
|
+
poolsState[name] = {
|
|
292
|
+
sent: [],
|
|
293
|
+
timestamps: [],
|
|
294
|
+
};
|
|
295
|
+
}
|
|
296
|
+
return {
|
|
297
|
+
types: Object.fromEntries(this.types.entries()),
|
|
298
|
+
stats: Object.fromEntries(this.stats.entries()),
|
|
299
|
+
pools: poolsState,
|
|
300
|
+
pendingMessages: Object.fromEntries(this.pendingMessages.entries()),
|
|
301
|
+
locked: Array.from(this.locked), // Will be converted to Set on import
|
|
302
|
+
};
|
|
303
|
+
}
|
|
304
|
+
/**
|
|
305
|
+
* Import state from persistence
|
|
306
|
+
*/
|
|
307
|
+
importState(state) {
|
|
308
|
+
// Restore types
|
|
309
|
+
this.types = new Map(Object.entries(state.types));
|
|
310
|
+
// Restore stats
|
|
311
|
+
this.stats = new Map(Object.entries(state.stats));
|
|
312
|
+
// Restore pools (recreate rate limiters)
|
|
313
|
+
for (const [_name, definition] of this.types.entries()) {
|
|
314
|
+
if (definition.rateLimitPool && !this.pools.has(definition.rateLimitPool)) {
|
|
315
|
+
const poolConfig = this.getPoolConfig(definition.priority);
|
|
316
|
+
this.pools.set(definition.rateLimitPool, {
|
|
317
|
+
limiter: new rateLimiter_js_1.RateLimiter(poolConfig),
|
|
318
|
+
config: poolConfig,
|
|
319
|
+
});
|
|
320
|
+
}
|
|
321
|
+
}
|
|
322
|
+
// Restore pending messages
|
|
323
|
+
this.pendingMessages = new Map(Object.entries(state.pendingMessages));
|
|
324
|
+
// Restore locked types
|
|
325
|
+
this.locked = new Set(Array.isArray(state.locked) ? state.locked : []);
|
|
326
|
+
}
|
|
327
|
+
/**
|
|
328
|
+
* Clean up old pending messages (call periodically)
|
|
329
|
+
*/
|
|
330
|
+
cleanup() {
|
|
331
|
+
const now = Date.now();
|
|
332
|
+
const maxAge = 24 * 60 * 60 * 1000; // 24 hours
|
|
333
|
+
for (const [msgId, record] of this.pendingMessages.entries()) {
|
|
334
|
+
if (now - record.sentAt > maxAge) {
|
|
335
|
+
this.pendingMessages.delete(msgId);
|
|
336
|
+
}
|
|
337
|
+
}
|
|
338
|
+
}
|
|
339
|
+
updateEngagementScore(type) {
|
|
340
|
+
const stat = this.stats.get(type);
|
|
341
|
+
if (!stat || stat.sent === 0)
|
|
342
|
+
return;
|
|
343
|
+
// Engagement score = weighted: (read*0.3 + replied*0.5 + (1-blocked)*0.2) × 100
|
|
344
|
+
// Rolling 7-day window (use all stats for simplicity)
|
|
345
|
+
const readRate = stat.read / stat.sent;
|
|
346
|
+
const replyRate = stat.replied / stat.sent;
|
|
347
|
+
const notBlockedRate = 1 - (stat.blocked / stat.sent);
|
|
348
|
+
stat.engagementScore = Math.round((readRate * 0.3 + replyRate * 0.5 + notBlockedRate * 0.2) * 100);
|
|
349
|
+
}
|
|
350
|
+
getPoolConfig(priority) {
|
|
351
|
+
switch (priority) {
|
|
352
|
+
case 'critical':
|
|
353
|
+
return {
|
|
354
|
+
...DEFAULT_POOL_CONFIG,
|
|
355
|
+
maxPerMinute: 5,
|
|
356
|
+
maxPerHour: 100,
|
|
357
|
+
maxPerDay: 500,
|
|
358
|
+
};
|
|
359
|
+
case 'bulk':
|
|
360
|
+
return {
|
|
361
|
+
...DEFAULT_POOL_CONFIG,
|
|
362
|
+
maxPerMinute: 15,
|
|
363
|
+
maxPerHour: 300,
|
|
364
|
+
maxPerDay: 2000,
|
|
365
|
+
minDelayMs: 1000,
|
|
366
|
+
maxDelayMs: 3000,
|
|
367
|
+
};
|
|
368
|
+
default:
|
|
369
|
+
return DEFAULT_POOL_CONFIG;
|
|
370
|
+
}
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
exports.MessageTypeRegistry = MessageTypeRegistry;
|