ishumdz-bail 1.0.17 → 1.0.18
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 +106 -0
- package/lib/Utils/index.js +3 -0
- package/lib/Utils/messages.js +110 -31
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -83,6 +83,7 @@ sock.ev.on('creds.update', saveCreds)
|
|
|
83
83
|
| 🔄 **Classify Disconnect** | Maps every WA disconnect code to `{ category, shouldReconnect, backoffMs }` — properly handles code 515 (restartRequired) as recoverable, not fatal |
|
|
84
84
|
| ⏱️ **Rate Limiter** | Anti-spam pacing calculator: enforces per-minute/hour/day caps, burst allowance, new-chat delays, identical-message dedup |
|
|
85
85
|
| 🖼️ **Album Message** | Send multiple images/videos as a single WA album/grid, with per-item or top-level caption and `gifPlayback` support |
|
|
86
|
+
| 🔘 **Horizontal Buttons** | Modern native-flow buttons that render on ALL clients — text, image, video, GIF headers + classic location map-card style + automatic group conversion |
|
|
86
87
|
| 🤖 **AI Label Config** | `aiLabel: true/false` in socket config — controls whether outgoing messages carry the `biz_bot` attribute that WA uses to render the AI icon |
|
|
87
88
|
| 🔎 **USync Username Protocol** | Username resolution baked into USync queries — resolve WA usernames alongside contacts in a single round-trip |
|
|
88
89
|
| 📥 **Offline Node Processor** | Batch-processes pending stanzas received while offline, preventing message loss on reconnect |
|
|
@@ -413,6 +414,108 @@ await sock.sendMessage(jid, {
|
|
|
413
414
|
|
|
414
415
|
---
|
|
415
416
|
|
|
417
|
+
## 🔘 Horizontal Buttons (Interactive Messages)
|
|
418
|
+
|
|
419
|
+
> ⚡ **Fixed in this fork:** classic text-only `buttonsMessage` is rejected by new WhatsApp
|
|
420
|
+
> clients ("Your version of WhatsApp doesn't support it"). The `buttons` API now sends
|
|
421
|
+
> **native flow (interactiveMessage)** buttons that render on **all modern clients** —
|
|
422
|
+
> normal messages, images, videos and GIFs. Location keeps the classic map-card style.
|
|
423
|
+
|
|
424
|
+
### Text + Horizontal Buttons
|
|
425
|
+
|
|
426
|
+
```javascript
|
|
427
|
+
await sock.sendMessage(jid, {
|
|
428
|
+
text: 'Pong 9 Ms ⚡\nDeveloper: ISHAN-X × LOVELY',
|
|
429
|
+
footer: 'ISHAN-X MD PRO',
|
|
430
|
+
buttons: [
|
|
431
|
+
{ buttonText: { displayText: 'menu' }, buttonId: 'menu', type: 1 },
|
|
432
|
+
{ buttonText: { displayText: 'owner' }, buttonId: 'owner', type: 1 }
|
|
433
|
+
]
|
|
434
|
+
})
|
|
435
|
+
```
|
|
436
|
+
|
|
437
|
+
### Image / Video / GIF + Buttons
|
|
438
|
+
|
|
439
|
+
```javascript
|
|
440
|
+
// Image header
|
|
441
|
+
await sock.sendMessage(jid, {
|
|
442
|
+
image: { url: './banner.jpg' },
|
|
443
|
+
caption: 'Welcome!',
|
|
444
|
+
footer: 'My Bot',
|
|
445
|
+
buttons: [
|
|
446
|
+
{ buttonText: { displayText: 'menu' }, buttonId: 'menu', type: 1 },
|
|
447
|
+
{ buttonText: { displayText: 'owner' }, buttonId: 'owner', type: 1 }
|
|
448
|
+
]
|
|
449
|
+
})
|
|
450
|
+
|
|
451
|
+
// GIF header (animated, loops like a GIF)
|
|
452
|
+
await sock.sendMessage(jid, {
|
|
453
|
+
gif: { url: './animation.gif' },
|
|
454
|
+
caption: 'Live preview!',
|
|
455
|
+
buttons: [ /* ... */ ]
|
|
456
|
+
})
|
|
457
|
+
|
|
458
|
+
// Video header (gifPlayback = loop like a GIF)
|
|
459
|
+
await sock.sendMessage(jid, {
|
|
460
|
+
video: { url: './clip.mp4' },
|
|
461
|
+
gifPlayback: true,
|
|
462
|
+
caption: 'Watch this',
|
|
463
|
+
buttons: [ /* ... */ ]
|
|
464
|
+
})
|
|
465
|
+
```
|
|
466
|
+
|
|
467
|
+
### Location Map-Card + Buttons (classic style, still renders on new clients)
|
|
468
|
+
|
|
469
|
+
```javascript
|
|
470
|
+
await sock.sendMessage(jid, {
|
|
471
|
+
location: {
|
|
472
|
+
degreesLatitude: 9.9312,
|
|
473
|
+
degreesLongitude: 76.2673,
|
|
474
|
+
name: 'ISHAN-X MD PRO', // bold title under the map
|
|
475
|
+
address: 'Select an option below', // 📍 subtitle
|
|
476
|
+
// jpegThumbnail: buffer — custom map image (optional)
|
|
477
|
+
},
|
|
478
|
+
text: 'Pong 9 Ms ⚡',
|
|
479
|
+
buttons: [
|
|
480
|
+
{ buttonText: { displayText: 'menu' }, buttonId: 'menu', type: 1 },
|
|
481
|
+
{ buttonText: { displayText: 'owner' }, buttonId: 'owner', type: 1 }
|
|
482
|
+
]
|
|
483
|
+
})
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Handling Button Clicks
|
|
487
|
+
|
|
488
|
+
Button replies arrive in `messages.upsert` — native flow uses
|
|
489
|
+
`interactiveResponseMessage`, classic uses `buttonsResponseMessage`:
|
|
490
|
+
|
|
491
|
+
```javascript
|
|
492
|
+
sock.ev.on('messages.upsert', async ({ messages, type }) => {
|
|
493
|
+
if (type !== 'notify') return
|
|
494
|
+
const msg = messages[0]
|
|
495
|
+
if (!msg.message || msg.key.fromMe) return
|
|
496
|
+
|
|
497
|
+
let clickedId = msg.message.buttonsResponseMessage?.selectedButtonId
|
|
498
|
+
const native = msg.message.interactiveResponseMessage?.nativeFlowResponseMessage
|
|
499
|
+
if (!clickedId && native?.paramsJson) {
|
|
500
|
+
try { clickedId = JSON.parse(native.paramsJson).id } catch {}
|
|
501
|
+
}
|
|
502
|
+
|
|
503
|
+
if (clickedId === 'menu') await sock.sendMessage(msg.key.remoteJid, { text: '📋 MENU...' })
|
|
504
|
+
if (clickedId === 'owner') await sock.sendMessage(msg.key.remoteJid, { text: '👑 Owner...' })
|
|
505
|
+
})
|
|
506
|
+
```
|
|
507
|
+
|
|
508
|
+
> **Notes**
|
|
509
|
+
> - Groups: `listMessage` / `buttonsMessage` / `templateMessage` are auto-converted to
|
|
510
|
+
> interactive native flow on send — buttons work in groups too.
|
|
511
|
+
> - WhatsApp Web and very old clients don't render native flow cards — mobile clients
|
|
512
|
+
> (recent Android/iOS) render them fully.
|
|
513
|
+
> - Full working demo: [`examples/ping-buttons.js`](examples/ping-buttons.js)
|
|
514
|
+
> (`.ping` → image + horizontal buttons + click handling) and a visual preview in
|
|
515
|
+
> [`banner-preview.html`](banner-preview.html).
|
|
516
|
+
|
|
517
|
+
---
|
|
518
|
+
|
|
416
519
|
## 🚫 Ban Checker
|
|
417
520
|
|
|
418
521
|
4-factor ban detection against WhatsApp's own servers — no third-party API:
|
|
@@ -576,6 +679,9 @@ sock.ev.on('connection.update', ({ connection, lastDisconnect }) => {
|
|
|
576
679
|
|
|
577
680
|
Anti-ban pacing calculator — wire into your send path before each `sendMessage`:
|
|
578
681
|
|
|
682
|
+
> ⚡ **Fixed in this fork:** `RateLimiter` and `classifyDisconnect` are now properly
|
|
683
|
+
> exported from the package root (the files existed but were never re-exported).
|
|
684
|
+
|
|
579
685
|
```javascript
|
|
580
686
|
import { RateLimiter } from 'ishumdz-bail'
|
|
581
687
|
|
package/lib/Utils/index.js
CHANGED
|
@@ -20,4 +20,7 @@ export * from './companion-reg-client-utils.js';
|
|
|
20
20
|
export * from './identity-change-handler.js';
|
|
21
21
|
export * from './stanza-ack.js';
|
|
22
22
|
export * from './message-composer.js';
|
|
23
|
+
// PATCH: export anti-ban utilities (files existed but were never re-exported)
|
|
24
|
+
export * from './rate-limiter.js';
|
|
25
|
+
export * from './classify-disconnect.js';
|
|
23
26
|
//# sourceMappingURL=index.js.map
|
package/lib/Utils/messages.js
CHANGED
|
@@ -589,48 +589,127 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
589
589
|
m = { interactiveMessage };
|
|
590
590
|
}
|
|
591
591
|
else if ('buttonsMessage' in message && !!message.buttonsMessage) {
|
|
592
|
-
// PATCH: raw buttonsMessage
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
//
|
|
597
|
-
|
|
592
|
+
// PATCH: raw buttonsMessage — media/location headers keep classic pass-through
|
|
593
|
+
// (new WhatsApp clients still render those fine, e.g. location+menu cards).
|
|
594
|
+
// Text-only ButtonsMessage is rejected by new clients ("version doesn't
|
|
595
|
+
// support it"), so convert it to interactiveMessage (native flow
|
|
596
|
+
// quick_reply buttons) which renders on all modern clients.
|
|
597
|
+
const bMsg = message.buttonsMessage;
|
|
598
|
+
const hasMediaHeader = !!(bMsg.imageMessage || bMsg.videoMessage || bMsg.documentMessage || bMsg.locationMessage);
|
|
599
|
+
if (hasMediaHeader) {
|
|
600
|
+
m = { buttonsMessage: proto.Message.ButtonsMessage.create(bMsg) };
|
|
601
|
+
} else {
|
|
602
|
+
const flowButtons = (bMsg.buttons || []).map(btn => ({
|
|
603
|
+
name: 'quick_reply',
|
|
604
|
+
buttonParamsJson: JSON.stringify({
|
|
605
|
+
display_text: btn.buttonText?.displayText || btn.buttonText || '',
|
|
606
|
+
id: btn.buttonId || btn.buttonText?.displayText || ''
|
|
607
|
+
})
|
|
608
|
+
}));
|
|
609
|
+
const interactive = {
|
|
610
|
+
nativeFlowMessage: {
|
|
611
|
+
buttons: flowButtons,
|
|
612
|
+
messageParamsJson: '',
|
|
613
|
+
messageVersion: 1
|
|
614
|
+
},
|
|
615
|
+
body: { text: bMsg.contentText || bMsg.text || '' },
|
|
616
|
+
footer: bMsg.footerText ? { text: bMsg.footerText } : undefined
|
|
617
|
+
};
|
|
618
|
+
applyContextInfoAndMentions(interactive, { contextInfo: bMsg.contextInfo });
|
|
619
|
+
m = { interactiveMessage: interactive };
|
|
620
|
+
}
|
|
598
621
|
}
|
|
599
622
|
else if ('buttons' in message && !!message.buttons) {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
623
|
+
// PATCH: `buttons` API — text/caption/img/video builds interactiveMessage
|
|
624
|
+
// (native flow) so the horizontal button row renders on ALL modern
|
|
625
|
+
// WhatsApp clients (classic text-only buttonsMessage is rejected by new
|
|
626
|
+
// clients: "version doesn't support it").
|
|
627
|
+
// `location` keeps the classic map-card style (LOCATION header 6) —
|
|
628
|
+
// big map header + title + body + horizontal buttons below, still
|
|
629
|
+
// rendered by modern clients.
|
|
630
|
+
if ('location' in message && !!message.location) {
|
|
631
|
+
const loc = message.location;
|
|
632
|
+
const buttonsMessage = {
|
|
633
|
+
buttons: message.buttons.map(b => ({ ...b, type: proto.Message.ButtonsMessage.Button.Type.RESPONSE })),
|
|
634
|
+
contentText: message.text || message.caption || '',
|
|
635
|
+
headerType: ButtonType.LOCATION,
|
|
636
|
+
locationMessage: {
|
|
637
|
+
degreesLatitude: loc.degreesLatitude ?? loc.latitude ?? 0,
|
|
638
|
+
degreesLongitude: loc.degreesLongitude ?? loc.longitude ?? 0,
|
|
639
|
+
name: loc.name ?? null,
|
|
640
|
+
address: loc.address ?? null,
|
|
641
|
+
...(loc.jpegThumbnail ? { jpegThumbnail: loc.jpegThumbnail } : {})
|
|
642
|
+
}
|
|
643
|
+
};
|
|
644
|
+
if ('footer' in message && !!message.footer) {
|
|
645
|
+
buttonsMessage.footerText = message.footer;
|
|
646
|
+
}
|
|
647
|
+
if (message.contextInfo || message.mentions) {
|
|
648
|
+
buttonsMessage.contextInfo = {
|
|
649
|
+
...(message.contextInfo || {}),
|
|
650
|
+
...(message.mentions ? { mentionedJid: message.mentions } : {})
|
|
651
|
+
};
|
|
652
|
+
}
|
|
653
|
+
m = { buttonsMessage: proto.Message.ButtonsMessage.create(buttonsMessage) };
|
|
606
654
|
}
|
|
607
655
|
else {
|
|
608
|
-
|
|
609
|
-
|
|
656
|
+
const flowButtons = message.buttons.map(b => ({
|
|
657
|
+
name: 'quick_reply',
|
|
658
|
+
buttonParamsJson: JSON.stringify({
|
|
659
|
+
display_text: b.buttonText?.displayText || b.displayText || b.text || '',
|
|
660
|
+
id: b.buttonId || b.id || b.buttonText?.displayText || ''
|
|
661
|
+
})
|
|
662
|
+
}));
|
|
663
|
+
const interactiveMessage = {
|
|
664
|
+
nativeFlowMessage: {
|
|
665
|
+
buttons: flowButtons,
|
|
666
|
+
messageParamsJson: '',
|
|
667
|
+
messageVersion: 1
|
|
668
|
+
}
|
|
669
|
+
};
|
|
670
|
+
if ('text' in message) {
|
|
671
|
+
interactiveMessage.body = { text: message.text };
|
|
672
|
+
} else if ('caption' in message) {
|
|
673
|
+
interactiveMessage.body = { text: message.caption };
|
|
674
|
+
}
|
|
675
|
+
if ('title' in message && !!message.title) {
|
|
676
|
+
interactiveMessage.header = {
|
|
677
|
+
title: message.title,
|
|
678
|
+
subtitle: message.subtitle || '',
|
|
679
|
+
hasMediaAttachment: false
|
|
680
|
+
};
|
|
610
681
|
}
|
|
611
|
-
// PATCH: prepare media headers for caption+buttons (fork bug fix).
|
|
612
|
-
// The media branch never ran before this point, so m is empty and
|
|
613
|
-
// getContentType(m) crashed. Build the header right here instead.
|
|
614
682
|
if ('video' in message && !!message.video) {
|
|
615
683
|
const media = await prepareWAMessageMedia({ video: message.video }, options);
|
|
616
|
-
|
|
617
|
-
|
|
618
|
-
|
|
684
|
+
interactiveMessage.header = {
|
|
685
|
+
...(interactiveMessage.header || {}),
|
|
686
|
+
videoMessage: media.videoMessage,
|
|
687
|
+
hasMediaAttachment: true
|
|
688
|
+
};
|
|
689
|
+
if (message.gifPlayback) interactiveMessage.header.videoMessage.gifPlayback = true;
|
|
690
|
+
} else if ('gif' in message && !!message.gif) {
|
|
691
|
+
// PATCH: direct `gif` key — animated GIF header (video with gifPlayback)
|
|
692
|
+
const media = await prepareWAMessageMedia({ video: message.gif }, options);
|
|
693
|
+
interactiveMessage.header = {
|
|
694
|
+
...(interactiveMessage.header || {}),
|
|
695
|
+
videoMessage: media.videoMessage,
|
|
696
|
+
hasMediaAttachment: true
|
|
697
|
+
};
|
|
698
|
+
interactiveMessage.header.videoMessage.gifPlayback = true;
|
|
619
699
|
} else if ('image' in message && !!message.image) {
|
|
620
700
|
const media = await prepareWAMessageMedia({ image: message.image }, options);
|
|
621
|
-
|
|
622
|
-
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
buttonsMessage.headerType = ButtonType[type];
|
|
627
|
-
Object.assign(buttonsMessage, m);
|
|
701
|
+
interactiveMessage.header = {
|
|
702
|
+
...(interactiveMessage.header || {}),
|
|
703
|
+
imageMessage: media.imageMessage,
|
|
704
|
+
hasMediaAttachment: true
|
|
705
|
+
};
|
|
628
706
|
}
|
|
707
|
+
if ('footer' in message && !!message.footer) {
|
|
708
|
+
interactiveMessage.footer = { text: message.footer };
|
|
709
|
+
}
|
|
710
|
+
applyContextInfoAndMentions(interactiveMessage, message);
|
|
711
|
+
m = { interactiveMessage };
|
|
629
712
|
}
|
|
630
|
-
if ('footer' in message && !!message.footer) {
|
|
631
|
-
buttonsMessage.footerText = message.footer;
|
|
632
|
-
}
|
|
633
|
-
m = { buttonsMessage };
|
|
634
713
|
}
|
|
635
714
|
else if (hasNonNullishProperty(message, 'sections') || hasNonNullishProperty(message, 'productList')) {
|
|
636
715
|
const isProductList = hasNonNullishProperty(message, 'productList');
|