@prototypevip/baileys 0.0.8 → 0.1.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/lib/AntiBan/antiban.js +922 -0
- package/lib/AntiBan/banRecoveryOrchestrator.js +284 -0
- package/lib/AntiBan/cli.js +177 -0
- package/lib/AntiBan/contactGraph.js +236 -0
- package/lib/AntiBan/contentVariator.js +150 -0
- package/lib/AntiBan/credsSnapshot.js +121 -0
- package/lib/AntiBan/deliveryTracker.js +107 -0
- package/lib/AntiBan/deviceFingerprint.js +106 -0
- package/lib/AntiBan/fleetEventStore.js +157 -0
- package/lib/AntiBan/groupOperationGuard.js +116 -0
- package/lib/AntiBan/health.js +207 -0
- package/lib/AntiBan/humanEntropy.js +337 -0
- package/lib/AntiBan/index.js +77 -0
- package/lib/AntiBan/instanceCoordinator.js +175 -0
- package/lib/AntiBan/jidCanonicalizer.js +256 -0
- package/lib/AntiBan/jidCircuitBreaker.js +186 -0
- package/lib/AntiBan/legitimacySignalInjector.js +231 -0
- package/lib/AntiBan/lidFirstResolver.js +174 -0
- package/lib/AntiBan/lidResolver.js +324 -0
- package/lib/AntiBan/messageQueue.js +187 -0
- package/lib/AntiBan/messageRecovery.js +283 -0
- package/lib/AntiBan/messageTypeRegistry.js +369 -0
- package/lib/AntiBan/observability.js +145 -0
- package/lib/AntiBan/patch.js +302 -0
- package/lib/AntiBan/persist.js +87 -0
- package/lib/AntiBan/presenceChoreographer.js +430 -0
- package/lib/AntiBan/presets.js +105 -0
- package/lib/AntiBan/profiles.js +31 -0
- package/lib/AntiBan/proxyRotator.js +306 -0
- package/lib/AntiBan/rateLimiter.js +261 -0
- package/lib/AntiBan/readReceiptVariance.js +88 -0
- package/lib/AntiBan/reconnectThrottle.js +180 -0
- package/lib/AntiBan/replyRatio.js +161 -0
- package/lib/AntiBan/reputationVoucher.js +373 -0
- package/lib/AntiBan/retryReason.js +91 -0
- package/lib/AntiBan/retryTracker.js +172 -0
- package/lib/AntiBan/scheduler.js +111 -0
- package/lib/AntiBan/sessionFingerprint.js +248 -0
- package/lib/AntiBan/sessionStability.js +330 -0
- package/lib/AntiBan/stateAdapter.js +73 -0
- package/lib/AntiBan/stateExport.js +152 -0
- package/lib/AntiBan/stealthConnect.js +130 -0
- package/lib/AntiBan/timelockGuard.js +181 -0
- package/lib/AntiBan/topologyThrottler.js +339 -0
- package/lib/AntiBan/warmup.js +140 -0
- package/lib/AntiBan/webhooks.js +80 -0
- package/lib/AntiBan/wrapper.js +456 -0
- package/lib/Socket/groups.js +26 -21
- package/lib/Socket/index.js +132 -7
- package/lib/Socket/messages-send.js +68 -5
- package/lib/Utils/ai-rich-formatter.js +256 -0
- package/lib/Utils/index.js +1 -0
- package/lib/index.js +1 -1
- package/package.json +1 -1
|
@@ -0,0 +1,150 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Content Variator — Auto-vary messages to avoid spam detection
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp flags identical messages sent to multiple recipients.
|
|
5
|
+
* This module adds invisible variations so each message is technically unique.
|
|
6
|
+
*/
|
|
7
|
+
const DEFAULT_CONFIG = {
|
|
8
|
+
zeroWidthChars: true,
|
|
9
|
+
punctuationVariation: true,
|
|
10
|
+
emojiPadding: false,
|
|
11
|
+
synonyms: false,
|
|
12
|
+
};
|
|
13
|
+
// Zero-width characters invisible to users
|
|
14
|
+
const ZERO_WIDTH = [
|
|
15
|
+
'\u200B', // zero-width space
|
|
16
|
+
'\u200C', // zero-width non-joiner
|
|
17
|
+
'\u200D', // zero-width joiner
|
|
18
|
+
'\uFEFF', // zero-width no-break space
|
|
19
|
+
];
|
|
20
|
+
const SYNONYMS = {
|
|
21
|
+
'hello': ['hi', 'hey', 'howdy'],
|
|
22
|
+
'hi': ['hello', 'hey', 'howdy'],
|
|
23
|
+
'thanks': ['thank you', 'thx', 'cheers'],
|
|
24
|
+
'please': ['kindly', 'pls'],
|
|
25
|
+
'great': ['awesome', 'excellent', 'wonderful'],
|
|
26
|
+
'good': ['great', 'nice', 'fine'],
|
|
27
|
+
'buy': ['purchase', 'get', 'grab'],
|
|
28
|
+
'sell': ['offer', 'list'],
|
|
29
|
+
'price': ['cost', 'amount', 'value'],
|
|
30
|
+
'available': ['in stock', 'on offer'],
|
|
31
|
+
'check': ['look at', 'see', 'view'],
|
|
32
|
+
'join': ['participate', 'enter', 'come to'],
|
|
33
|
+
'start': ['begin', 'kick off', 'commence'],
|
|
34
|
+
'end': ['finish', 'close', 'conclude'],
|
|
35
|
+
'bid': ['offer', 'place a bid'],
|
|
36
|
+
'win': ['secure', 'take home'],
|
|
37
|
+
'item': ['lot', 'piece', 'product'],
|
|
38
|
+
};
|
|
39
|
+
export class ContentVariator {
|
|
40
|
+
config;
|
|
41
|
+
counter = 0;
|
|
42
|
+
constructor(config = {}) {
|
|
43
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
44
|
+
}
|
|
45
|
+
/**
|
|
46
|
+
* Create a unique variation of a message
|
|
47
|
+
* Each call produces a slightly different version
|
|
48
|
+
*/
|
|
49
|
+
vary(text) {
|
|
50
|
+
let result = text;
|
|
51
|
+
this.counter++;
|
|
52
|
+
if (this.config.customVariator) {
|
|
53
|
+
return this.config.customVariator(result, this.counter);
|
|
54
|
+
}
|
|
55
|
+
if (this.config.synonyms) {
|
|
56
|
+
result = this.applySynonyms(result);
|
|
57
|
+
}
|
|
58
|
+
if (this.config.zeroWidthChars) {
|
|
59
|
+
result = this.addZeroWidth(result);
|
|
60
|
+
}
|
|
61
|
+
if (this.config.punctuationVariation) {
|
|
62
|
+
result = this.varyPunctuation(result);
|
|
63
|
+
}
|
|
64
|
+
if (this.config.emojiPadding) {
|
|
65
|
+
result = this.addEmojiPadding(result);
|
|
66
|
+
}
|
|
67
|
+
return result;
|
|
68
|
+
}
|
|
69
|
+
/**
|
|
70
|
+
* Create N unique variations of a message
|
|
71
|
+
*/
|
|
72
|
+
varyBulk(text, count) {
|
|
73
|
+
const results = [];
|
|
74
|
+
const seen = new Set();
|
|
75
|
+
for (let i = 0; i < count; i++) {
|
|
76
|
+
let variation = this.vary(text);
|
|
77
|
+
// Ensure uniqueness
|
|
78
|
+
let attempts = 0;
|
|
79
|
+
while (seen.has(variation) && attempts < 10) {
|
|
80
|
+
variation = this.vary(text);
|
|
81
|
+
attempts++;
|
|
82
|
+
}
|
|
83
|
+
seen.add(variation);
|
|
84
|
+
results.push(variation);
|
|
85
|
+
}
|
|
86
|
+
return results;
|
|
87
|
+
}
|
|
88
|
+
addZeroWidth(text) {
|
|
89
|
+
const words = text.split(' ');
|
|
90
|
+
if (words.length < 2)
|
|
91
|
+
return text;
|
|
92
|
+
// Insert 1-2 zero-width chars at random positions between words
|
|
93
|
+
const positions = this.randomPositions(words.length - 1, Math.min(2, words.length - 1));
|
|
94
|
+
return words.map((word, i) => {
|
|
95
|
+
if (positions.includes(i)) {
|
|
96
|
+
const zwc = ZERO_WIDTH[Math.floor(Math.random() * ZERO_WIDTH.length)];
|
|
97
|
+
return word + zwc;
|
|
98
|
+
}
|
|
99
|
+
return word;
|
|
100
|
+
}).join(' ');
|
|
101
|
+
}
|
|
102
|
+
varyPunctuation(text) {
|
|
103
|
+
const variations = [
|
|
104
|
+
// Trailing space variations
|
|
105
|
+
() => text + ' ',
|
|
106
|
+
() => text + ' ',
|
|
107
|
+
// Period variations
|
|
108
|
+
() => text.endsWith('.') ? text.slice(0, -1) : text + '.',
|
|
109
|
+
// Nothing
|
|
110
|
+
() => text,
|
|
111
|
+
// Capitalize first letter variation
|
|
112
|
+
() => text.charAt(0) === text.charAt(0).toUpperCase()
|
|
113
|
+
? text.charAt(0).toLowerCase() + text.slice(1)
|
|
114
|
+
: text,
|
|
115
|
+
];
|
|
116
|
+
return variations[this.counter % variations.length]();
|
|
117
|
+
}
|
|
118
|
+
addEmojiPadding(text) {
|
|
119
|
+
const emojis = ['', ' 👍', ' ✅', ' 📌', ' 💬', ' 📢'];
|
|
120
|
+
return text + emojis[this.counter % emojis.length];
|
|
121
|
+
}
|
|
122
|
+
applySynonyms(text) {
|
|
123
|
+
const words = text.split(/\b/);
|
|
124
|
+
let replaced = false;
|
|
125
|
+
return words.map(word => {
|
|
126
|
+
if (replaced)
|
|
127
|
+
return word;
|
|
128
|
+
const lower = word.toLowerCase();
|
|
129
|
+
const synonymList = SYNONYMS[lower];
|
|
130
|
+
if (synonymList && Math.random() > 0.5) {
|
|
131
|
+
replaced = true; // Only replace one word per message
|
|
132
|
+
const synonym = synonymList[Math.floor(Math.random() * synonymList.length)];
|
|
133
|
+
// Preserve original casing
|
|
134
|
+
return word[0] === word[0].toUpperCase()
|
|
135
|
+
? synonym.charAt(0).toUpperCase() + synonym.slice(1)
|
|
136
|
+
: synonym;
|
|
137
|
+
}
|
|
138
|
+
return word;
|
|
139
|
+
}).join('');
|
|
140
|
+
}
|
|
141
|
+
randomPositions(max, count) {
|
|
142
|
+
const positions = [];
|
|
143
|
+
while (positions.length < count) {
|
|
144
|
+
const pos = Math.floor(Math.random() * max);
|
|
145
|
+
if (!positions.includes(pos))
|
|
146
|
+
positions.push(pos);
|
|
147
|
+
}
|
|
148
|
+
return positions;
|
|
149
|
+
}
|
|
150
|
+
}
|
|
@@ -0,0 +1,121 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Atomic Credentials Snapshot
|
|
3
|
+
*
|
|
4
|
+
* Pre-reconnect backup to kill code-500 corruption loop.
|
|
5
|
+
* Take snapshots before risky operations, restore on corruption.
|
|
6
|
+
*
|
|
7
|
+
* @author Kobus Wentzel <kobie@pop.co.za>
|
|
8
|
+
* @license MIT
|
|
9
|
+
*/
|
|
10
|
+
import { promises as fs } from 'fs';
|
|
11
|
+
import * as path from 'path';
|
|
12
|
+
const noop = () => { };
|
|
13
|
+
export function credsSnapshot(config) {
|
|
14
|
+
const { credsPath, snapshotDir = path.join(path.dirname(credsPath), '.snapshots'), keep = 3, logger = {}, } = config;
|
|
15
|
+
const log = {
|
|
16
|
+
info: logger.info || noop,
|
|
17
|
+
warn: logger.warn || noop,
|
|
18
|
+
error: logger.error || noop,
|
|
19
|
+
};
|
|
20
|
+
async function take() {
|
|
21
|
+
try {
|
|
22
|
+
// Check if creds file exists
|
|
23
|
+
try {
|
|
24
|
+
await fs.access(credsPath);
|
|
25
|
+
}
|
|
26
|
+
catch {
|
|
27
|
+
log.warn(`[credsSnapshot] Creds file not found: ${credsPath}`);
|
|
28
|
+
return null;
|
|
29
|
+
}
|
|
30
|
+
// Ensure snapshot dir exists
|
|
31
|
+
await fs.mkdir(snapshotDir, { recursive: true });
|
|
32
|
+
// Generate snapshot path
|
|
33
|
+
const timestamp = new Date().toISOString().replace(/[:.]/g, '-');
|
|
34
|
+
const snapshotPath = path.join(snapshotDir, `creds-${timestamp}.json`);
|
|
35
|
+
const tmpPath = `${snapshotPath}.tmp`;
|
|
36
|
+
// Atomic copy: write to .tmp, then rename
|
|
37
|
+
await fs.copyFile(credsPath, tmpPath);
|
|
38
|
+
await fs.rename(tmpPath, snapshotPath);
|
|
39
|
+
log.info(`[credsSnapshot] Snapshot taken: ${snapshotPath}`);
|
|
40
|
+
// Rotate old snapshots
|
|
41
|
+
await rotate();
|
|
42
|
+
return snapshotPath;
|
|
43
|
+
}
|
|
44
|
+
catch (err) {
|
|
45
|
+
log.error(`[credsSnapshot] Failed to take snapshot: ${err}`);
|
|
46
|
+
return null;
|
|
47
|
+
}
|
|
48
|
+
}
|
|
49
|
+
async function rotate() {
|
|
50
|
+
try {
|
|
51
|
+
const snapshots = await list();
|
|
52
|
+
const toDelete = snapshots.slice(keep);
|
|
53
|
+
for (const snap of toDelete) {
|
|
54
|
+
await fs.unlink(snap.path);
|
|
55
|
+
log.info(`[credsSnapshot] Rotated out: ${snap.path}`);
|
|
56
|
+
}
|
|
57
|
+
}
|
|
58
|
+
catch (err) {
|
|
59
|
+
log.error(`[credsSnapshot] Rotation failed: ${err}`);
|
|
60
|
+
}
|
|
61
|
+
}
|
|
62
|
+
async function list() {
|
|
63
|
+
try {
|
|
64
|
+
await fs.access(snapshotDir);
|
|
65
|
+
}
|
|
66
|
+
catch {
|
|
67
|
+
return [];
|
|
68
|
+
}
|
|
69
|
+
try {
|
|
70
|
+
const files = await fs.readdir(snapshotDir);
|
|
71
|
+
const snapshots = await Promise.all(files
|
|
72
|
+
.filter((f) => f.startsWith('creds-') && f.endsWith('.json'))
|
|
73
|
+
.map(async (f) => {
|
|
74
|
+
const fullPath = path.join(snapshotDir, f);
|
|
75
|
+
const stat = await fs.stat(fullPath);
|
|
76
|
+
// Use file mtime for timestamp (simpler than parsing filename)
|
|
77
|
+
return {
|
|
78
|
+
path: fullPath,
|
|
79
|
+
takenAt: stat.mtime,
|
|
80
|
+
size: stat.size,
|
|
81
|
+
};
|
|
82
|
+
}));
|
|
83
|
+
// Sort newest first
|
|
84
|
+
return snapshots.sort((a, b) => b.takenAt.getTime() - a.takenAt.getTime());
|
|
85
|
+
}
|
|
86
|
+
catch (err) {
|
|
87
|
+
log.error(`[credsSnapshot] Failed to list snapshots: ${err}`);
|
|
88
|
+
return [];
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
async function restoreLatest() {
|
|
92
|
+
const snapshots = await list();
|
|
93
|
+
if (snapshots.length === 0) {
|
|
94
|
+
log.warn('[credsSnapshot] No snapshots available to restore');
|
|
95
|
+
return false;
|
|
96
|
+
}
|
|
97
|
+
return restore(snapshots[0].path);
|
|
98
|
+
}
|
|
99
|
+
async function restore(snapshotPath) {
|
|
100
|
+
try {
|
|
101
|
+
// Verify snapshot exists
|
|
102
|
+
await fs.access(snapshotPath);
|
|
103
|
+
// Atomic restore: copy to .tmp, then rename
|
|
104
|
+
const tmpPath = `${credsPath}.tmp`;
|
|
105
|
+
await fs.copyFile(snapshotPath, tmpPath);
|
|
106
|
+
await fs.rename(tmpPath, credsPath);
|
|
107
|
+
log.info(`[credsSnapshot] Restored from: ${snapshotPath}`);
|
|
108
|
+
return true;
|
|
109
|
+
}
|
|
110
|
+
catch (err) {
|
|
111
|
+
log.error(`[credsSnapshot] Failed to restore from ${snapshotPath}: ${err}`);
|
|
112
|
+
return false;
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
return {
|
|
116
|
+
take,
|
|
117
|
+
restoreLatest,
|
|
118
|
+
restore,
|
|
119
|
+
list,
|
|
120
|
+
};
|
|
121
|
+
}
|
|
@@ -0,0 +1,107 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* DeliveryTracker — tracks actual delivery rate vs sent
|
|
3
|
+
*
|
|
4
|
+
* WhatsApp delivery receipts arrive via messages.update events as
|
|
5
|
+
* update.update.status = 3 (DELIVERY_ACK) or 4 (READ).
|
|
6
|
+
*
|
|
7
|
+
* Low delivery rate (< 60%) = strong soft-ban signal.
|
|
8
|
+
* Exposes deliveryRate for health monitoring and stats.
|
|
9
|
+
*/
|
|
10
|
+
const DEFAULT_CONFIG = {
|
|
11
|
+
windowMs: 3600000, // 1 hour
|
|
12
|
+
minSampleSize: 10,
|
|
13
|
+
onLowDeliveryRate: () => { },
|
|
14
|
+
lowRateThreshold: 0.6,
|
|
15
|
+
};
|
|
16
|
+
export class DeliveryTracker {
|
|
17
|
+
config;
|
|
18
|
+
messages = new Map();
|
|
19
|
+
lastLowRateAlert = 0;
|
|
20
|
+
constructor(config = {}) {
|
|
21
|
+
this.config = { ...DEFAULT_CONFIG, ...config };
|
|
22
|
+
}
|
|
23
|
+
/**
|
|
24
|
+
* Register a sent message.
|
|
25
|
+
*/
|
|
26
|
+
onMessageSent(msgId) {
|
|
27
|
+
this.messages.set(msgId, {
|
|
28
|
+
sentAt: Date.now(),
|
|
29
|
+
delivered: false,
|
|
30
|
+
});
|
|
31
|
+
this.pruneOldMessages();
|
|
32
|
+
}
|
|
33
|
+
/**
|
|
34
|
+
* Mark a message as delivered (status 3 or 4).
|
|
35
|
+
*/
|
|
36
|
+
onDeliveryReceipt(msgId) {
|
|
37
|
+
const record = this.messages.get(msgId);
|
|
38
|
+
if (record) {
|
|
39
|
+
record.delivered = true;
|
|
40
|
+
}
|
|
41
|
+
this.pruneOldMessages();
|
|
42
|
+
this.checkDeliveryRate();
|
|
43
|
+
}
|
|
44
|
+
/**
|
|
45
|
+
* Get current delivery statistics.
|
|
46
|
+
*/
|
|
47
|
+
getStats() {
|
|
48
|
+
this.pruneOldMessages();
|
|
49
|
+
const now = Date.now();
|
|
50
|
+
const cutoff = now - this.config.windowMs;
|
|
51
|
+
let sentInWindow = 0;
|
|
52
|
+
let deliveredInWindow = 0;
|
|
53
|
+
for (const record of this.messages.values()) {
|
|
54
|
+
if (record.sentAt >= cutoff) {
|
|
55
|
+
sentInWindow++;
|
|
56
|
+
if (record.delivered) {
|
|
57
|
+
deliveredInWindow++;
|
|
58
|
+
}
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
const deliveryRate = sentInWindow >= this.config.minSampleSize
|
|
62
|
+
? deliveredInWindow / sentInWindow
|
|
63
|
+
: null;
|
|
64
|
+
return {
|
|
65
|
+
sentInWindow,
|
|
66
|
+
deliveredInWindow,
|
|
67
|
+
deliveryRate,
|
|
68
|
+
windowMs: this.config.windowMs,
|
|
69
|
+
};
|
|
70
|
+
}
|
|
71
|
+
/**
|
|
72
|
+
* Prune messages older than the window.
|
|
73
|
+
*/
|
|
74
|
+
pruneOldMessages() {
|
|
75
|
+
const now = Date.now();
|
|
76
|
+
const cutoff = now - this.config.windowMs;
|
|
77
|
+
for (const [msgId, record] of this.messages.entries()) {
|
|
78
|
+
if (record.sentAt < cutoff) {
|
|
79
|
+
this.messages.delete(msgId);
|
|
80
|
+
}
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
/**
|
|
84
|
+
* Check delivery rate and trigger callback if below threshold.
|
|
85
|
+
*/
|
|
86
|
+
checkDeliveryRate() {
|
|
87
|
+
const stats = this.getStats();
|
|
88
|
+
// Only alert if we have enough samples
|
|
89
|
+
if (stats.deliveryRate === null)
|
|
90
|
+
return;
|
|
91
|
+
// Only alert once per hour to avoid spam
|
|
92
|
+
const now = Date.now();
|
|
93
|
+
if (now - this.lastLowRateAlert < 3600000)
|
|
94
|
+
return;
|
|
95
|
+
if (stats.deliveryRate < this.config.lowRateThreshold) {
|
|
96
|
+
this.lastLowRateAlert = now;
|
|
97
|
+
this.config.onLowDeliveryRate(stats.deliveryRate);
|
|
98
|
+
}
|
|
99
|
+
}
|
|
100
|
+
/**
|
|
101
|
+
* Reset all tracked messages.
|
|
102
|
+
*/
|
|
103
|
+
reset() {
|
|
104
|
+
this.messages.clear();
|
|
105
|
+
this.lastLowRateAlert = 0;
|
|
106
|
+
}
|
|
107
|
+
}
|
|
@@ -0,0 +1,106 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Device Fingerprint Randomization
|
|
3
|
+
*
|
|
4
|
+
* Randomizes appVersion, osVersion, and deviceModel to prevent Meta's
|
|
5
|
+
* clientPayload fingerprinting. Addresses the #1 gap in anti-ban coverage.
|
|
6
|
+
*
|
|
7
|
+
* @author Kobus Wentzel <kobie@pop.co.za>
|
|
8
|
+
* @license MIT
|
|
9
|
+
*/
|
|
10
|
+
// Default pools - real-world values observed in the wild
|
|
11
|
+
const DEFAULT_APP_VERSION_POOL = [
|
|
12
|
+
[2, 24, 5, 18],
|
|
13
|
+
[2, 24, 5, 17],
|
|
14
|
+
[2, 24, 4, 77],
|
|
15
|
+
[2, 24, 5, 15],
|
|
16
|
+
[2, 24, 3, 91],
|
|
17
|
+
[2, 24, 5, 20],
|
|
18
|
+
];
|
|
19
|
+
const DEFAULT_OS_VERSION_POOL = ['10', '11', '12', '13', '14'];
|
|
20
|
+
const DEFAULT_DEVICE_MODEL_POOL = [
|
|
21
|
+
'Pixel 6',
|
|
22
|
+
'Pixel 7',
|
|
23
|
+
'Galaxy S22',
|
|
24
|
+
'Galaxy S23',
|
|
25
|
+
'Xiaomi 13',
|
|
26
|
+
'Xiaomi 12',
|
|
27
|
+
'OnePlus 11',
|
|
28
|
+
'Moto G84',
|
|
29
|
+
'Moto G54',
|
|
30
|
+
'Realme 11',
|
|
31
|
+
'Vivo V29',
|
|
32
|
+
'Oppo Find X6',
|
|
33
|
+
];
|
|
34
|
+
/**
|
|
35
|
+
* Simple deterministic PRNG using mulberry32
|
|
36
|
+
* Seeded from string hash for consistent results per session
|
|
37
|
+
*/
|
|
38
|
+
class SeededRandom {
|
|
39
|
+
state;
|
|
40
|
+
constructor(seed) {
|
|
41
|
+
// Hash string to 32-bit seed
|
|
42
|
+
let hash = 0;
|
|
43
|
+
for (let i = 0; i < seed.length; i++) {
|
|
44
|
+
hash = (hash << 5) - hash + seed.charCodeAt(i);
|
|
45
|
+
hash = hash & hash; // Convert to 32-bit int
|
|
46
|
+
}
|
|
47
|
+
this.state = Math.abs(hash) || 1;
|
|
48
|
+
}
|
|
49
|
+
next() {
|
|
50
|
+
let t = (this.state += 0x6d2b79f5);
|
|
51
|
+
t = Math.imul(t ^ (t >>> 15), t | 1);
|
|
52
|
+
t ^= t + Math.imul(t ^ (t >>> 7), t | 61);
|
|
53
|
+
return ((t ^ (t >>> 14)) >>> 0) / 4294967296;
|
|
54
|
+
}
|
|
55
|
+
pick(array) {
|
|
56
|
+
return array[Math.floor(this.next() * array.length)];
|
|
57
|
+
}
|
|
58
|
+
}
|
|
59
|
+
/**
|
|
60
|
+
* Generate a randomized fingerprint for one session.
|
|
61
|
+
* Stable for the same sessionId — call once per socket init.
|
|
62
|
+
*/
|
|
63
|
+
export function generateFingerprint(config = {}, sessionId) {
|
|
64
|
+
const { enabled = true, randomizeAppVersion = true, randomizeOsVersion = true, randomizeDeviceModel = true, seed, appVersionPool = DEFAULT_APP_VERSION_POOL, osVersionPool = DEFAULT_OS_VERSION_POOL, deviceModelPool = DEFAULT_DEVICE_MODEL_POOL, } = config;
|
|
65
|
+
const finalSessionId = sessionId || `session-${Date.now()}-${Math.random()}`;
|
|
66
|
+
const rng = new SeededRandom(seed || finalSessionId);
|
|
67
|
+
// Pick random values if enabled, otherwise use first pool item
|
|
68
|
+
const appVersion = enabled && randomizeAppVersion
|
|
69
|
+
? rng.pick(appVersionPool)
|
|
70
|
+
: appVersionPool[0];
|
|
71
|
+
const osVersion = enabled && randomizeOsVersion ? rng.pick(osVersionPool) : osVersionPool[0];
|
|
72
|
+
const deviceModel = enabled && randomizeDeviceModel
|
|
73
|
+
? rng.pick(deviceModelPool)
|
|
74
|
+
: deviceModelPool[0];
|
|
75
|
+
return {
|
|
76
|
+
appVersion: [...appVersion], // Copy to avoid mutation
|
|
77
|
+
osVersion,
|
|
78
|
+
deviceModel,
|
|
79
|
+
sessionId: finalSessionId,
|
|
80
|
+
};
|
|
81
|
+
}
|
|
82
|
+
/**
|
|
83
|
+
* Apply fingerprint to a Baileys SocketConfig before makeWASocket().
|
|
84
|
+
*
|
|
85
|
+
* Example:
|
|
86
|
+
* const fp = generateFingerprint({});
|
|
87
|
+
* const sock = makeWASocket(applyFingerprint(socketConfig, fp));
|
|
88
|
+
*/
|
|
89
|
+
export function applyFingerprint(socketConfig, fp) {
|
|
90
|
+
// Create defensive copy
|
|
91
|
+
const config = { ...socketConfig };
|
|
92
|
+
// Apply version if field exists
|
|
93
|
+
if (config.version !== undefined || 'version' in config || true) {
|
|
94
|
+
config.version = fp.appVersion;
|
|
95
|
+
}
|
|
96
|
+
// Apply browser tuple if field exists
|
|
97
|
+
// Baileys browser format: [deviceName, osVersion, appVersion]
|
|
98
|
+
if (config.browser !== undefined || 'browser' in config || true) {
|
|
99
|
+
config.browser = [
|
|
100
|
+
fp.deviceModel,
|
|
101
|
+
fp.osVersion,
|
|
102
|
+
`WhatsApp/${fp.appVersion.join('.')}`,
|
|
103
|
+
];
|
|
104
|
+
}
|
|
105
|
+
return config;
|
|
106
|
+
}
|
|
@@ -0,0 +1,157 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Fleet Event Store — Multi-instance coordination via shared event log
|
|
3
|
+
*
|
|
4
|
+
* Enables multiple WhatsApp instances to share ban/warn/recovery signals
|
|
5
|
+
* via a pluggable backend (MySQL, in-memory, etc).
|
|
6
|
+
*
|
|
7
|
+
* Architecture:
|
|
8
|
+
* - EventStoreBackend interface — caller provides storage
|
|
9
|
+
* - Two built-in backends:
|
|
10
|
+
* 1. MySQLEventStoreBackend — persistent, multi-instance (peer dep)
|
|
11
|
+
* 2. InMemoryEventStoreBackend — ephemeral, single-instance, testing
|
|
12
|
+
*
|
|
13
|
+
* Usage with MySQL:
|
|
14
|
+
* import mysql from 'mysql2/promise';
|
|
15
|
+
* const pool = mysql.createPool({ ... });
|
|
16
|
+
* const backend = createMySQLEventStoreBackend(pool);
|
|
17
|
+
* const store = createFleetEventStore({
|
|
18
|
+
* connectionId: 'wa-instance-1',
|
|
19
|
+
* backend,
|
|
20
|
+
* pollIntervalMs: 10_000
|
|
21
|
+
* });
|
|
22
|
+
* store.emit('warn', { risk: 'medium' });
|
|
23
|
+
* store.startPolling((events) => console.log('New events:', events));
|
|
24
|
+
*
|
|
25
|
+
* Usage in-memory (testing):
|
|
26
|
+
* const backend = createInMemoryEventStoreBackend();
|
|
27
|
+
* const store = createFleetEventStore({ connectionId: 'test', backend });
|
|
28
|
+
*/
|
|
29
|
+
export function createFleetEventStore(config) {
|
|
30
|
+
const { connectionId, backend, logger } = config;
|
|
31
|
+
const pollIntervalMs = config.pollIntervalMs ?? 10_000;
|
|
32
|
+
let lastSeenEpoch = Date.now();
|
|
33
|
+
let pollTimer = null;
|
|
34
|
+
const emit = async (eventType, payload) => {
|
|
35
|
+
const epoch = Date.now();
|
|
36
|
+
await backend.emit(connectionId, eventType, epoch, payload);
|
|
37
|
+
logger?.info('[fleet-events] emitted', { connectionId, eventType, epoch });
|
|
38
|
+
};
|
|
39
|
+
const startPolling = (onNewEvents) => {
|
|
40
|
+
if (pollTimer)
|
|
41
|
+
return;
|
|
42
|
+
pollTimer = setInterval(() => {
|
|
43
|
+
void (async () => {
|
|
44
|
+
try {
|
|
45
|
+
const events = await backend.poll(connectionId, lastSeenEpoch);
|
|
46
|
+
if (events.length > 0) {
|
|
47
|
+
// Update cursor
|
|
48
|
+
const maxEpoch = Math.max(...events.map((e) => e.epoch));
|
|
49
|
+
lastSeenEpoch = maxEpoch;
|
|
50
|
+
onNewEvents(events);
|
|
51
|
+
}
|
|
52
|
+
}
|
|
53
|
+
catch (error) {
|
|
54
|
+
logger?.warn('[fleet-events] poll failed', { connectionId, err: error });
|
|
55
|
+
}
|
|
56
|
+
})();
|
|
57
|
+
}, pollIntervalMs);
|
|
58
|
+
// @ts-ignore — unref exists on NodeJS.Timeout
|
|
59
|
+
pollTimer.unref?.();
|
|
60
|
+
logger?.info('[fleet-events] polling started', { connectionId, pollIntervalMs });
|
|
61
|
+
};
|
|
62
|
+
const stop = () => {
|
|
63
|
+
if (pollTimer) {
|
|
64
|
+
clearInterval(pollTimer);
|
|
65
|
+
pollTimer = null;
|
|
66
|
+
logger?.info('[fleet-events] polling stopped', { connectionId });
|
|
67
|
+
}
|
|
68
|
+
};
|
|
69
|
+
return { emit, startPolling, stop };
|
|
70
|
+
}
|
|
71
|
+
export function createMySQLEventStoreBackend(pool) {
|
|
72
|
+
// Ensure table exists on first emit (idempotent)
|
|
73
|
+
let tableEnsured = false;
|
|
74
|
+
const ensureTable = async () => {
|
|
75
|
+
if (tableEnsured)
|
|
76
|
+
return;
|
|
77
|
+
const createTableSQL = `
|
|
78
|
+
CREATE TABLE IF NOT EXISTS antiban_events (
|
|
79
|
+
id BIGINT AUTO_INCREMENT PRIMARY KEY,
|
|
80
|
+
connection_id VARCHAR(255) NOT NULL,
|
|
81
|
+
event_type VARCHAR(50) NOT NULL,
|
|
82
|
+
epoch BIGINT NOT NULL,
|
|
83
|
+
payload JSON,
|
|
84
|
+
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP,
|
|
85
|
+
INDEX idx_conn (connection_id),
|
|
86
|
+
INDEX idx_epoch (epoch)
|
|
87
|
+
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4
|
|
88
|
+
`;
|
|
89
|
+
try {
|
|
90
|
+
await pool.query(createTableSQL);
|
|
91
|
+
tableEnsured = true;
|
|
92
|
+
}
|
|
93
|
+
catch (error) {
|
|
94
|
+
// Log but don't throw — table might already exist
|
|
95
|
+
console.warn('[mysql-backend] table creation failed (may already exist)', error);
|
|
96
|
+
tableEnsured = true; // Assume it exists
|
|
97
|
+
}
|
|
98
|
+
};
|
|
99
|
+
const emit = async (connectionId, eventType, epoch, payload) => {
|
|
100
|
+
try {
|
|
101
|
+
await ensureTable();
|
|
102
|
+
await pool.execute('INSERT INTO antiban_events (connection_id, event_type, epoch, payload) VALUES (?, ?, ?, ?)', [connectionId, eventType, epoch, payload ? JSON.stringify(payload) : null]);
|
|
103
|
+
}
|
|
104
|
+
catch (error) {
|
|
105
|
+
// Never throw — event emission is non-critical
|
|
106
|
+
console.warn('[mysql-backend] emit failed', { connectionId, eventType, error });
|
|
107
|
+
}
|
|
108
|
+
};
|
|
109
|
+
const poll = async (connectionId, sinceEpoch) => {
|
|
110
|
+
try {
|
|
111
|
+
await ensureTable();
|
|
112
|
+
const [rows] = (await pool.execute('SELECT id, connection_id, event_type, epoch, payload, created_at FROM antiban_events WHERE connection_id = ? AND epoch > ? ORDER BY epoch ASC LIMIT 50', [connectionId, sinceEpoch]));
|
|
113
|
+
return rows.map((row) => ({
|
|
114
|
+
id: row.id,
|
|
115
|
+
connectionId: row.connection_id,
|
|
116
|
+
eventType: row.event_type,
|
|
117
|
+
epoch: row.epoch,
|
|
118
|
+
payload: row.payload ? JSON.parse(row.payload) : null,
|
|
119
|
+
createdAt: row.created_at,
|
|
120
|
+
}));
|
|
121
|
+
}
|
|
122
|
+
catch (error) {
|
|
123
|
+
console.warn('[mysql-backend] poll failed', { connectionId, error });
|
|
124
|
+
return [];
|
|
125
|
+
}
|
|
126
|
+
};
|
|
127
|
+
return { emit, poll };
|
|
128
|
+
}
|
|
129
|
+
// ===============================
|
|
130
|
+
// Built-in Backend: In-Memory
|
|
131
|
+
// ===============================
|
|
132
|
+
export function createInMemoryEventStoreBackend() {
|
|
133
|
+
const events = [];
|
|
134
|
+
let nextId = 1;
|
|
135
|
+
const emit = async (connectionId, eventType, epoch, payload) => {
|
|
136
|
+
const event = {
|
|
137
|
+
id: nextId++,
|
|
138
|
+
connectionId,
|
|
139
|
+
eventType,
|
|
140
|
+
epoch,
|
|
141
|
+
payload: payload || null,
|
|
142
|
+
createdAt: new Date(),
|
|
143
|
+
};
|
|
144
|
+
events.push(event);
|
|
145
|
+
// Evict oldest if over 1000 entries
|
|
146
|
+
if (events.length > 1000) {
|
|
147
|
+
events.shift();
|
|
148
|
+
}
|
|
149
|
+
};
|
|
150
|
+
const poll = async (connectionId, sinceEpoch) => {
|
|
151
|
+
return events
|
|
152
|
+
.filter((e) => e.connectionId === connectionId && e.epoch > sinceEpoch)
|
|
153
|
+
.sort((a, b) => a.epoch - b.epoch)
|
|
154
|
+
.slice(0, 50);
|
|
155
|
+
};
|
|
156
|
+
return { emit, poll };
|
|
157
|
+
}
|