innovators-bot2 1.1.2 → 1.1.4

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/README.md CHANGED
@@ -12,6 +12,10 @@ A powerful WhatsApp client library that provides seamless integration between Ba
12
12
  - 💾 Message history and chat management
13
13
  - 🔄 Auto-reconnect functionality
14
14
  - 📝 Read receipts
15
+ - 🛡️ Anti-abuse protection with rate limiting
16
+ - 🔁 Automatic retry with exponential backoff
17
+ - ⚡ Idempotency to prevent duplicate sends
18
+ - 🚦 Circuit breaker to prevent spam
15
19
 
16
20
  ## Installation
17
21
 
@@ -185,6 +189,17 @@ const messages = await client.loadMessages(chatId, 50)
185
189
  const message = await client.loadMessage(chatId, messageId)
186
190
  ```
187
191
 
192
+ ### 5. Anti-Abuse Protection
193
+
194
+ The library now includes built-in anti-abuse protection to help prevent your bot from being banned:
195
+
196
+ - **Rate Limiting**: Automatic rate limiting to prevent sending messages too quickly
197
+ - **Idempotency**: Prevents duplicate messages from being sent
198
+ - **Retry Logic**: Automatic retry with exponential backoff for failed sends
199
+ - **Circuit Breaker**: Temporarily stops sending when errors are detected
200
+
201
+ All message sending methods automatically use these protections.
202
+
188
203
  ## More Examples and Information
189
204
 
190
205
  For a complete working example with message handling, group management, and error handling, check out our [`example.js`](https://github.com/innovatorssoft/innovators-bot2/blob/main/example.js) file. This example includes:
package/antiban.d.ts ADDED
@@ -0,0 +1,46 @@
1
+ type SendFn<R = any> = (jid: string, content: any, opts?: any) => Promise<R>;
2
+ interface GuardOpts {
3
+ maxConcurrent?: number;
4
+ minGlobalIntervalMs?: number;
5
+ perJidMinIntervalMs?: number;
6
+ burstCapacity?: number;
7
+ refillPerSecond?: number;
8
+ maxAttempts?: number;
9
+ baseBackoffMs?: number;
10
+ maxBackoffMs?: number;
11
+ cbWindowMs?: number;
12
+ cbErrorThreshold?: number;
13
+ cbOpenMs?: number;
14
+ idempotencyTtlMs?: number;
15
+ classifyStatus?: (e: any) => number | null;
16
+ onLog?: (msg: string, meta?: Record<string, any>) => void;
17
+ }
18
+ export declare class WhatsAppSafeSender {
19
+ private queue;
20
+ private running;
21
+ private lastGlobalSend;
22
+ private lastPerJid;
23
+ private tokens;
24
+ private burstCapacity;
25
+ private refillPerSecond;
26
+ private lastRefill;
27
+ private attempts;
28
+ private idempotencyTtl;
29
+ private cbEvents;
30
+ private cbOpenUntil;
31
+ private cfg;
32
+ private classifyStatus;
33
+ private onLog;
34
+ constructor(opts?: GuardOpts);
35
+ send<R = any>(sendFn: SendFn<R>, jid: string, content: any, opts?: any, idempotencyKey?: string): Promise<R>;
36
+ private enqueue;
37
+ private coreSend;
38
+ private waitForBudget;
39
+ private refillTokens;
40
+ private enforceIntervals;
41
+ private recordOk;
42
+ private recordError;
43
+ private pruneCb;
44
+ private tripCircuit;
45
+ }
46
+ export {};
package/antiban.js ADDED
@@ -0,0 +1,236 @@
1
+ "use strict";
2
+ // Anti-abuse, policy-friendly WhatsApp sender helper (TypeScript)
3
+ // Goal: be conservative, respect limits, back off on throttling, avoid duplicate sends.
4
+ Object.defineProperty(exports, "__esModule", { value: true });
5
+ exports.WhatsAppSafeSender = void 0;
6
+ class WhatsAppSafeSender {
7
+ constructor(opts = {}) {
8
+ var _a, _b, _c, _d, _e, _f, _g, _h, _j, _k, _l, _m, _o, _p;
9
+ this.queue = Promise.resolve();
10
+ this.running = 0;
11
+ this.lastGlobalSend = 0;
12
+ this.lastPerJid = new Map();
13
+ this.lastRefill = Date.now();
14
+ this.attempts = new Map(); // idempotency keys
15
+ // circuit breaker
16
+ this.cbEvents = [];
17
+ this.cbOpenUntil = 0;
18
+ this.cfg = {
19
+ maxConcurrent: (_a = opts.maxConcurrent) !== null && _a !== void 0 ? _a : 1,
20
+ minGlobalIntervalMs: (_b = opts.minGlobalIntervalMs) !== null && _b !== void 0 ? _b : 1200, // be conservative
21
+ perJidMinIntervalMs: (_c = opts.perJidMinIntervalMs) !== null && _c !== void 0 ? _c : 5000, // do not spam a single chat
22
+ burstCapacity: (_d = opts.burstCapacity) !== null && _d !== void 0 ? _d : 3,
23
+ refillPerSecond: (_e = opts.refillPerSecond) !== null && _e !== void 0 ? _e : 0.5, // 1 token every 2s
24
+ maxAttempts: (_f = opts.maxAttempts) !== null && _f !== void 0 ? _f : 4,
25
+ baseBackoffMs: (_g = opts.baseBackoffMs) !== null && _g !== void 0 ? _g : 1500,
26
+ maxBackoffMs: (_h = opts.maxBackoffMs) !== null && _h !== void 0 ? _h : 60000,
27
+ cbWindowMs: (_j = opts.cbWindowMs) !== null && _j !== void 0 ? _j : 60000,
28
+ cbErrorThreshold: (_k = opts.cbErrorThreshold) !== null && _k !== void 0 ? _k : 8,
29
+ cbOpenMs: (_l = opts.cbOpenMs) !== null && _l !== void 0 ? _l : 120000,
30
+ idempotencyTtlMs: (_m = opts.idempotencyTtlMs) !== null && _m !== void 0 ? _m : 5 * 60000,
31
+ };
32
+ this.tokens = this.cfg.burstCapacity;
33
+ this.burstCapacity = this.cfg.burstCapacity;
34
+ this.refillPerSecond = this.cfg.refillPerSecond;
35
+ this.idempotencyTtl = this.cfg.idempotencyTtlMs;
36
+ this.classifyStatus = (_o = opts.classifyStatus) !== null && _o !== void 0 ? _o : defaultStatusClassifier;
37
+ this.onLog = (_p = opts.onLog) !== null && _p !== void 0 ? _p : (() => { });
38
+ }
39
+ // Public entry: safe send
40
+ async send(sendFn, jid, content, opts, idempotencyKey) {
41
+ // Serialize with a tiny internal queue to honor maxConcurrent cleanly
42
+ return this.enqueue(() => this.coreSend(sendFn, jid, content, opts, idempotencyKey));
43
+ }
44
+ // Simple concurrency gate + FIFO
45
+ enqueue(task) {
46
+ const run = async () => {
47
+ // Wait for a slot
48
+ while (this.running >= this.cfg.maxConcurrent) {
49
+ await sleep(25);
50
+ }
51
+ this.running++;
52
+ try {
53
+ return await task();
54
+ }
55
+ finally {
56
+ this.running--;
57
+ }
58
+ };
59
+ this.queue = this.queue.then(run, run);
60
+ return this.queue;
61
+ }
62
+ async coreSend(sendFn, jid, content, opts, idemKey) {
63
+ var _a, _b;
64
+ // Circuit breaker check
65
+ const now = Date.now();
66
+ if (this.cbOpenUntil > now) {
67
+ const wait = this.cbOpenUntil - now;
68
+ this.onLog("circuit_open_wait", { wait });
69
+ await sleep(wait);
70
+ }
71
+ // Idempotency: drop duplicates within TTL
72
+ if (idemKey) {
73
+ const seenAt = this.attempts.get(idemKey);
74
+ if (seenAt && now - seenAt < this.idempotencyTtl) {
75
+ this.onLog("idempotent_drop", { idemKey });
76
+ // @ts-ignore
77
+ return undefined;
78
+ }
79
+ this.attempts.set(idemKey, now);
80
+ (_b = (_a = setTimeout(() => this.attempts.delete(idemKey), this.idempotencyTtl)).unref) === null || _b === void 0 ? void 0 : _b.call(_a);
81
+ }
82
+ // Respect rate limits
83
+ await this.waitForBudget();
84
+ await this.enforceIntervals(jid);
85
+ let attempt = 0;
86
+ let backoff = this.cfg.baseBackoffMs;
87
+ // Retry loop
88
+ while (true) {
89
+ attempt++;
90
+ try {
91
+ const res = await sendFn(jid, content, opts);
92
+ // success: record global/per-jid time and token consumption
93
+ this.lastGlobalSend = Date.now();
94
+ this.lastPerJid.set(jid, this.lastGlobalSend);
95
+ this.recordOk();
96
+ return res;
97
+ }
98
+ catch (err) {
99
+ const status = this.classifyStatus(err);
100
+ // Check Retry-After if present
101
+ const retryAfterMs = parseRetryAfter(err);
102
+ const isThrottle = status === 429 || retryAfterMs > 0;
103
+ const isAuthOrForbidden = status === 401 || status === 403;
104
+ const isClientBad = status && status >= 400 && status < 500 && !isThrottle && !isAuthOrForbidden;
105
+ const isServer = status && status >= 500;
106
+ this.recordError();
107
+ // Client errors (other 4xx): do not retry blindly
108
+ if (isClientBad) {
109
+ this.onLog("client_error_no_retry", { status, attempt, message: safeMsg(err) });
110
+ throw err;
111
+ }
112
+ // Authentication/Forbidden: open circuit for a bit to avoid hammering
113
+ if (isAuthOrForbidden) {
114
+ const waitMs = Math.min(Math.max(retryAfterMs, backoff), this.cfg.maxBackoffMs);
115
+ this.tripCircuit(waitMs * 2);
116
+ this.onLog("auth_forbidden_backoff", { status, waitMs, attempt });
117
+ await sleep(waitMs);
118
+ }
119
+ else if (isThrottle || isServer) {
120
+ // Throttle or 5xx: exponential backoff with jitter
121
+ const waitMs = Math.min(Math.max(retryAfterMs, jitter(backoff)), this.cfg.maxBackoffMs);
122
+ this.onLog("retry_backoff", { status, waitMs, attempt });
123
+ await sleep(waitMs);
124
+ backoff = Math.min(backoff * 2, this.cfg.maxBackoffMs);
125
+ }
126
+ else {
127
+ // Unknown: one conservative wait, then rethrow
128
+ const waitMs = Math.min(jitter(backoff), this.cfg.maxBackoffMs);
129
+ this.onLog("unknown_backoff_once", { status, waitMs, attempt });
130
+ await sleep(waitMs);
131
+ }
132
+ if (attempt >= this.cfg.maxAttempts) {
133
+ this.onLog("max_attempts_reached", { attempt, status, message: safeMsg(err) });
134
+ throw err;
135
+ }
136
+ }
137
+ }
138
+ }
139
+ // Token bucket for bursts
140
+ async waitForBudget() {
141
+ while (true) {
142
+ this.refillTokens();
143
+ if (this.tokens >= 1) {
144
+ this.tokens -= 1;
145
+ return;
146
+ }
147
+ await sleep(100);
148
+ }
149
+ }
150
+ refillTokens() {
151
+ const now = Date.now();
152
+ const delta = (now - this.lastRefill) / 1000;
153
+ if (delta > 0) {
154
+ this.tokens = Math.min(this.burstCapacity, this.tokens + delta * this.refillPerSecond);
155
+ this.lastRefill = now;
156
+ }
157
+ }
158
+ async enforceIntervals(jid) {
159
+ var _a;
160
+ const now = Date.now();
161
+ // global interval
162
+ const nextGlobal = this.lastGlobalSend + this.cfg.minGlobalIntervalMs;
163
+ if (now < nextGlobal) {
164
+ await sleep(nextGlobal - now);
165
+ }
166
+ // per-jid interval
167
+ const lastJid = (_a = this.lastPerJid.get(jid)) !== null && _a !== void 0 ? _a : 0;
168
+ const nextJid = lastJid + this.cfg.perJidMinIntervalMs;
169
+ const n2 = Date.now();
170
+ if (n2 < nextJid) {
171
+ await sleep(nextJid - n2);
172
+ }
173
+ }
174
+ recordOk() {
175
+ this.pruneCb();
176
+ this.cbEvents.push(1); // mark OK as 1 to maintain window length
177
+ }
178
+ recordError() {
179
+ this.pruneCb();
180
+ this.cbEvents.push(0); // errors as 0
181
+ // If too many failures, open circuit
182
+ const errors = this.cbEvents.filter(x => x === 0).length;
183
+ if (errors >= this.cfg.cbErrorThreshold) {
184
+ this.tripCircuit(this.cfg.cbOpenMs);
185
+ }
186
+ }
187
+ pruneCb() {
188
+ const cutoff = Date.now() - this.cfg.cbWindowMs;
189
+ // store timestamps implicitly via array length; we’ll just keep size bounded
190
+ if (this.cbEvents.length > 1000)
191
+ this.cbEvents = this.cbEvents.slice(-500);
192
+ // no exact timestamps; threshold is coarse but fine for a rolling minute window
193
+ }
194
+ tripCircuit(ms) {
195
+ this.cbOpenUntil = Date.now() + ms;
196
+ }
197
+ }
198
+ exports.WhatsAppSafeSender = WhatsAppSafeSender;
199
+ // Helpers
200
+ function sleep(ms) {
201
+ return new Promise(resolve => setTimeout(resolve, ms));
202
+ }
203
+ function jitter(ms) {
204
+ const r = 0.5 + Math.random(); // 0.5x .. 1.5x
205
+ return Math.floor(ms * r);
206
+ }
207
+ function safeMsg(e) {
208
+ var _a;
209
+ const m = (_a = e === null || e === void 0 ? void 0 : e.message) !== null && _a !== void 0 ? _a : String(e);
210
+ return m.substring(0, 500);
211
+ }
212
+ function parseRetryAfter(e) {
213
+ var _a, _b, _c, _d;
214
+ try {
215
+ const ra = (_c = (_b = (_a = e === null || e === void 0 ? void 0 : e.response) === null || _a === void 0 ? void 0 : _a.headers) === null || _b === void 0 ? void 0 : _b["retry-after"]) !== null && _c !== void 0 ? _c : (_d = e === null || e === void 0 ? void 0 : e.headers) === null || _d === void 0 ? void 0 : _d["retry-after"];
216
+ if (!ra)
217
+ return 0;
218
+ const asNum = Number(ra);
219
+ if (!Number.isNaN(asNum))
220
+ return asNum * 1000;
221
+ const asDate = Date.parse(ra);
222
+ if (!Number.isNaN(asDate))
223
+ return Math.max(0, asDate - Date.now());
224
+ return 0;
225
+ }
226
+ catch (_e) {
227
+ return 0;
228
+ }
229
+ }
230
+ function defaultStatusClassifier(e) {
231
+ var _a, _b, _c, _d, _e;
232
+ // Try to normalize common shapes
233
+ const s = (_d = (_b = (_a = e === null || e === void 0 ? void 0 : e.status) !== null && _a !== void 0 ? _a : e === null || e === void 0 ? void 0 : e.code) !== null && _b !== void 0 ? _b : (_c = e === null || e === void 0 ? void 0 : e.response) === null || _c === void 0 ? void 0 : _c.status) !== null && _d !== void 0 ? _d : (_e = e === null || e === void 0 ? void 0 : e.output) === null || _e === void 0 ? void 0 : _e.statusCode;
234
+ const n = Number(s);
235
+ return Number.isFinite(n) ? n : null;
236
+ }
package/config.json CHANGED
@@ -1,5 +1,5 @@
1
1
  {
2
2
  "whatsapp": {
3
- "authMethod": "pairing"
3
+ "authMethod": "qr"
4
4
  }
5
5
  }
package/example.js CHANGED
@@ -452,3 +452,6 @@ client.on('message', async msg => {
452
452
  client.on('error', error => {
453
453
  console.error('Client Error:', error)
454
454
  })
455
+
456
+ // The client now automatically uses anti-abuse protection for all message sending methods
457
+ // No additional configuration is needed - all sendMessage, sendMedia, etc. calls are protected
package/index.js CHANGED
@@ -12,6 +12,8 @@ const path = require('path');
12
12
  const mime = require('mime');
13
13
  const figlet = require('figlet');
14
14
  const readline = require('readline');
15
+ const { WhatsAppSafeSender } = require('./antiban');
16
+ const { buildIdemKey } = require('./utils');
15
17
 
16
18
  let rl;
17
19
  const question = (text) => {
@@ -59,6 +61,15 @@ class WhatsAppClient extends EventEmitter {
59
61
  this._connectionState = 'disconnected' // Track connection state
60
62
  this.authmethod = config.authmethod || 'qr'; // 'qr' or 'pairing'
61
63
  this._reconnectDelay = 5000; // 5 seconds
64
+
65
+ // Initialize the WhatsAppSafeSender with conservative settings
66
+ this.guard = new WhatsAppSafeSender({
67
+ minGlobalIntervalMs: 1500,
68
+ perJidMinIntervalMs: 6000,
69
+ burstCapacity: 2,
70
+ refillPerSecond: 0.5,
71
+ onLog: (msg, meta) => console.log(`[guard] ${msg}`, meta),
72
+ });
62
73
  }
63
74
 
64
75
  async connect() {
@@ -333,7 +344,9 @@ class WhatsAppClient extends EventEmitter {
333
344
  }
334
345
 
335
346
  try {
336
- return await this.sock.sendMessage(chatId, messageContent, options);
347
+ // Use the safe sender to send the message with idempotency
348
+ const idemKey = buildIdemKey(chatId, messageContent);
349
+ return await this.guard.send(this.sock.sendMessage.bind(this.sock), chatId, messageContent, options, idemKey);
337
350
  } catch (error) {
338
351
  console.error('Error sending message:', error);
339
352
  throw error;
@@ -399,7 +412,9 @@ class WhatsAppClient extends EventEmitter {
399
412
  throw new Error('Unsupported file type: ' + fileExtension);
400
413
  }
401
414
 
402
- return await this.sock.sendMessage(chatId, mediaMessage);
415
+ // Use the safe sender to send the media with idempotency
416
+ const idemKey = buildIdemKey(chatId, mediaMessage);
417
+ return await this.guard.send(this.sock.sendMessage.bind(this.sock), chatId, mediaMessage, undefined, idemKey);
403
418
  } catch (error) {
404
419
  console.error('Error sending media:', error);
405
420
  throw error;
@@ -429,12 +444,16 @@ class WhatsAppClient extends EventEmitter {
429
444
  const fileName = path.basename(filePath);
430
445
  const mimeType = mime.getType(filePath);
431
446
 
432
- return await this.sock.sendMessage(chatId, {
447
+ const documentMessage = {
433
448
  document: fileBuffer,
434
449
  caption: caption,
435
450
  mimetype: mimeType,
436
451
  fileName: fileName,
437
- });
452
+ };
453
+
454
+ // Use the safe sender to send the document with idempotency
455
+ const idemKey = buildIdemKey(chatId, documentMessage);
456
+ return await this.guard.send(this.sock.sendMessage.bind(this.sock), chatId, documentMessage, undefined, idemKey);
438
457
  } catch (error) {
439
458
  console.error('Error sending document:', error);
440
459
  throw error;
@@ -495,8 +514,9 @@ class WhatsAppClient extends EventEmitter {
495
514
  };
496
515
  }
497
516
 
498
- // Send the message with buttons
499
- return await this.sock.sendMessage(chatId, messageContent, extraOptions);
517
+ // Use the safe sender to send the buttons with idempotency
518
+ const idemKey = buildIdemKey(chatId, messageContent);
519
+ return await this.guard.send(this.sock.sendMessage.bind(this.sock), chatId, messageContent, extraOptions, idemKey);
500
520
  } catch (error) {
501
521
  console.error('Error sending buttons:', error);
502
522
  throw error;
@@ -536,7 +556,9 @@ class WhatsAppClient extends EventEmitter {
536
556
  })),
537
557
  };
538
558
 
539
- return await this.sock.sendMessage(chatId, listMessage);
559
+ // Use the safe sender to send the list with idempotency
560
+ const idemKey = buildIdemKey(chatId, listMessage);
561
+ return await this.guard.send(this.sock.sendMessage.bind(this.sock), chatId, listMessage, undefined, idemKey);
540
562
  } catch (error) {
541
563
  console.error('Error sending list message:', error);
542
564
  throw error;
@@ -562,8 +584,7 @@ class WhatsAppClient extends EventEmitter {
562
584
  // Read the local image as Buffer
563
585
  const bufferLocalFile = fs.readFileSync(imgpath);
564
586
 
565
- // Send message with external ad reply using local image
566
- await this.sock.sendMessage(number, {
587
+ const adReplyMessage = {
567
588
  text: msg,
568
589
  contextInfo: {
569
590
  externalAdReply: {
@@ -578,7 +599,11 @@ class WhatsAppClient extends EventEmitter {
578
599
  mediaUrl: sourceurl || 'https://m.facebook.com/innovatorssoft'
579
600
  }
580
601
  }
581
- });
602
+ };
603
+
604
+ // Use the safe sender to send the ad reply with idempotency
605
+ const idemKey = buildIdemKey(number, adReplyMessage);
606
+ await this.guard.send(this.sock.sendMessage.bind(this.sock), number, adReplyMessage, undefined, idemKey);
582
607
 
583
608
  } catch (err) {
584
609
  console.error('Failed to send externalAdReply:', err);
@@ -648,6 +673,7 @@ class WhatsAppClient extends EventEmitter {
648
673
  }
649
674
 
650
675
  try {
676
+ // For readMessages, we don't need idempotency since it's a read operation
651
677
  await this.sock.readMessages([messageId]);
652
678
  } catch (error) {
653
679
  console.error('Error marking message as read:', error);
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "innovators-bot2",
3
- "version": "1.1.2",
3
+ "version": "1.1.4",
4
4
  "description": "",
5
5
  "main": "index.js",
6
6
  "scripts": {
@@ -29,7 +29,7 @@
29
29
  },
30
30
  "homepage": "https://github.com/innovatorssoft/WhatsAppAPI?tab=readme-ov-file#whatsapp-api",
31
31
  "dependencies": {
32
- "@itsukichan/baileys": "github:Itsukichann/Baileys",
32
+ "@itsukichan/baileys": "github:WhiskeySockets/Baileys",
33
33
  "link-preview-js": "^3.0.13",
34
34
  "mime-types": "^2.1.35",
35
35
  "pino": "^9.6.0",
package/utils.js ADDED
@@ -0,0 +1,18 @@
1
+ const crypto = require('crypto');
2
+
3
+ function hashContent(content) {
4
+ return crypto.createHash("sha256")
5
+ .update(JSON.stringify(content))
6
+ .digest("hex")
7
+ .slice(0, 16); // short is fine
8
+ }
9
+
10
+ // Same message retried? Same idemKey.
11
+ function buildIdemKey(jid, content) {
12
+ return `wa:${jid}:${hashContent(content)}`;
13
+ }
14
+
15
+ module.exports = {
16
+ hashContent,
17
+ buildIdemKey
18
+ };