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 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
 
@@ -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
@@ -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 pass-through — classic horizontal buttons (reference: Case X Plugin case.js).
593
- // Fully-formed ButtonsMessage with any header (location/image/video/
594
- // document), contentText, footerText, headerType and buttons[].
595
- // type:1 buttons render in one horizontal row; nativeFlowInfo
596
- // single_select opens a list sheet on tap.
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
- const buttonsMessage = {
601
- buttons: message.buttons.map(b => ({ ...b, type: proto.Message.ButtonsMessage.Button.Type.RESPONSE }))
602
- };
603
- if ('text' in message) {
604
- buttonsMessage.contentText = message.text;
605
- buttonsMessage.headerType = ButtonType.EMPTY;
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
- if ('caption' in message) {
609
- buttonsMessage.contentText = message.caption;
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
- const contentType = getContentType(m);
625
- const type = contentType.replace('Message', '').toUpperCase();
626
- buttonsMessage.headerType = ButtonType[type];
627
- Object.assign(buttonsMessage, m);
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');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ishumdz-bail",
3
- "version": "1.0.17",
3
+ "version": "1.0.19",
4
4
  "description": "WhatsApp API Modification By Lovely",
5
5
  "keywords": [
6
6
  "whatsapp",