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 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
 
@@ -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
@@ -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 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.
597
- m = { buttonsMessage: proto.Message.ButtonsMessage.create(message.buttonsMessage) };
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
- 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;
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
- if ('caption' in message) {
609
- buttonsMessage.contentText = message.caption;
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
- buttonsMessage.videoMessage = media.videoMessage;
617
- if (message.gifPlayback) buttonsMessage.videoMessage.gifPlayback = true;
618
- buttonsMessage.headerType = ButtonType.VIDEO;
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
- buttonsMessage.imageMessage = media.imageMessage;
622
- buttonsMessage.headerType = ButtonType.IMAGE;
623
- } else {
624
- const contentType = getContentType(m);
625
- const type = contentType.replace('Message', '').toUpperCase();
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');
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "ishumdz-bail",
3
- "version": "1.0.17",
3
+ "version": "1.0.18",
4
4
  "description": "WhatsApp API Modification By Lovely",
5
5
  "keywords": [
6
6
  "whatsapp",