ishumdz-bail 1.0.17 → 1.0.19
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 +123 -0
- package/lib/Utils/index.js +3 -0
- package/lib/Utils/messages.js +107 -24
- 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,125 @@ await sock.sendMessage(jid, {
|
|
|
413
414
|
|
|
414
415
|
---
|
|
415
416
|
|
|
417
|
+
## 🔘 Horizontal Buttons (Interactive Messages)
|
|
418
|
+
|
|
419
|
+
> ⚡ **How it renders:** the `buttons` API sends the **classic ButtonsMessage** — the
|
|
420
|
+
> only format WhatsApp renders as a true **horizontal button row**. Modern clients
|
|
421
|
+
> render it when the message has a **header** (image / video / GIF / location map-card).
|
|
422
|
+
> A **location header needs no media upload** — coordinates alone give the big map
|
|
423
|
+
> card + horizontal row, and a **custom image or GIF** works with one media upload.
|
|
424
|
+
> Set `{ nativeFlow: true }` for vertical native-flow quick_reply buttons instead.
|
|
425
|
+
|
|
426
|
+
### Text + Horizontal Buttons (location header = zero uploads)
|
|
427
|
+
|
|
428
|
+
```javascript
|
|
429
|
+
await sock.sendMessage(jid, {
|
|
430
|
+
location: { degreesLatitude: 9.9312, degreesLongitude: 76.2673 }, // simple map card
|
|
431
|
+
text: 'Pong 9 Ms ⚡\nDeveloper: ISHAN-X × LOVELY',
|
|
432
|
+
footer: 'ISHAN-X MD PRO',
|
|
433
|
+
buttons: [
|
|
434
|
+
{ buttonText: { displayText: 'menu' }, buttonId: 'menu', type: 1 },
|
|
435
|
+
{ buttonText: { displayText: 'owner' }, buttonId: 'owner', type: 1 }
|
|
436
|
+
]
|
|
437
|
+
})
|
|
438
|
+
```
|
|
439
|
+
|
|
440
|
+
### Custom Pic / GIF + Horizontal Buttons
|
|
441
|
+
|
|
442
|
+
```javascript
|
|
443
|
+
// Image header (one media upload, then horizontal row)
|
|
444
|
+
await sock.sendMessage(jid, {
|
|
445
|
+
image: { url: './banner.jpg' },
|
|
446
|
+
caption: 'Welcome!',
|
|
447
|
+
footer: 'My Bot',
|
|
448
|
+
buttons: [
|
|
449
|
+
{ buttonText: { displayText: 'menu' }, buttonId: 'menu', type: 1 },
|
|
450
|
+
{ buttonText: { displayText: 'owner' }, buttonId: 'owner', type: 1 }
|
|
451
|
+
]
|
|
452
|
+
})
|
|
453
|
+
|
|
454
|
+
// Animated GIF header
|
|
455
|
+
await sock.sendMessage(jid, {
|
|
456
|
+
gif: { url: './animation.gif' },
|
|
457
|
+
caption: 'Live preview!',
|
|
458
|
+
buttons: [ /* ... */ ]
|
|
459
|
+
})
|
|
460
|
+
|
|
461
|
+
// Video header (gifPlayback = loop like a GIF)
|
|
462
|
+
await sock.sendMessage(jid, {
|
|
463
|
+
video: { url: './clip.mp4' },
|
|
464
|
+
gifPlayback: true,
|
|
465
|
+
caption: 'Watch this',
|
|
466
|
+
buttons: [ /* ... */ ]
|
|
467
|
+
})
|
|
468
|
+
```
|
|
469
|
+
|
|
470
|
+
### Location Map-Card + Buttons (custom name/address)
|
|
471
|
+
|
|
472
|
+
```javascript
|
|
473
|
+
await sock.sendMessage(jid, {
|
|
474
|
+
location: {
|
|
475
|
+
degreesLatitude: 9.9312,
|
|
476
|
+
degreesLongitude: 76.2673,
|
|
477
|
+
name: 'ISHAN-X MD PRO', // bold title under the map
|
|
478
|
+
address: 'Select an option below', // 📍 subtitle
|
|
479
|
+
// jpegThumbnail: buffer — custom map image (optional)
|
|
480
|
+
},
|
|
481
|
+
text: 'Pong 9 Ms ⚡',
|
|
482
|
+
buttons: [ /* ... */ ]
|
|
483
|
+
})
|
|
484
|
+
```
|
|
485
|
+
|
|
486
|
+
### Text-only (TEXT header, best-effort) & native flow escape hatch
|
|
487
|
+
|
|
488
|
+
```javascript
|
|
489
|
+
// text-only: TEXT headerType — some clients still show the row vertically;
|
|
490
|
+
// for a guaranteed horizontal row attach image/gif/location as above.
|
|
491
|
+
await sock.sendMessage(jid, {
|
|
492
|
+
text: 'Pong 9 Ms ⚡', footer: 'ISHAN-X',
|
|
493
|
+
buttons: [ { buttonText: { displayText: 'menu' }, buttonId: 'menu', type: 1 } ]
|
|
494
|
+
})
|
|
495
|
+
|
|
496
|
+
// vertical native-flow quick_reply buttons (renders on all modern clients)
|
|
497
|
+
await sock.sendMessage(jid, {
|
|
498
|
+
text: 'Menu?', nativeFlow: true,
|
|
499
|
+
buttons: [ { buttonText: { displayText: 'menu' }, buttonId: 'menu', type: 1 } ]
|
|
500
|
+
})
|
|
501
|
+
```
|
|
502
|
+
|
|
503
|
+
### Handling Button Clicks
|
|
504
|
+
|
|
505
|
+
Button replies arrive in `messages.upsert` — native flow uses
|
|
506
|
+
`interactiveResponseMessage`, classic uses `buttonsResponseMessage`:
|
|
507
|
+
|
|
508
|
+
```javascript
|
|
509
|
+
sock.ev.on('messages.upsert', async ({ messages, type }) => {
|
|
510
|
+
if (type !== 'notify') return
|
|
511
|
+
const msg = messages[0]
|
|
512
|
+
if (!msg.message || msg.key.fromMe) return
|
|
513
|
+
|
|
514
|
+
let clickedId = msg.message.buttonsResponseMessage?.selectedButtonId
|
|
515
|
+
const native = msg.message.interactiveResponseMessage?.nativeFlowResponseMessage
|
|
516
|
+
if (!clickedId && native?.paramsJson) {
|
|
517
|
+
try { clickedId = JSON.parse(native.paramsJson).id } catch {}
|
|
518
|
+
}
|
|
519
|
+
|
|
520
|
+
if (clickedId === 'menu') await sock.sendMessage(msg.key.remoteJid, { text: '📋 MENU...' })
|
|
521
|
+
if (clickedId === 'owner') await sock.sendMessage(msg.key.remoteJid, { text: '👑 Owner...' })
|
|
522
|
+
})
|
|
523
|
+
```
|
|
524
|
+
|
|
525
|
+
> **Notes**
|
|
526
|
+
> - Groups: `listMessage` / `buttonsMessage` / `templateMessage` are auto-converted to
|
|
527
|
+
> interactive native flow on send — buttons work in groups too.
|
|
528
|
+
> - WhatsApp Web and very old clients don't render native flow cards — mobile clients
|
|
529
|
+
> (recent Android/iOS) render them fully.
|
|
530
|
+
> - Full working demo: [`examples/ping-buttons.js`](examples/ping-buttons.js)
|
|
531
|
+
> (`.ping` → image + horizontal buttons + click handling) and a visual preview in
|
|
532
|
+
> [`banner-preview.html`](banner-preview.html).
|
|
533
|
+
|
|
534
|
+
---
|
|
535
|
+
|
|
416
536
|
## 🚫 Ban Checker
|
|
417
537
|
|
|
418
538
|
4-factor ban detection against WhatsApp's own servers — no third-party API:
|
|
@@ -576,6 +696,9 @@ sock.ev.on('connection.update', ({ connection, lastDisconnect }) => {
|
|
|
576
696
|
|
|
577
697
|
Anti-ban pacing calculator — wire into your send path before each `sendMessage`:
|
|
578
698
|
|
|
699
|
+
> ⚡ **Fixed in this fork:** `RateLimiter` and `classifyDisconnect` are now properly
|
|
700
|
+
> exported from the package root (the files existed but were never re-exported).
|
|
701
|
+
|
|
579
702
|
```javascript
|
|
580
703
|
import { RateLimiter } from 'ishumdz-bail'
|
|
581
704
|
|
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
|
@@ -517,6 +517,13 @@ const applyContextInfoAndMentions = (interactiveMessage, message) => {
|
|
|
517
517
|
}
|
|
518
518
|
}
|
|
519
519
|
}
|
|
520
|
+
// PATCH: 1x1 transparent PNG — text-only buttons attach this as an invisible
|
|
521
|
+
// IMAGE header so modern clients render the horizontal row (classic requires
|
|
522
|
+
// a header), with no map card and no "version doesn't support it" error.
|
|
523
|
+
const BUTTONS_INVISIBLE_PNG = () => Buffer.from(
|
|
524
|
+
'iVBORw0KGgoAAAANSUhEUgAAAAEAAAABCAQAAAC1HAwCAAAAC0lEQVR42mNkYAAAAAYAAjCB0C8AAAAASUVORK5CYII=',
|
|
525
|
+
'base64'
|
|
526
|
+
)
|
|
520
527
|
export const generateWAMessageContent = async (message, options) => {
|
|
521
528
|
var _a, _b;
|
|
522
529
|
let m = {};
|
|
@@ -589,48 +596,124 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
589
596
|
m = { interactiveMessage };
|
|
590
597
|
}
|
|
591
598
|
else if ('buttonsMessage' in message && !!message.buttonsMessage) {
|
|
592
|
-
// PATCH: raw buttonsMessage
|
|
593
|
-
//
|
|
594
|
-
//
|
|
595
|
-
//
|
|
596
|
-
//
|
|
599
|
+
// PATCH: raw buttonsMessage — always classic pass-through. Modern clients
|
|
600
|
+
// render the horizontal row when a header exists (image/video/document/
|
|
601
|
+
// location) or when a thumbnail header is attached; text-only without any
|
|
602
|
+
// header is the "version doesn't support it" case. Add a header in that
|
|
603
|
+
// situation, or use the `buttons` API which auto-adds one.
|
|
597
604
|
m = { buttonsMessage: proto.Message.ButtonsMessage.create(message.buttonsMessage) };
|
|
598
605
|
}
|
|
599
606
|
else if ('buttons' in message && !!message.buttons) {
|
|
600
|
-
|
|
601
|
-
|
|
602
|
-
|
|
603
|
-
|
|
604
|
-
|
|
605
|
-
|
|
607
|
+
// PATCH: `buttons` API — HORIZONTAL button row via classic ButtonsMessage
|
|
608
|
+
// with a header (modern clients only render classic horizontal buttons
|
|
609
|
+
// when a header exists: image/video/gif/location, or an embedded
|
|
610
|
+
// thumbnail for text-only). Set { nativeFlow: true } to opt out and get
|
|
611
|
+
// vertical native-flow quick_reply buttons instead.
|
|
612
|
+
if (message.nativeFlow === true) {
|
|
613
|
+
const flowButtons = message.buttons.map(b => ({
|
|
614
|
+
name: 'quick_reply',
|
|
615
|
+
buttonParamsJson: JSON.stringify({
|
|
616
|
+
display_text: b.buttonText?.displayText || b.displayText || b.text || '',
|
|
617
|
+
id: b.buttonId || b.id || b.buttonText?.displayText || ''
|
|
618
|
+
})
|
|
619
|
+
}));
|
|
620
|
+
const interactiveMessage = {
|
|
621
|
+
nativeFlowMessage: {
|
|
622
|
+
buttons: flowButtons,
|
|
623
|
+
messageParamsJson: '',
|
|
624
|
+
messageVersion: 1
|
|
625
|
+
}
|
|
626
|
+
};
|
|
627
|
+
if ('text' in message) {
|
|
628
|
+
interactiveMessage.body = { text: message.text };
|
|
629
|
+
} else if ('caption' in message) {
|
|
630
|
+
interactiveMessage.body = { text: message.caption };
|
|
631
|
+
}
|
|
632
|
+
if ('footer' in message && !!message.footer) {
|
|
633
|
+
interactiveMessage.footer = { text: message.footer };
|
|
634
|
+
}
|
|
635
|
+
applyContextInfoAndMentions(interactiveMessage, message);
|
|
636
|
+
m = { interactiveMessage };
|
|
637
|
+
}
|
|
638
|
+
else if ('location' in message && !!message.location) {
|
|
639
|
+
const loc = message.location;
|
|
640
|
+
const buttonsMessage = {
|
|
641
|
+
buttons: message.buttons.map(b => ({ ...b, type: proto.Message.ButtonsMessage.Button.Type.RESPONSE })),
|
|
642
|
+
contentText: message.text || message.caption || '',
|
|
643
|
+
headerType: ButtonType.LOCATION,
|
|
644
|
+
locationMessage: {
|
|
645
|
+
degreesLatitude: loc.degreesLatitude ?? loc.latitude ?? 0,
|
|
646
|
+
degreesLongitude: loc.degreesLongitude ?? loc.longitude ?? 0,
|
|
647
|
+
name: loc.name ?? null,
|
|
648
|
+
address: loc.address ?? null,
|
|
649
|
+
...(loc.jpegThumbnail ? { jpegThumbnail: loc.jpegThumbnail } : {})
|
|
650
|
+
}
|
|
651
|
+
};
|
|
652
|
+
if ('footer' in message && !!message.footer) {
|
|
653
|
+
buttonsMessage.footerText = message.footer;
|
|
654
|
+
}
|
|
655
|
+
if (message.contextInfo || message.mentions) {
|
|
656
|
+
buttonsMessage.contextInfo = {
|
|
657
|
+
...(message.contextInfo || {}),
|
|
658
|
+
...(message.mentions ? { mentionedJid: message.mentions } : {})
|
|
659
|
+
};
|
|
660
|
+
}
|
|
661
|
+
m = { buttonsMessage: proto.Message.ButtonsMessage.create(buttonsMessage) };
|
|
606
662
|
}
|
|
607
663
|
else {
|
|
608
|
-
|
|
609
|
-
|
|
664
|
+
const buttonsMessage = {
|
|
665
|
+
buttons: message.buttons.map(b => ({ ...b, type: proto.Message.ButtonsMessage.Button.Type.RESPONSE }))
|
|
666
|
+
};
|
|
667
|
+
if ('footer' in message && !!message.footer) {
|
|
668
|
+
buttonsMessage.footerText = message.footer;
|
|
669
|
+
}
|
|
670
|
+
if (message.contextInfo || message.mentions) {
|
|
671
|
+
buttonsMessage.contextInfo = {
|
|
672
|
+
...(message.contextInfo || {}),
|
|
673
|
+
...(message.mentions ? { mentionedJid: message.mentions } : {})
|
|
674
|
+
};
|
|
610
675
|
}
|
|
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
676
|
if ('video' in message && !!message.video) {
|
|
615
677
|
const media = await prepareWAMessageMedia({ video: message.video }, options);
|
|
616
678
|
buttonsMessage.videoMessage = media.videoMessage;
|
|
617
679
|
if (message.gifPlayback) buttonsMessage.videoMessage.gifPlayback = true;
|
|
618
680
|
buttonsMessage.headerType = ButtonType.VIDEO;
|
|
681
|
+
} else if ('gif' in message && !!message.gif) {
|
|
682
|
+
const media = await prepareWAMessageMedia({ video: message.gif }, options);
|
|
683
|
+
buttonsMessage.videoMessage = media.videoMessage;
|
|
684
|
+
buttonsMessage.videoMessage.gifPlayback = true;
|
|
685
|
+
buttonsMessage.headerType = ButtonType.VIDEO;
|
|
619
686
|
} else if ('image' in message && !!message.image) {
|
|
620
687
|
const media = await prepareWAMessageMedia({ image: message.image }, options);
|
|
621
688
|
buttonsMessage.imageMessage = media.imageMessage;
|
|
622
689
|
buttonsMessage.headerType = ButtonType.IMAGE;
|
|
623
690
|
} else {
|
|
624
|
-
|
|
625
|
-
|
|
626
|
-
|
|
627
|
-
|
|
691
|
+
if ('text' in message || 'caption' in message) {
|
|
692
|
+
const body = message.text ?? message.caption;
|
|
693
|
+
buttonsMessage.contentText = body;
|
|
694
|
+
// PATCH: text-only — invisible 1x1 PNG header so the horizontal
|
|
695
|
+
// row renders on modern clients without a map card and without
|
|
696
|
+
// the "doesn't support it" error. Upload failure falls back to
|
|
697
|
+
// the TEXT header (may render vertical, still no error).
|
|
698
|
+
try {
|
|
699
|
+
const media = await prepareWAMessageMedia({ image: BUTTONS_INVISIBLE_PNG() }, options);
|
|
700
|
+
buttonsMessage.imageMessage = media.imageMessage;
|
|
701
|
+
buttonsMessage.headerType = ButtonType.IMAGE;
|
|
702
|
+
} catch {
|
|
703
|
+
buttonsMessage.text = (message.title || String(body).split('\n')[0] || '').slice(0, 60);
|
|
704
|
+
buttonsMessage.headerType = ButtonType.TEXT;
|
|
705
|
+
}
|
|
706
|
+
} else {
|
|
707
|
+
buttonsMessage.headerType = ButtonType.EMPTY;
|
|
708
|
+
}
|
|
628
709
|
}
|
|
710
|
+
if ('text' in message && (buttonsMessage.videoMessage || buttonsMessage.imageMessage)) {
|
|
711
|
+
// media header already carries the caption
|
|
712
|
+
if (buttonsMessage.videoMessage && 'caption' in message) buttonsMessage.videoMessage.caption = message.caption;
|
|
713
|
+
if (buttonsMessage.imageMessage && 'caption' in message) buttonsMessage.imageMessage.caption = message.caption;
|
|
714
|
+
}
|
|
715
|
+
m = { buttonsMessage: proto.Message.ButtonsMessage.create(buttonsMessage) };
|
|
629
716
|
}
|
|
630
|
-
if ('footer' in message && !!message.footer) {
|
|
631
|
-
buttonsMessage.footerText = message.footer;
|
|
632
|
-
}
|
|
633
|
-
m = { buttonsMessage };
|
|
634
717
|
}
|
|
635
718
|
else if (hasNonNullishProperty(message, 'sections') || hasNonNullishProperty(message, 'productList')) {
|
|
636
719
|
const isProductList = hasNonNullishProperty(message, 'productList');
|