ishumdz-bail 1.0.27 → 1.0.29
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 +62 -7
- package/lib/Pro/sheet-buttons.js +179 -0
- package/lib/Utils/messages.js +17 -51
- package/lib/index.js +4 -0
- package/package.json +1 -1
package/README.md
CHANGED
|
@@ -698,12 +698,15 @@ Action buttons (`url` / `copy` / `call`) never send a reply — nothing to handl
|
|
|
698
698
|
### Base Compatibility — cantarella / Case X Style Code (v1.0.27+)
|
|
699
699
|
|
|
700
700
|
Code copied from other Baileys bases (cantarella-baileys, Case X Plugin YOUHU MD, etc.)
|
|
701
|
-
runs without changes
|
|
701
|
+
runs without changes — and renders as a **horizontal one-line row** just like those
|
|
702
|
+
bases. Three shapes are accepted:
|
|
702
703
|
|
|
703
|
-
**1) Raw `buttonsMessage` classic menu card —
|
|
704
|
-
message that mixes `nativeFlowInfo` (single_select sheet) or `cta_*` buttons
|
|
705
|
-
plain buttons
|
|
706
|
-
|
|
704
|
+
**1) Raw `buttonsMessage` classic menu card — mixed extras are flattened.** A raw
|
|
705
|
+
classic message that mixes `nativeFlowInfo` (single_select sheet) or `cta_*` buttons
|
|
706
|
+
with plain buttons **stays classic horizontal**: the header (location/image/video)
|
|
707
|
+
is preserved, every extra becomes a plain button with its original `buttonId`, and
|
|
708
|
+
its click arrives as a normal `buttonsResponseMessage` — so your bot can send the
|
|
709
|
+
list sheet / URL action itself (e.g. a separate `nativeFlow: true` message):
|
|
707
710
|
|
|
708
711
|
```javascript
|
|
709
712
|
// Case X / cantarella style — works as-is:
|
|
@@ -745,8 +748,60 @@ await sock.sendMessage(jid, { interactiveMessage: { buttons: [{ name: 'quick_rep
|
|
|
745
748
|
|
|
746
749
|
**3) Plain raw `buttonsMessage`** (no native-flow extras) stays classic and gets the
|
|
747
750
|
automatic invisible-PNG header when you omit one. Flow params are accepted as
|
|
748
|
-
`paramsJson` or `buttonParamsJson
|
|
749
|
-
|
|
751
|
+
`paramsJson` or `buttonParamsJson`.
|
|
752
|
+
|
|
753
|
+
> 💡 **Want the tap-to-open sheet too?** Send it as a second message when the button
|
|
754
|
+
> is clicked (see `examples/menu-buttons.js` — the `allmenus` case). Mixing a sheet
|
|
755
|
+
> into the same classic row is what produced the "doesn't support it" bubble; the
|
|
756
|
+
> library keeps the row safe by flattening extras, and the two-message pattern gives
|
|
757
|
+
> you both layouts.
|
|
758
|
+
|
|
759
|
+
### One-Tap Horizontal Button → Automatic List Sheet — v1.0.28+
|
|
760
|
+
|
|
761
|
+
`attachSheetAutoReply` does the two-message pattern **for you**: register it once,
|
|
762
|
+
and when the user taps a horizontal button, the list sheet is sent automatically —
|
|
763
|
+
no extra code in your `messages.upsert` handler:
|
|
764
|
+
|
|
765
|
+
```javascript
|
|
766
|
+
const { makeWASocket, attachSheetAutoReply } = require('ishumdz-bail')
|
|
767
|
+
|
|
768
|
+
const sock = makeWASocket({ auth: state, ... })
|
|
769
|
+
|
|
770
|
+
// register ONCE, right after creating the socket:
|
|
771
|
+
attachSheetAutoReply(sock, {
|
|
772
|
+
// click id → full sheet (auto-sent, click never reaches your parser)
|
|
773
|
+
allmenus: {
|
|
774
|
+
text: '📂 *ALL MENUS* — tap a section 👇',
|
|
775
|
+
footer: 'ISHAN-X MD PRO',
|
|
776
|
+
buttonText: '📂 open menu',
|
|
777
|
+
sectionTitle: 'All Menus',
|
|
778
|
+
highlightLabel: '⭐ MAIN',
|
|
779
|
+
rows: [
|
|
780
|
+
{ title: '📥 Download Menu', description: 'Downloader commands', rowId: '.downloadmenu' },
|
|
781
|
+
{ title: '✨ AI Menu', description: 'AI commands', rowId: '.aimenu' },
|
|
782
|
+
{ title: '🎮 Game Menu', description: 'Fun & games', rowId: '.gamemenu' }
|
|
783
|
+
]
|
|
784
|
+
},
|
|
785
|
+
// shorthand: click id → rewrite into a normal command for your parser
|
|
786
|
+
support: '.owner'
|
|
787
|
+
})
|
|
788
|
+
|
|
789
|
+
// then your horizontal row stays 100% plain → guaranteed one-line render:
|
|
790
|
+
await sock.sendMessage(jid, {
|
|
791
|
+
location: { degreesLatitude: 0, degreesLongitude: 0,
|
|
792
|
+
name: 'ISHAN-X MD PRO', address: '📂 Select Menu 👇' },
|
|
793
|
+
text: menuText, footer,
|
|
794
|
+
buttons: [
|
|
795
|
+
{ buttonText: { displayText: '📂 allmenus' }, buttonId: 'allmenus', type: 1 },
|
|
796
|
+
{ buttonText: { displayText: '👤 owner' }, buttonId: 'owner', type: 1 },
|
|
797
|
+
{ buttonText: { displayText: '⚡ ping' }, buttonId: 'ping', type: 1 }
|
|
798
|
+
]
|
|
799
|
+
}, { quoted: msg })
|
|
800
|
+
// user taps "allmenus" → sheet opens instantly; taps "owner" → your parser gets '.owner'
|
|
801
|
+
```
|
|
802
|
+
|
|
803
|
+
Also exported: `sendSheet(sock, jid, entry, quoted)` (send a sheet manually) and
|
|
804
|
+
`decodeButtonClick(msg)` (the click bridge, shipped with the library).
|
|
750
805
|
|
|
751
806
|
---
|
|
752
807
|
|
|
@@ -0,0 +1,179 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* sheet-buttons.js — one-tap horizontal button → automatic list sheet (v1.0.28+)
|
|
3
|
+
*
|
|
4
|
+
* Classic horizontal rows cannot carry a native-flow sheet inside the SAME
|
|
5
|
+
* message on current clients (that mix renders as "version doesn't support
|
|
6
|
+
* it"). The working pattern is TWO messages: the horizontal row, and — when
|
|
7
|
+
* the user clicks the menu button — a native-flow list sheet sent
|
|
8
|
+
* automatically. This module does the second half for you:
|
|
9
|
+
*
|
|
10
|
+
* const { attachSheetAutoReply } = require('ishumdz-bail')
|
|
11
|
+
*
|
|
12
|
+
* attachSheetAutoReply(sock, {
|
|
13
|
+
* // click id → full sheet definition
|
|
14
|
+
* allmenus: {
|
|
15
|
+
* text: '📂 *ALL MENUS* — tap a section 👇',
|
|
16
|
+
* footer: 'ISHAN-X MD PRO',
|
|
17
|
+
* buttonText: '📂 open menu',
|
|
18
|
+
* highlightLabel: '⭐ MAIN',
|
|
19
|
+
* sectionTitle: 'All Menus',
|
|
20
|
+
* rows: [
|
|
21
|
+
* { title: '📥 Download Menu', description: 'Downloaders', rowId: '.downloadmenu' },
|
|
22
|
+
* { title: '✨ AI Menu', description: 'AI commands', rowId: '.aimenu' }
|
|
23
|
+
* ]
|
|
24
|
+
* },
|
|
25
|
+
* // or a shorthand: rewrite the click into another command
|
|
26
|
+
* support: '.owner'
|
|
27
|
+
* })
|
|
28
|
+
*
|
|
29
|
+
* Now a horizontal row can stay 100% plain (guaranteed one-line render):
|
|
30
|
+
*
|
|
31
|
+
* await sock.sendMessage(jid, {
|
|
32
|
+
* location: { degreesLatitude: 0, degreesLongitude: 0,
|
|
33
|
+
* name: 'ISHAN-X MD PRO', address: '📂 Select Menu 👇' },
|
|
34
|
+
* text: menuText, footer,
|
|
35
|
+
* buttons: [
|
|
36
|
+
* { buttonText: { displayText: '📂 allmenus' }, buttonId: 'allmenus', type: 1 },
|
|
37
|
+
* { buttonText: { displayText: '👤 owner' }, buttonId: 'owner', type: 1 }
|
|
38
|
+
* ]
|
|
39
|
+
* })
|
|
40
|
+
*
|
|
41
|
+
* User taps "allmenus" → the sheet opens instantly, and your own
|
|
42
|
+
* messages.upsert handler is NOT called for that click (consumed).
|
|
43
|
+
* A shorthand entry rewrites the click into the given command text and
|
|
44
|
+
* lets your normal command parser handle it.
|
|
45
|
+
*/
|
|
46
|
+
|
|
47
|
+
// Decode any button / sheet-row / list-row click into its id (same bridge
|
|
48
|
+
// the examples use, now shipped with the library).
|
|
49
|
+
export function decodeButtonClick(msg) {
|
|
50
|
+
// classic horizontal quick-reply buttons
|
|
51
|
+
const classic = msg?.message?.buttonsResponseMessage?.selectedButtonId
|
|
52
|
+
if (classic) return classic
|
|
53
|
+
|
|
54
|
+
// native flow (sheet rows / card quick replies)
|
|
55
|
+
const nf = msg?.message?.interactiveResponseMessage?.nativeFlowResponseMessage
|
|
56
|
+
if (nf?.paramsJson) {
|
|
57
|
+
try {
|
|
58
|
+
const p = JSON.parse(nf.paramsJson)
|
|
59
|
+
return p.id || p.selectedId || p.selected_row_id || null
|
|
60
|
+
} catch { /* ignore */ }
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
// classic listMessage rows
|
|
64
|
+
const list = msg?.message?.listResponseMessage?.singleSelectReply?.selectedRowId
|
|
65
|
+
if (list) return list
|
|
66
|
+
|
|
67
|
+
return null
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
const normalizeRows = (rows) => (rows || []).map(r => ({
|
|
71
|
+
title: r.title,
|
|
72
|
+
...(r.description ? { description: r.description } : {}),
|
|
73
|
+
id: r.rowId ?? r.id ?? r.title
|
|
74
|
+
}))
|
|
75
|
+
|
|
76
|
+
const buildSections = (entry) => {
|
|
77
|
+
if (Array.isArray(entry.sections) && entry.sections.length) {
|
|
78
|
+
return entry.sections.map(s => ({
|
|
79
|
+
...(s.title ? { title: s.title } : {}),
|
|
80
|
+
...(s.highlight_label || s.highlightLabel ? { highlight_label: s.highlight_label || s.highlightLabel } : {}),
|
|
81
|
+
rows: normalizeRows(s.rows)
|
|
82
|
+
}))
|
|
83
|
+
}
|
|
84
|
+
return [{
|
|
85
|
+
title: entry.sectionTitle || entry.buttonText || 'Menu',
|
|
86
|
+
...(entry.highlightLabel ? { highlight_label: entry.highlightLabel } : {}),
|
|
87
|
+
rows: normalizeRows(entry.rows)
|
|
88
|
+
}]
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
const normalizeHandlers = (handlers) => {
|
|
92
|
+
const sheets = {}
|
|
93
|
+
const commands = {}
|
|
94
|
+
for (const [id, val] of Object.entries(handlers || {})) {
|
|
95
|
+
if (typeof val === 'string') {
|
|
96
|
+
commands[id] = val
|
|
97
|
+
} else if (val && (Array.isArray(val.rows) || Array.isArray(val.sections))) {
|
|
98
|
+
sheets[id] = val
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
return { sheets, commands }
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
/**
|
|
105
|
+
* Manually send a native-flow list sheet (the tap-to-open menu).
|
|
106
|
+
* attachSheetAutoReply calls this for you on a matching click.
|
|
107
|
+
*/
|
|
108
|
+
export async function sendSheet(sock, jid, entry, quoted) {
|
|
109
|
+
const sections = buildSections(entry)
|
|
110
|
+
return sock.sendMessage(jid, {
|
|
111
|
+
text: entry.text || '📋 Select an option 👇',
|
|
112
|
+
...(entry.footer ? { footer: entry.footer } : {}),
|
|
113
|
+
nativeFlow: true,
|
|
114
|
+
buttons: [{
|
|
115
|
+
buttonText: { displayText: entry.buttonText || '☰ menu' },
|
|
116
|
+
buttonId: entry.buttonId || 'sheet',
|
|
117
|
+
type: 1,
|
|
118
|
+
sections
|
|
119
|
+
}]
|
|
120
|
+
}, quoted ? { quoted } : {})
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
/**
|
|
124
|
+
* Register automatic sheet replies on a socket.
|
|
125
|
+
*
|
|
126
|
+
* @param {object} sock the makeWASocket() instance
|
|
127
|
+
* @param {object} handlers map of clickId → sheet definition { text?, footer?,
|
|
128
|
+
* buttonText?, sectionTitle?, highlightLabel?, rows|sections }
|
|
129
|
+
* OR clickId → '.command' string shorthand
|
|
130
|
+
* @returns {Function} uninstall — restores the original sock.ev.on
|
|
131
|
+
*/
|
|
132
|
+
export function attachSheetAutoReply(sock, handlers) {
|
|
133
|
+
const { sheets, commands } = normalizeHandlers(handlers)
|
|
134
|
+
|
|
135
|
+
async function handleUpsert(payload) {
|
|
136
|
+
const { messages, type } = payload || {}
|
|
137
|
+
if (type !== 'notify' || !Array.isArray(messages)) return false
|
|
138
|
+
for (const msg of messages) {
|
|
139
|
+
if (!msg?.message || msg.key?.fromMe) continue
|
|
140
|
+
const clicked = decodeButtonClick(msg)
|
|
141
|
+
if (!clicked) continue
|
|
142
|
+
|
|
143
|
+
// 1) full sheet definition → send the sheet, consume the click
|
|
144
|
+
const entry = sheets[clicked]
|
|
145
|
+
if (entry) {
|
|
146
|
+
try {
|
|
147
|
+
await sendSheet(sock, msg.key.remoteJid, { ...entry, buttonId: clicked }, msg)
|
|
148
|
+
} catch (e) {
|
|
149
|
+
try { sock.logger?.error?.({ err: e }, 'sheet auto-reply failed') } catch { /* ignore */ }
|
|
150
|
+
}
|
|
151
|
+
return true
|
|
152
|
+
}
|
|
153
|
+
|
|
154
|
+
// 2) command shorthand → rewrite into normal command text for the parser
|
|
155
|
+
const cmd = commands[clicked]
|
|
156
|
+
if (cmd) {
|
|
157
|
+
msg.message = { conversation: cmd }
|
|
158
|
+
return false
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return false
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
// Wrap the user's messages.upsert registration so the sheet fires first
|
|
165
|
+
// and consumed clicks never reach the bot's command parser.
|
|
166
|
+
const origOn = sock.ev.on.bind(sock.ev)
|
|
167
|
+
sock.ev.on = function (ev, handler) {
|
|
168
|
+
if (ev === 'messages.upsert') {
|
|
169
|
+
return origOn(ev, async (...args) => {
|
|
170
|
+
const consumed = await handleUpsert(args[0]).catch(() => false)
|
|
171
|
+
if (consumed) return true
|
|
172
|
+
return handler(...args)
|
|
173
|
+
})
|
|
174
|
+
}
|
|
175
|
+
return origOn(ev, handler)
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
return () => { sock.ev.on = origOn }
|
|
179
|
+
}
|
package/lib/Utils/messages.js
CHANGED
|
@@ -699,56 +699,23 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
699
699
|
const rawExtras = (inputButtons || []).filter(b =>
|
|
700
700
|
b?.nativeFlowInfo?.name || Number(b?.type) === proto.Message.ButtonsMessage.Button.Type.NATIVE_FLOW)
|
|
701
701
|
if (rawExtras.length) {
|
|
702
|
-
|
|
703
|
-
|
|
704
|
-
|
|
705
|
-
|
|
706
|
-
|
|
707
|
-
|
|
708
|
-
|
|
709
|
-
|
|
710
|
-
|
|
711
|
-
|
|
712
|
-
|
|
713
|
-
|
|
714
|
-
|
|
715
|
-
|
|
716
|
-
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
}
|
|
720
|
-
})
|
|
721
|
-
const interactiveMessage = {
|
|
722
|
-
nativeFlowMessage: { buttons: flowButtons, messageParamsJson: '', messageVersion: 1 }
|
|
723
|
-
}; const bodyText = raw.contentText || (raw.imageMessage?.caption ?? raw.videoMessage?.caption ?? '') || ''
|
|
724
|
-
if (bodyText) interactiveMessage.body = { text: bodyText };
|
|
725
|
-
if (raw.footerText) interactiveMessage.footer = { text: raw.footerText };
|
|
726
|
-
const header = { title: raw.text || (interactiveMessage.body.text ? String(interactiveMessage.body.text).split('\n')[0].slice(0, 42) : '') || 'Menu', subtitle: '', hasMediaAttachment: false };
|
|
727
|
-
if (rawLoc) {
|
|
728
|
-
header.locationMessage = {
|
|
729
|
-
degreesLatitude: rawLoc.degreesLatitude ?? 0,
|
|
730
|
-
degreesLongitude: rawLoc.degreesLongitude ?? 0,
|
|
731
|
-
name: rawLoc.name ?? null,
|
|
732
|
-
address: rawLoc.address ?? null,
|
|
733
|
-
...(rawLoc.jpegThumbnail ? { jpegThumbnail: rawLoc.jpegThumbnail } : {})
|
|
734
|
-
};
|
|
735
|
-
header.hasMediaAttachment = true;
|
|
736
|
-
} else if (raw.imageMessage) {
|
|
737
|
-
header.imageMessage = raw.imageMessage;
|
|
738
|
-
header.hasMediaAttachment = true;
|
|
739
|
-
} else if (raw.videoMessage) {
|
|
740
|
-
header.videoMessage = raw.videoMessage;
|
|
741
|
-
header.hasMediaAttachment = true;
|
|
742
|
-
} else if (raw.documentMessage) {
|
|
743
|
-
header.documentMessage = raw.documentMessage;
|
|
744
|
-
header.hasMediaAttachment = true;
|
|
745
|
-
}
|
|
746
|
-
if (header.title || header.hasMediaAttachment) interactiveMessage.header = header;
|
|
747
|
-
if (raw.contextInfo?.mentionedJid?.length) {
|
|
748
|
-
interactiveMessage.contextInfo = { mentionedJid: raw.contextInfo.mentionedJid };
|
|
749
|
-
}
|
|
750
|
-
m = { interactiveMessage };
|
|
751
|
-
} else {
|
|
702
|
+
// PATCH (1.0.28): a raw classic message that MIXES native-flow extras
|
|
703
|
+
// (single_select nativeFlowInfo, cta_* buttons) into a ButtonsMessage is
|
|
704
|
+
// the Case X / cantarella menu-card pattern — on current clients it
|
|
705
|
+
// renders as "version doesn't support it" (real-device evidence).
|
|
706
|
+
// Instead of upgrading to the vertical native-flow card, keep the
|
|
707
|
+
// HORIZONTAL classic layout: every extra becomes a plain RESPONSE
|
|
708
|
+
// button with its original buttonId, clicks arrive as
|
|
709
|
+
// buttonsResponseMessage, and the bot sends the list sheet / URL /
|
|
710
|
+
// copy action itself (e.g. a separate nativeFlow message). The header
|
|
711
|
+
// (location/image/video/document) is preserved untouched — that is the
|
|
712
|
+
// proven horizontal Case X / cantarella render.
|
|
713
|
+
raw.buttons = inputButtons.map(b => proto.Message.ButtonsMessage.Button.fromObject({
|
|
714
|
+
buttonId: b?.buttonId || b?.buttonText?.displayText || 'button',
|
|
715
|
+
buttonText: { displayText: b?.buttonText?.displayText || b?.displayText || 'Button' },
|
|
716
|
+
type: proto.Message.ButtonsMessage.Button.Type.RESPONSE
|
|
717
|
+
}))
|
|
718
|
+
}
|
|
752
719
|
if (!hasHeader) {
|
|
753
720
|
try {
|
|
754
721
|
const media = await prepareWAMessageMedia({ image: BUTTONS_INVISIBLE_PNG() }, options)
|
|
@@ -757,7 +724,6 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
757
724
|
} catch { /* upload failed — send as-is */ }
|
|
758
725
|
}
|
|
759
726
|
m = { buttonsMessage: raw };
|
|
760
|
-
}
|
|
761
727
|
}
|
|
762
728
|
else if ('buttons' in message && !!message.buttons) {
|
|
763
729
|
// PATCH: `buttons` API — HORIZONTAL button row via classic ButtonsMessage
|
package/lib/index.js
CHANGED
|
@@ -2,6 +2,7 @@ import makeWASocket from './Socket/index.js';
|
|
|
2
2
|
import chalk from "chalk";
|
|
3
3
|
import { enableCallAutoAnswer, getActiveVoipClient, CallState } from './Caller/auto-answer.js';
|
|
4
4
|
import { attachProMethods } from './Pro/index.js';
|
|
5
|
+
import { attachSheetAutoReply, sendSheet, decodeButtonClick } from './Pro/sheet-buttons.js';
|
|
5
6
|
import * as PollsPro from './Pro/polls.js';
|
|
6
7
|
import * as NewsletterPro from './Pro/newsletter.js';
|
|
7
8
|
import * as StatusPro from './Pro/status.js';
|
|
@@ -91,5 +92,8 @@ export const removeProfilePicture = ProfilePro.removeProfilePicture;
|
|
|
91
92
|
export const rejectCall = ProfilePro.rejectCall;
|
|
92
93
|
|
|
93
94
|
export { enableCallAutoAnswer, getActiveVoipClient, CallState, attachProMethods, makeWASocket };
|
|
95
|
+
|
|
96
|
+
// One-tap horizontal button → automatic list sheet (v1.0.28+)
|
|
97
|
+
export { attachSheetAutoReply, sendSheet, decodeButtonClick };
|
|
94
98
|
export default makeWASocket;
|
|
95
99
|
//# sourceMappingURL=index.js.map
|