@violetix/baileys 0.3.18 โ 0.4.0
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 +174 -0
- package/lib/Socket/index.d.ts +7 -0
- package/lib/Socket/messages-send.js +136 -6
- package/lib/Socket/socket.d.ts +6 -0
- package/lib/Socket/socket.js +18 -0
- package/lib/Utils/a2ui.d.ts +50 -0
- package/lib/Utils/a2ui.js +168 -0
- package/lib/Utils/anchor-guard.d.ts +71 -0
- package/lib/Utils/anchor-guard.js +404 -0
- package/lib/Utils/business.js +25 -11
- package/lib/Utils/ignore-key.d.ts +51 -0
- package/lib/Utils/ignore-key.js +174 -0
- package/lib/Utils/index.d.ts +3 -0
- package/lib/Utils/index.js +3 -0
- package/lib/Utils/messages-media.js +22 -0
- package/lib/Utils/messages.d.ts +1 -0
- package/lib/Utils/messages.js +72 -0
- package/lib/Utils/use-multi-file-auth-state.d.ts +2 -1
- package/lib/Utils/use-multi-file-auth-state.js +27 -6
- package/lib/Utils/use-single-file-auth-state.d.ts +1 -1
- package/lib/Utils/use-single-file-auth-state.js +12 -3
- package/package.json +2 -2
package/README.md
CHANGED
|
@@ -381,6 +381,14 @@ and the sections after it.
|
|
|
381
381
|
- ๐ข `version` accepts a resolver function, and a `405 client_too_old` failure fetches the live WA Web build and applies it on the next reconnect. `fetchLatestWaWebVersion()` now times out after 10s (`timeoutMs` / `signal` options).
|
|
382
382
|
- ๐ Passkey-gated (Shortcake) accounts are surfaced: `connection.update { passkeyRequired: true }` plus a warning when the server sends `passkey_prologue_request`. Expired pairing codes (`refresh_code`) are re-registered automatically with the same code, or surfaced as `connection.update { pairingCodeExpired: true }`.
|
|
383
383
|
- ๐ข `onWhatsAppUsername()` also reports `isBusiness` / `pnJid`; the USync business parser treats a `<error/>` child as "not a business" instead of failing the whole query, and `withBusinessProtocol(null)` asks for the verified name only.
|
|
384
|
+
- ๐ผ๏ธ Added [image polls](#%EF%B8%8F-image-poll): `sendMessage(jid, { imagePoll })` uploads one image per option, hashes each option over its uploaded `fileSha256`, sends the `pollCreationMessageV3` parent with `pollContentType: IMAGE`, then one `pollCreationOptionImageMessage` child per option associated back by `MEDIA_POLL`. `hashImagePollOption()` is exported separately. Ported from [`@rennzsync/baileys`](https://github.com/RennZSync/baileys).
|
|
385
|
+
- ๐งฉ Added [A2UI widgets](#-a2ui-widget): `sendMessage(jid, { a2ui })` with a declarative builder (`Text`, `Image`, `Video`, `Button`, `Card`, `Column`, `Row`, `Divider`, `CheckBox`, `TextField`, `ChoicePicker`, plus `listCard`) compiled into `interactiveMessage.bloksWidget`. Ported from [`@rennzsync/baileys`](https://github.com/RennZSync/baileys), rebuilt on this fork's own interactive path so the `<biz>` node and button shapes are the existing ones.
|
|
386
|
+
- ๐ Added the [`exclusive`](#-exclusive-message) send option: a group message addressed to a single member (`to=<group> participant=<member>`, per-device encryption instead of the group sender key), so no other member receives the stanza. The recipient comes from the quoted message's author, or from an explicit jid.
|
|
387
|
+
- ๐ซ Added [`sock.ignoreKey()`](#-ignoring-inbound-stanzas): drops matching inbound stanzas before any handler โ including the decryption and persistence paths โ while still acking so the server stops redelivering. Matches by `remoteJid` / `fromMe` / `id` / `participant` (PN โ LID alt attributes included) or by predicate, over `message`, `receipt`, `notification`, `presence`, `chatstate` and `call`. `category='peer'` traffic from your own devices is never dropped, so an app-state key share cannot be filtered away by accident.
|
|
388
|
+
- ๐ก๏ธ Added [`createAnchorGuard()`](#%EF%B8%8F-anchor-guard) / `detectBug()`: flags payloads that force-close WhatsApp clients โ invisible and combining character floods, mention bombs, oversized native-flow / list / carousel payloads, broken `buttonParamsJson`, over-deep or circular structures โ then deletes them and optionally blocks the sender or kicks on burst. Ported from [`@rexxhayanasi/elaina-anchorguard`](https://www.npmjs.com/package/@rexxhayanasi/elaina-anchorguard) (MIT) via [`@rennzsync/baileys`](https://github.com/RennZSync/baileys).
|
|
389
|
+
- ๐๏ธ Sent `encEventResponseMessage` and `EVENT_EDIT` secrets as `type=event`. Only `eventMessage` was matched, so event responses went out as `type=text` โ a pairing a real client never sends, and the server validates stanza shape.
|
|
390
|
+
- ๐งพ Stamped `mediatype=group_history` on `messageHistoryBundle` sends, which previously carried no media type at all.
|
|
391
|
+
- ๐ `useMultiFileAuthState(folder, logger)` and `useSingleFileAuthState(file, logger)` accept a logger. Credential write failures were swallowed entirely (the internal logger was hard-wired to `null`), so a full disk or a permission error lost the session and looked like a random logout on the next boot. Corrupt or unreadable key files are reported too, and `_destroy()` releases the process exit hooks a long-lived multi-session host would otherwise leak.
|
|
384
392
|
|
|
385
393
|
## ๐จ Messages Handling & Compatibility
|
|
386
394
|
|
|
@@ -390,6 +398,8 @@ and the sections after it.
|
|
|
390
398
|
- ๐๐ป [Interactive Message](#-sending-interactive-messages) (buttons, lists, native flows, templates, carousels).
|
|
391
399
|
- ๐๏ธ [Status Mention Message](#%EF%B8%8F-status-mention)
|
|
392
400
|
- ๐ฆ [Sticker Pack Message](#-sticker-pack)
|
|
401
|
+
- ๐ผ๏ธ [Image Poll](#%EF%B8%8F-image-poll)
|
|
402
|
+
- ๐งฉ [A2UI Widget](#-a2ui-widget)
|
|
393
403
|
- โจ [Rich Response Message](#-rich-response)
|
|
394
404
|
- ๐งพ [Message with Code Blocks](#-message-with-code-block)
|
|
395
405
|
- [๐ Message with Inline Entities](#-message-with-inline-entities)
|
|
@@ -406,6 +416,7 @@ and the sections after it.
|
|
|
406
416
|
- ๐ฃ [`mentionAll`](#-mention) - Mention all group participants without requiring their JIDs in `mentions` or `mentionedJid`
|
|
407
417
|
- ๐ง [`ephemeral`](#-ephemeral), [`groupStatus`](#%E2%80%8D%E2%80%8D-group-status), [`isLottie`](#-lottie-sticker), [`spoiler`](#-spoiler), [`viewOnce`](#%EF%B8%8F-view-once), [`viewOnceV2`](#%EF%B8%8F-view-once-v2), [`viewOnceV2Extension`](#%EF%B8%8F-view-once-v2-extension), [`interactiveAsTemplate`](#%EF%B8%8F-interactive) - Message wrappers
|
|
408
418
|
- ๐ [`secureMetaServiceLabel`](#%EF%B8%8F-secure-meta-service-label) - Secure meta service label on message
|
|
419
|
+
- ๐ [`exclusive`](#-exclusive-message) - Send into a group addressed to one member only
|
|
409
420
|
- ๐ [`raw`](#-raw) - Build your message manually **(DO NOT USE FOR EXPLOITATION)**
|
|
410
421
|
- ๐๏ธ [`statusPrivacy`](#%EF%B8%8F-status-mention) - Control who receives a status broadcast (`contacts` | `allowlist` | `denylist`)
|
|
411
422
|
|
|
@@ -768,6 +779,30 @@ sock.sendMessage(jid, {
|
|
|
768
779
|
})
|
|
769
780
|
```
|
|
770
781
|
|
|
782
|
+
### ๐ผ๏ธ Image Poll
|
|
783
|
+
|
|
784
|
+
A poll whose options are images instead of text.
|
|
785
|
+
|
|
786
|
+
```javascript
|
|
787
|
+
sock.sendMessage(jid, {
|
|
788
|
+
imagePoll: {
|
|
789
|
+
name: '๐ผ๏ธ Pick your favourite',
|
|
790
|
+
selectableCount: 1, // --- Optional, defaults to 1
|
|
791
|
+
options: [
|
|
792
|
+
{ name: 'Gambar 1', image: { url: 'https://example.com/1.jpg' } },
|
|
793
|
+
{ name: 'Gambar 2', image: { url: 'https://example.com/2.jpg' } }
|
|
794
|
+
]
|
|
795
|
+
}
|
|
796
|
+
}, {
|
|
797
|
+
quoted: message
|
|
798
|
+
})
|
|
799
|
+
```
|
|
800
|
+
|
|
801
|
+
Each option's image is uploaded first, because the option hash covers the uploaded media's `fileSha256` โ `sha256(hex(sha256(name)) + base64(fileSha256))`, exported as `hashImagePollOption(name, fileSha256)` if you need it on its own. The parent goes out as `pollCreationMessageV3` with `pollContentType: IMAGE` carrying only the names and hashes; the images follow as one `pollCreationOptionImageMessage` each, tied back to the parent by a `MEDIA_POLL` association (the same mechanism albums use). A vote carries the hash, not the name.
|
|
802
|
+
|
|
803
|
+
> [!WARNING]
|
|
804
|
+
> This is a client-side implementation of a message type WhatsApp's proto defines but no Baileys fork ships a builder for. Treat it as experimental and check how it renders on your target client before relying on it.
|
|
805
|
+
|
|
771
806
|
### ๐ญ Button Response
|
|
772
807
|
|
|
773
808
|
```javascript
|
|
@@ -2708,6 +2743,145 @@ sock.CLIENT_PERSIST_GQL_IDS;
|
|
|
2708
2743
|
> [!WARNING]
|
|
2709
2744
|
> These calls leave the WhatsApp WebSocket and hit Meta's HTTPS endpoints directly. They have no built-in timeout, so wrap them in your own if a hung request would stall your bot.
|
|
2710
2745
|
|
|
2746
|
+
### ๐งฉ A2UI Widget
|
|
2747
|
+
|
|
2748
|
+
Declarative widgets sent as `interactiveMessage.bloksWidget`. Every builder method registers a component and returns its id, so a tree is written bottom-up.
|
|
2749
|
+
|
|
2750
|
+
```javascript
|
|
2751
|
+
import { A2UI } from "@violetix/baileys";
|
|
2752
|
+
|
|
2753
|
+
const ui = new A2UI();
|
|
2754
|
+
const title = ui.text("Halo!", { variant: "h1" });
|
|
2755
|
+
const button = ui.button(ui.text("Klik saya"), { action: { name: "noop" } });
|
|
2756
|
+
ui.root([ui.card(ui.column([title, button]))]);
|
|
2757
|
+
|
|
2758
|
+
await sock.sendMessage(jid, { a2ui: ui, text: "Widget", footer: "A2UI" });
|
|
2759
|
+
|
|
2760
|
+
// --- product list shortcut, its own payload shape
|
|
2761
|
+
const list = new A2UI().listCard({
|
|
2762
|
+
title: "Menu",
|
|
2763
|
+
items: [{ title: "Nasi Goreng", price: "Rp15.000" }, { title: "Es Teh", price: "Rp5.000" }]
|
|
2764
|
+
});
|
|
2765
|
+
await sock.sendMessage(jid, { a2ui: list, text: "Pesan menu" });
|
|
2766
|
+
|
|
2767
|
+
// --- widget as the whole message, plus native flow buttons
|
|
2768
|
+
await sock.sendMessage(jid, {
|
|
2769
|
+
a2ui: ui,
|
|
2770
|
+
singleScreen: true,
|
|
2771
|
+
nativeFlow: [{ text: "๐ Visit", url: "https://example.com" }]
|
|
2772
|
+
});
|
|
2773
|
+
```
|
|
2774
|
+
|
|
2775
|
+
| Component | Builder |
|
|
2776
|
+
| -------------- | ------------------------------------------------------------------------ |
|
|
2777
|
+
| `Text` | `ui.text(text, { variant })` โ `variant` defaults to `body` |
|
|
2778
|
+
| `Image` | `ui.image(url, { variant, fit })` โ `fit` defaults to `cover` |
|
|
2779
|
+
| `Video` | `ui.video(url)` |
|
|
2780
|
+
| `Button` | `ui.button(childId, { variant, action })` |
|
|
2781
|
+
| `Card` / `Column` / `Row` | `ui.card(childId)`, `ui.column(ids, { justify, align })`, `ui.row(ids)` |
|
|
2782
|
+
| `CheckBox` / `TextField` / `ChoicePicker` / `Divider` | `ui.checkbox(label, { value })`, `ui.textField(label, { variant })`, `ui.choicePicker(label, options, { variant, displayStyle, filterable })`, `ui.divider()` |
|
|
2783
|
+
|
|
2784
|
+
Send options on the content: `text` (body), `footer`, `singleScreen` (no bubble around the widget), `nativeFlow` (this fork's usual [button shapes](#%EF%B8%8F-interactive)), `messageSecret`, `a2uiType`, `a2uiWrapped: false` (emit a bare `{ components }` payload instead of the `createSurface` envelope). `nativeFlowMessage` is always attached โ it is what keys the interactive render and what makes the `<biz>` node go out.
|
|
2785
|
+
|
|
2786
|
+
> [!WARNING]
|
|
2787
|
+
> Internal WhatsApp format. A send can succeed with no error while the widget renders as nothing on the recipient's client. Test on a throwaway number first.
|
|
2788
|
+
|
|
2789
|
+
### ๐ Exclusive Message
|
|
2790
|
+
|
|
2791
|
+
Sends into a group but addressed to **one member**. Other members never receive the stanza at all, so there is nothing for them to see or decrypt.
|
|
2792
|
+
|
|
2793
|
+
```javascript
|
|
2794
|
+
// the quoted message names the recipient โ its author
|
|
2795
|
+
await sock.sendMessage(m.chat, { text: "Hi, only you can see this." }, { quoted: m, exclusive: true });
|
|
2796
|
+
|
|
2797
|
+
// or address someone else explicitly, in the group's own addressing mode
|
|
2798
|
+
await sock.sendMessage(groupJid, { text: "psst" }, { exclusive: "43411111111111@lid" });
|
|
2799
|
+
```
|
|
2800
|
+
|
|
2801
|
+
`exclusive: true` requires `quoted` โ the quoted message's author is the only place the recipient comes from, and it already carries the group's addressing mode (a LID-addressed group quotes `@lid`, a PN one quotes `@s.whatsapp.net`), so nothing has to be mapped. Without a quote, or with a quote that has no `participant` (a 1:1 quote), the send throws. So does `exclusive` outside a group.
|
|
2802
|
+
|
|
2803
|
+
How it works: the stanza goes out as `to=<group> participant=<member>` with per-device `pkmsg`/`msg` encryption instead of the group sender key, reusing the retry-resend path. A `skmsg` stanza is fanned out to the whole group by the server, so sender-key sends can never be targeted. One stanza per device of the recipient, all sharing the message id.
|
|
2804
|
+
|
|
2805
|
+
> [!NOTE]
|
|
2806
|
+
> Your own linked devices are not addressed, so the message does not appear in the group on your phone โ only `messages.upsert` fires locally (via `emitOwnEvents`). The recipient sees an ordinary group message; nothing marks it as private on their side.
|
|
2807
|
+
|
|
2808
|
+
### ๐ซ Ignoring Inbound Stanzas
|
|
2809
|
+
|
|
2810
|
+
`sock.ignoreKey(input)` drops matching inbound stanzas **before any handler runs** โ including the decryption and persistence paths. The ack is still sent, so the server stops redelivering them. It returns an unregister function.
|
|
2811
|
+
|
|
2812
|
+
```javascript
|
|
2813
|
+
// --- Descriptor: drop everything from one peer
|
|
2814
|
+
const off = sock.ignoreKey({ remoteJid: "6281111111111@s.whatsapp.net" });
|
|
2815
|
+
|
|
2816
|
+
// Only that peer's messages; receipts and presence still arrive
|
|
2817
|
+
sock.ignoreKey({ remoteJid: "6281111111111@s.whatsapp.net", only: ["message"] });
|
|
2818
|
+
|
|
2819
|
+
// Several chats at once (array entries OR)
|
|
2820
|
+
sock.ignoreKey({ remoteJid: ["111111@g.us", "222222@g.us"] });
|
|
2821
|
+
|
|
2822
|
+
// Your own outbound echoes from other devices
|
|
2823
|
+
sock.ignoreKey({ fromMe: true, only: ["message"] });
|
|
2824
|
+
|
|
2825
|
+
off(); // unregister
|
|
2826
|
+
|
|
2827
|
+
// --- Predicate: anything the descriptor cannot express
|
|
2828
|
+
sock.ignoreKey((m) => m.kind === "message" && isJidGroup(m.remoteJid ?? ""));
|
|
2829
|
+
sock.ignoreKey((m) => isJidStatusBroadcast(m.remoteJid ?? ""));
|
|
2830
|
+
```
|
|
2831
|
+
|
|
2832
|
+
| Field | Type | Notes |
|
|
2833
|
+
| ------------- | --------------------------------------------------------------------------------------- | ------------------------------------------------------------------------------------------------------------------------------ |
|
|
2834
|
+
| `remoteJid` | `string \| string[]` | Chat JID. Array entries OR. Also matched against the alt `sender_pn` / `sender_lid` attributes, so one JID form catches the other. |
|
|
2835
|
+
| `fromMe` | `boolean` | Whether the stanza was sent by this account, resolved against both your PN and LID. |
|
|
2836
|
+
| `id` | `string` | Stanza id. |
|
|
2837
|
+
| `participant` | `string` | Author in groups / broadcasts. Alt forms are matched too. |
|
|
2838
|
+
| `only` | `('message' \| 'receipt' \| 'notification' \| 'presence' \| 'chatstate' \| 'call')[]` | Restrict to specific tags. Default: all six. |
|
|
2839
|
+
|
|
2840
|
+
Top-level fields AND together; the `remoteJid` array ORs. At least one of `remoteJid` / `fromMe` / `id` / `participant` is required โ empty descriptors throw, as do unknown `only` values.
|
|
2841
|
+
|
|
2842
|
+
A predicate receives the already-parsed stanza: `{ kind, remoteJid, fromMe, id, participant }`, with `remoteJid` and `participant` device-stripped so they line up with `event.key.remoteJid`. Return `true` to drop. A predicate that throws is logged and the stanza is kept.
|
|
2843
|
+
|
|
2844
|
+
> [!IMPORTANT]
|
|
2845
|
+
> `category='peer'` messages from your own devices are **never** dropped, whatever the filter says. They carry app-state key shares, history sync and PDO responses between your devices, so a `{ fromMe: true }` filter would otherwise swallow the app-state key share and leave the collections blocked on a key they re-request on every sync. The exemption applies only when the sender really is this account, so a foreign stanza cannot dodge a filter by stamping the attribute. Stream control and the auth-critical `success` / `failure` tags bypass filters as well.
|
|
2846
|
+
|
|
2847
|
+
### ๐ก๏ธ Anchor Guard
|
|
2848
|
+
|
|
2849
|
+
Detects payloads that force-close WhatsApp clients, deletes them, and optionally blocks the sender.
|
|
2850
|
+
|
|
2851
|
+
```javascript
|
|
2852
|
+
import { createAnchorGuard, detectBug } from "@violetix/baileys";
|
|
2853
|
+
|
|
2854
|
+
const guard = createAnchorGuard(sock, {
|
|
2855
|
+
blockOnBug: true,
|
|
2856
|
+
guardOutgoing: true, // also reject your own crash payloads before they are sent
|
|
2857
|
+
onDetect: ({ direction, jid, reasons }) => console.log("๐ก๏ธ", direction, jid, reasons),
|
|
2858
|
+
});
|
|
2859
|
+
|
|
2860
|
+
// one-off check without attaching anything
|
|
2861
|
+
const { flagged, reasons } = detectBug(msg.message);
|
|
2862
|
+
|
|
2863
|
+
guard.stop(); // detach listeners and unwrap sendMessage
|
|
2864
|
+
```
|
|
2865
|
+
|
|
2866
|
+
Flags invisible and combining character floods, mention bombs, oversized native-flow / list / carousel payloads, `buttonParamsJson` that is broken or huge, excessive newlines, over-deep, over-wide and circular structures, and โ when you pass `proto` โ an encoded size over `maxBytes`.
|
|
2867
|
+
|
|
2868
|
+
| Option | Default | Description |
|
|
2869
|
+
| --------------------------------- | -------- | ---------------------------------------------------------------------------------------- |
|
|
2870
|
+
| `autoDelete` | `true` | Delete a flagged inbound message. |
|
|
2871
|
+
| `deleteMode` | `'auto'` | `'auto'` revokes your own messages and deletes others locally; `'everyone'` always revokes. |
|
|
2872
|
+
| `guardIncoming` | `true` | Guard `messages.upsert`. |
|
|
2873
|
+
| `guardOutgoing` | `false` | Wrap `sock.sendMessage` and throw on a crash payload. |
|
|
2874
|
+
| `blockOnBug` | `false` | Block the sender of a flagged message. |
|
|
2875
|
+
| `selfOnly` | `false` | Only guard your own chats. |
|
|
2876
|
+
| `burstThreshold` / `burstWindowMs` | `0` / `4000` | Messages from one sender inside the window before it counts as a burst. `0` disables. |
|
|
2877
|
+
| `kickOnBurst` | `false` | Remove the sender from the group on burst (needs admin). |
|
|
2878
|
+
| `guardGroupAdds` | `false` | Flag / kick when a burst-flagged inviter adds you to a group. |
|
|
2879
|
+
| `metaAiNumbers` | `false` | Guard Meta AI's own numbers instead of exempting them. |
|
|
2880
|
+
| `thresholds` | `{}` | Override any of `ANCHORGUARD_DEFAULTS`. |
|
|
2881
|
+
|
|
2882
|
+
> [!NOTE]
|
|
2883
|
+
> `guardOutgoing` replaces `sock.sendMessage` with a wrapper, which is why it is off by default โ turn it on knowingly if your own code also wraps `sendMessage`. `stop()` only unwinds its own wrapper, so a wrapper you installed afterwards survives.
|
|
2884
|
+
|
|
2711
2885
|
### ๐ก Events
|
|
2712
2886
|
|
|
2713
2887
|
```javascript
|
package/lib/Socket/index.d.ts
CHANGED
|
@@ -636,6 +636,7 @@ declare function makeWASocket(config: import('../Types/Socket.js').SocketConfig)
|
|
|
636
636
|
updateMemberLabel: (jid: any, memberLabel: any) => Promise<any>;
|
|
637
637
|
updateMediaMessage: (message: any) => Promise<any>;
|
|
638
638
|
sendMessage: (jid: any, content: any, options?: {}) => Promise<import("../index.js").proto.WebMessageInfo | undefined>;
|
|
639
|
+
groupStatus: (jid: any, content?: any, options?: {}) => Promise<import("../index.js").proto.WebMessageInfo | undefined>;
|
|
639
640
|
executeWMexQuery: (variables: any, queryId: any, dataPath: any) => Promise<any>;
|
|
640
641
|
newsletterCreate: (name: any, description: any) => Promise<{
|
|
641
642
|
id: any;
|
|
@@ -875,6 +876,12 @@ declare function makeWASocket(config: import('../Types/Socket.js').SocketConfig)
|
|
|
875
876
|
signalRepository: any;
|
|
876
877
|
user: any;
|
|
877
878
|
generateMessageTag: () => string;
|
|
879
|
+
/**
|
|
880
|
+
* Drops matching inbound stanzas before any handler โ including the decryption and
|
|
881
|
+
* persistence paths โ sees them. The ack is still sent so the server stops re-delivering.
|
|
882
|
+
* Returns an unregister function.
|
|
883
|
+
*/
|
|
884
|
+
ignoreKey: (input: import('../Utils/ignore-key.js').WaIgnoreKeyInput) => () => void;
|
|
878
885
|
query: (node: any, timeoutMs: any) => Promise<any>;
|
|
879
886
|
waitForMessage: (msgId: any, timeoutMs?: any) => Promise<any>;
|
|
880
887
|
waitForSocketOpen: () => Promise<void>;
|
|
@@ -3,7 +3,7 @@ import { Boom } from '@hapi/boom';
|
|
|
3
3
|
import { randomBytes } from 'crypto';
|
|
4
4
|
import { proto } from '../../WAProto/index.js';
|
|
5
5
|
import { BIZ_BOT_SUPPORT_PAYLOAD, DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
|
|
6
|
-
import { aggregateMessageKeysNotFromMe, assertMediaContent, assertMeId, bindWaitForEvent, decryptMediaRetryData, DEF_MEDIA_HOST, delay, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2, generateParticipantHashV2, generateWAMessage, generateWAMessageFromContent, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, hasValidAlbumMedia, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, shouldIncludeBizBinaryNode, unixTimestampSeconds } from '../Utils/index.js';
|
|
6
|
+
import { aggregateMessageKeysNotFromMe, assertMediaContent, assertMeId, bindWaitForEvent, decryptMediaRetryData, DEF_MEDIA_HOST, delay, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2, generateParticipantHashV2, generateWAMessage, generateWAMessageFromContent, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, hashImagePollOption, hasValidAlbumMedia, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, prepareWAMessageMedia, shouldIncludeBizBinaryNode, unixTimestampSeconds } from '../Utils/index.js';
|
|
7
7
|
import { AssociationType } from '../Types/index.js';
|
|
8
8
|
import { setBotMessageSecret } from '../Utils/decode-wa-message.js';
|
|
9
9
|
import { getUrlInfo } from '../Utils/link-preview.js';
|
|
@@ -1041,6 +1041,79 @@ export const makeMessagesSocket = (config) => {
|
|
|
1041
1041
|
throw err;
|
|
1042
1042
|
}
|
|
1043
1043
|
};
|
|
1044
|
+
/**
|
|
1045
|
+
* Uploads one image per poll option and returns the hashed options for the parent poll
|
|
1046
|
+
* alongside the prepared media the children will carry. The hash covers the uploaded
|
|
1047
|
+
* media's `fileSha256`, so the upload has to happen before the parent is built.
|
|
1048
|
+
*/
|
|
1049
|
+
const prepareImagePollOptions = async (imagePoll, genOptions) => {
|
|
1050
|
+
if (!Array.isArray(imagePoll.options) || !imagePoll.options.length) {
|
|
1051
|
+
throw new Boom('Invalid imagePoll options', { statusCode: 400 });
|
|
1052
|
+
}
|
|
1053
|
+
const optionMedia = [];
|
|
1054
|
+
const options = [];
|
|
1055
|
+
for (const option of imagePoll.options) {
|
|
1056
|
+
if (!option?.image) {
|
|
1057
|
+
throw new Boom('every imagePoll option needs an image', { statusCode: 400 });
|
|
1058
|
+
}
|
|
1059
|
+
const media = await prepareWAMessageMedia({ image: option.image }, genOptions);
|
|
1060
|
+
optionMedia.push(media);
|
|
1061
|
+
options.push({
|
|
1062
|
+
optionName: option.name,
|
|
1063
|
+
optionHash: hashImagePollOption(option.name, media.imageMessage?.fileSha256)
|
|
1064
|
+
});
|
|
1065
|
+
}
|
|
1066
|
+
return { optionMedia, options };
|
|
1067
|
+
};
|
|
1068
|
+
/**
|
|
1069
|
+
* Picks the single group member an exclusive message is addressed to.
|
|
1070
|
+
*
|
|
1071
|
+
* `exclusive: true` reads the author of the quoted message, which is why a quote is required:
|
|
1072
|
+
* it is the only place the recipient can come from, and it already carries the group's own
|
|
1073
|
+
* addressing mode (a LID-addressed group quotes `@lid`, a PN one quotes `@s.whatsapp.net`),
|
|
1074
|
+
* so no mapping is needed. Pass a jid instead to address someone else โ in the group's mode.
|
|
1075
|
+
*/
|
|
1076
|
+
const resolveExclusiveTarget = (options) => {
|
|
1077
|
+
if (typeof options.exclusive === 'string') {
|
|
1078
|
+
return options.exclusive;
|
|
1079
|
+
}
|
|
1080
|
+
const quoted = options.quoted;
|
|
1081
|
+
return quoted?.key?.participant || quoted?.participant || undefined;
|
|
1082
|
+
};
|
|
1083
|
+
/**
|
|
1084
|
+
* Sends into a group but addressed to one member: the stanza goes out as
|
|
1085
|
+
* `to=<group> participant=<member>`, so the server routes it to that member's devices and no
|
|
1086
|
+
* other member receives anything at all. Reuses the retry-resend path โ per-device pkmsg/msg
|
|
1087
|
+
* instead of the group sender key, which is what makes the targeting possible: a `skmsg`
|
|
1088
|
+
* stanza is fanned out to the whole group by the server.
|
|
1089
|
+
*
|
|
1090
|
+
* One stanza per device of the recipient, all sharing the message id, because a group
|
|
1091
|
+
* `participant` attribute addresses a device and not an account.
|
|
1092
|
+
*/
|
|
1093
|
+
const relayExclusive = async (jid, message, options, relayOpts) => {
|
|
1094
|
+
if (!isJidGroup(jid)) {
|
|
1095
|
+
throw new Boom('exclusive messages are only supported in groups', { statusCode: 400 });
|
|
1096
|
+
}
|
|
1097
|
+
const target = resolveExclusiveTarget(options);
|
|
1098
|
+
if (!target) {
|
|
1099
|
+
throw new Boom('exclusive requires a quoted message to pick the recipient, or an explicit jid', { statusCode: 400 });
|
|
1100
|
+
}
|
|
1101
|
+
const targetUser = jidNormalizedUser(target);
|
|
1102
|
+
const deviceJids = (await getUSyncDevices([targetUser], relayOpts.useUserDevicesCache !== false, false))
|
|
1103
|
+
.map(device => device.jid)
|
|
1104
|
+
.filter(Boolean);
|
|
1105
|
+
// No device list means the member is unknown to usync; addressing the bare jid still
|
|
1106
|
+
// reaches their primary device rather than failing the send outright.
|
|
1107
|
+
const targets = deviceJids.length ? deviceJids : [targetUser];
|
|
1108
|
+
logger.debug({ jid, target: targetUser, devices: targets.length }, 'sending exclusive group message');
|
|
1109
|
+
for (const deviceJid of targets) {
|
|
1110
|
+
await relayMessage(jid, message, {
|
|
1111
|
+
...relayOpts,
|
|
1112
|
+
participant: { jid: deviceJid, count: 0 }
|
|
1113
|
+
});
|
|
1114
|
+
}
|
|
1115
|
+
return targets;
|
|
1116
|
+
};
|
|
1044
1117
|
const getMessageType = (message) => {
|
|
1045
1118
|
if (!message)
|
|
1046
1119
|
return 'text';
|
|
@@ -1055,7 +1128,12 @@ export const makeMessagesSocket = (config) => {
|
|
|
1055
1128
|
message.pollUpdateMessage) {
|
|
1056
1129
|
return 'poll';
|
|
1057
1130
|
}
|
|
1058
|
-
|
|
1131
|
+
// An event response and an EVENT_EDIT secret both belong to the event thread. Only
|
|
1132
|
+
// `eventMessage` was matched here, so those two went out as `media`/`text`; the server
|
|
1133
|
+
// validates stanza shape, which is a pairing a real client never sends.
|
|
1134
|
+
if (message.eventMessage ||
|
|
1135
|
+
message.encEventResponseMessage ||
|
|
1136
|
+
message.secretEncryptedMessage?.secretEncType === proto.Message.SecretEncryptedMessage.SecretEncType.EVENT_EDIT) {
|
|
1059
1137
|
return 'event';
|
|
1060
1138
|
}
|
|
1061
1139
|
if (getMediaType(message) !== '') {
|
|
@@ -1115,6 +1193,9 @@ export const makeMessagesSocket = (config) => {
|
|
|
1115
1193
|
else if (message.extendedTextMessage?.matchedText || message.groupInviteMessage) {
|
|
1116
1194
|
return 'url';
|
|
1117
1195
|
}
|
|
1196
|
+
else if (message.messageHistoryBundle) {
|
|
1197
|
+
return 'group_history';
|
|
1198
|
+
}
|
|
1118
1199
|
// Lia@Note 02-02-26 --- Add more message type here
|
|
1119
1200
|
else if ((message.extendedTextMessage?.text || message.conversation || '').includes('://wa.me/c/')) {
|
|
1120
1201
|
return 'cataloglink';
|
|
@@ -1327,7 +1408,23 @@ export const makeMessagesSocket = (config) => {
|
|
|
1327
1408
|
await groupToggleEphemeral(jid, value);
|
|
1328
1409
|
}
|
|
1329
1410
|
else {
|
|
1330
|
-
|
|
1411
|
+
// An image poll uploads one image per option before the parent is built: the
|
|
1412
|
+
// parent only carries each option's hash, and the hash covers the uploaded
|
|
1413
|
+
// media's fileSha256. The images follow as children once the parent has a key.
|
|
1414
|
+
let imagePollOptionMedia;
|
|
1415
|
+
let sendContent = content;
|
|
1416
|
+
if ('imagePoll' in content && content.imagePoll) {
|
|
1417
|
+
const prepared = await prepareImagePollOptions(content.imagePoll, {
|
|
1418
|
+
logger,
|
|
1419
|
+
upload: waUploadToServer,
|
|
1420
|
+
mediaCache: config.mediaCache,
|
|
1421
|
+
options: config.options,
|
|
1422
|
+
...options
|
|
1423
|
+
});
|
|
1424
|
+
imagePollOptionMedia = prepared.optionMedia;
|
|
1425
|
+
sendContent = { ...content, imagePoll: { ...content.imagePoll, options: prepared.options } };
|
|
1426
|
+
}
|
|
1427
|
+
const fullMsg = await generateWAMessage(jid, sendContent, {
|
|
1331
1428
|
logger,
|
|
1332
1429
|
userJid,
|
|
1333
1430
|
getUrlInfo: text => getUrlInfo(text, {
|
|
@@ -1354,7 +1451,7 @@ export const makeMessagesSocket = (config) => {
|
|
|
1354
1451
|
const isEditMsg = 'edit' in content && !!content.edit;
|
|
1355
1452
|
const isPinMsg = 'pin' in content && !!content.pin;
|
|
1356
1453
|
const isKeepMsg = 'keep' in content && !!content.keep;
|
|
1357
|
-
const isPollMsg = 'poll' in content && !!content.poll;
|
|
1454
|
+
const isPollMsg = ('poll' in content && !!content.poll) || !!imagePollOptionMedia;
|
|
1358
1455
|
const isQuizMsg = 'poll' in content && !!content.poll.pollType;
|
|
1359
1456
|
const isAiMsg = 'ai' in content && !!content.ai;
|
|
1360
1457
|
const isNeedBizAttrs = 'secureMetaServiceLabel' in content && !!content.secureMetaServiceLabel;
|
|
@@ -1417,7 +1514,7 @@ export const makeMessagesSocket = (config) => {
|
|
|
1417
1514
|
});
|
|
1418
1515
|
delete content.ai;
|
|
1419
1516
|
}
|
|
1420
|
-
|
|
1517
|
+
const relayOpts = {
|
|
1421
1518
|
messageId: fullMsg.key.id,
|
|
1422
1519
|
useCachedGroupMetadata: options.useCachedGroupMetadata,
|
|
1423
1520
|
addBizAttributes: isNeedBizAttrs,
|
|
@@ -1425,10 +1522,43 @@ export const makeMessagesSocket = (config) => {
|
|
|
1425
1522
|
statusPrivacy: options.statusPrivacy,
|
|
1426
1523
|
additionalAttributes,
|
|
1427
1524
|
additionalNodes
|
|
1428
|
-
}
|
|
1525
|
+
};
|
|
1526
|
+
if (options.exclusive) {
|
|
1527
|
+
await relayExclusive(jid, fullMsg.message, options, relayOpts);
|
|
1528
|
+
}
|
|
1529
|
+
else {
|
|
1530
|
+
await relayMessage(jid, fullMsg.message, relayOpts);
|
|
1531
|
+
}
|
|
1429
1532
|
if (config.emitOwnEvents) {
|
|
1430
1533
|
setImmediate(() => emitOwnUpsert(fullMsg));
|
|
1431
1534
|
}
|
|
1535
|
+
// The option images follow the parent, each pointing back at it. Same shape as
|
|
1536
|
+
// the album loop below, with MEDIA_POLL instead of MEDIA_ALBUM.
|
|
1537
|
+
if (imagePollOptionMedia?.length) {
|
|
1538
|
+
for (const media of imagePollOptionMedia) {
|
|
1539
|
+
const optionMsg = generateWAMessageFromContent(jid, {
|
|
1540
|
+
pollCreationOptionImageMessage: { message: media },
|
|
1541
|
+
messageContextInfo: {
|
|
1542
|
+
messageAssociation: {
|
|
1543
|
+
parentMessageKey: fullMsg.key,
|
|
1544
|
+
associationType: AssociationType.MEDIA_POLL
|
|
1545
|
+
}
|
|
1546
|
+
}
|
|
1547
|
+
}, {
|
|
1548
|
+
userJid,
|
|
1549
|
+
messageId: generateMessageIDV2(userJid)
|
|
1550
|
+
});
|
|
1551
|
+
await relayMessage(jid, optionMsg.message, {
|
|
1552
|
+
messageId: optionMsg.key.id,
|
|
1553
|
+
useCachedGroupMetadata: options.useCachedGroupMetadata,
|
|
1554
|
+
additionalAttributes,
|
|
1555
|
+
additionalNodes: []
|
|
1556
|
+
});
|
|
1557
|
+
if (config.emitOwnEvents) {
|
|
1558
|
+
process.nextTick(() => emitOwnUpsert(optionMsg));
|
|
1559
|
+
}
|
|
1560
|
+
}
|
|
1561
|
+
}
|
|
1432
1562
|
// Lia@Changes 31-01-26 --- Add support for album messages
|
|
1433
1563
|
// Lia@Note 06-02-26 --- Refactored to reduce high RSS usage (โ โฅโ ๏นโ โฅโ )
|
|
1434
1564
|
if ('album' in content) {
|
package/lib/Socket/socket.d.ts
CHANGED
|
@@ -25,6 +25,12 @@ export function makeSocket(config: any): {
|
|
|
25
25
|
signalRepository: any;
|
|
26
26
|
readonly user: any;
|
|
27
27
|
generateMessageTag: () => string;
|
|
28
|
+
/**
|
|
29
|
+
* Drops matching inbound stanzas before any handler โ including the decryption and
|
|
30
|
+
* persistence paths โ sees them. The ack is still sent so the server stops re-delivering.
|
|
31
|
+
* Returns an unregister function.
|
|
32
|
+
*/
|
|
33
|
+
ignoreKey: (input: import('../Utils/ignore-key.js').WaIgnoreKeyInput) => () => void;
|
|
28
34
|
query: (node: any, timeoutMs: any) => Promise<any>;
|
|
29
35
|
waitForMessage: (msgId: any, timeoutMs?: any) => Promise<any>;
|
|
30
36
|
waitForSocketOpen: () => Promise<void>;
|
package/lib/Socket/socket.js
CHANGED
|
@@ -7,6 +7,8 @@ import { DEF_CALLBACK_PREFIX, DEF_TAG_PREFIX, INITIAL_PREKEY_COUNT, MIN_PREKEY_C
|
|
|
7
7
|
import { QueryIds, ReachoutTimelockEnforcementType } from '../Types/index.js';
|
|
8
8
|
import { DisconnectReason, XWAPaths } from '../Types/index.js';
|
|
9
9
|
import { addTransactionCapability, aesEncryptCTR, bindWaitForConnectionUpdate, buildPairingQRData, bytesToCrockford, configureSuccessfulPairing, Curve, derivePairingCodeKey, fetchLatestWaWebVersion, generateLoginNode, generateMdTagPrefix, generateRegistrationNode, getCodeFromWSError, getErrorCodeFromStreamError, getNextPreKeysNode, getPairingCodePlatform, invalidPairingCodeChars, makeEventBuffer, makeNoiseHandler, normalizePairingCode, PAIRING_CODE_ALPHABET, promiseTimeout, signedKeyPair, xmppSignedPreKey } from '../Utils/index.js';
|
|
10
|
+
import { ignoredStanzaNeedsAck, makeIgnoreKeyRegistry } from '../Utils/ignore-key.js';
|
|
11
|
+
import { buildAckStanza } from '../Utils/stanza-ack.js';
|
|
10
12
|
import { assertNodeErrorFree, binaryNodeToString, encodeBinaryNode, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildren, isLidUser, jidDecode, jidEncode, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
11
13
|
import { BinaryInfo } from '../WAM/BinaryInfo.js';
|
|
12
14
|
import { USyncQuery, USyncUser } from '../WAUSync/index.js';
|
|
@@ -560,6 +562,11 @@ export const makeSocket = (config) => {
|
|
|
560
562
|
// Don't throw - allow connection to continue even if pre-key check fails
|
|
561
563
|
}
|
|
562
564
|
};
|
|
565
|
+
const ignoreKeyRegistry = makeIgnoreKeyRegistry({
|
|
566
|
+
getMeId: () => authState.creds.me?.id,
|
|
567
|
+
getMeLid: () => authState.creds.me?.lid,
|
|
568
|
+
logger
|
|
569
|
+
});
|
|
563
570
|
const onMessageReceived = async (data) => {
|
|
564
571
|
await noise.decodeFrame(data, frame => {
|
|
565
572
|
lastDateRecv = new Date();
|
|
@@ -570,6 +577,16 @@ export const makeSocket = (config) => {
|
|
|
570
577
|
if (logger.level === 'trace') {
|
|
571
578
|
logger.trace({ xml: binaryNodeToString(frame), msg: 'recv xml' });
|
|
572
579
|
}
|
|
580
|
+
// Filters run before every other handler, so a dropped stanza never reaches the
|
|
581
|
+
// decryption or persistence paths. The ack still goes out, otherwise the server
|
|
582
|
+
// keeps re-delivering what the host just asked to ignore.
|
|
583
|
+
if (ignoreKeyRegistry.shouldDrop(frame)) {
|
|
584
|
+
logger.debug({ tag: frame.tag, id: msgId, from: frame.attrs.from }, 'stanza dropped by ignoreKey');
|
|
585
|
+
if (ignoredStanzaNeedsAck(frame.tag)) {
|
|
586
|
+
sendNode(buildAckStanza(frame, undefined, authState.creds.me?.id)).catch(err => logger.error({ err, id: msgId }, 'failed to ack ignored stanza'));
|
|
587
|
+
}
|
|
588
|
+
return;
|
|
589
|
+
}
|
|
573
590
|
anyTriggered = ws.emit(`${DEF_TAG_PREFIX}${msgId}`, frame) || anyTriggered;
|
|
574
591
|
const l0 = frame.tag;
|
|
575
592
|
const l1 = frame.attrs || {};
|
|
@@ -1227,6 +1244,7 @@ export const makeSocket = (config) => {
|
|
|
1227
1244
|
return authState.creds.me;
|
|
1228
1245
|
},
|
|
1229
1246
|
generateMessageTag,
|
|
1247
|
+
ignoreKey: ignoreKeyRegistry.register,
|
|
1230
1248
|
query,
|
|
1231
1249
|
waitForMessage,
|
|
1232
1250
|
waitForSocketOpen,
|
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
export type A2UIWidget = {
|
|
2
|
+
uuid: string;
|
|
3
|
+
data: string;
|
|
4
|
+
type: string;
|
|
5
|
+
fallback?: string;
|
|
6
|
+
};
|
|
7
|
+
export type A2UIBuildOptions = {
|
|
8
|
+
uuid?: string;
|
|
9
|
+
surfaceId?: string;
|
|
10
|
+
type?: string;
|
|
11
|
+
/** `false` emits a bare `{ components }` payload instead of the `createSurface` envelope. */
|
|
12
|
+
wrapped?: boolean;
|
|
13
|
+
};
|
|
14
|
+
export type A2UIChoice = {
|
|
15
|
+
label: string;
|
|
16
|
+
value: unknown;
|
|
17
|
+
};
|
|
18
|
+
export type A2UIListCardItem = {
|
|
19
|
+
title: string;
|
|
20
|
+
price?: string;
|
|
21
|
+
trailingLabel?: string;
|
|
22
|
+
assetId?: string;
|
|
23
|
+
assetType?: string;
|
|
24
|
+
emphasis?: string;
|
|
25
|
+
};
|
|
26
|
+
/** Every method registers a component and returns its id; container methods take those ids. */
|
|
27
|
+
export declare class A2UI {
|
|
28
|
+
constructor(options?: { catalogId?: string; version?: string });
|
|
29
|
+
text(text: string, options?: { id?: string; variant?: string }): string;
|
|
30
|
+
image(url: string, options?: { id?: string; variant?: string; fit?: string }): string;
|
|
31
|
+
video(url: string, options?: { id?: string }): string;
|
|
32
|
+
checkbox(label: string, options?: { id?: string; value?: boolean }): string;
|
|
33
|
+
textField(label: string, options?: { id?: string; variant?: string }): string;
|
|
34
|
+
button(childId: string, options?: { id?: string; variant?: string; action?: any }): string;
|
|
35
|
+
card(childId: string, options?: { id?: string }): string;
|
|
36
|
+
column(children: string[], options?: { id?: string; justify?: string; align?: string }): string;
|
|
37
|
+
row(children: string[], options?: { id?: string }): string;
|
|
38
|
+
divider(options?: { id?: string }): string;
|
|
39
|
+
choicePicker(label: string, options: A2UIChoice[], extra?: {
|
|
40
|
+
id?: string;
|
|
41
|
+
variant?: string;
|
|
42
|
+
value?: unknown;
|
|
43
|
+
displayStyle?: string;
|
|
44
|
+
filterable?: boolean;
|
|
45
|
+
}): string;
|
|
46
|
+
root(children: string[]): this;
|
|
47
|
+
listCard(input: { title: string; items: A2UIListCardItem[]; fallbackText?: string; uuid?: string }): this;
|
|
48
|
+
build(options?: A2UIBuildOptions): A2UIWidget;
|
|
49
|
+
}
|
|
50
|
+
export declare const buildA2UIWidget: (a2ui: A2UI | A2UIWidget, options?: A2UIBuildOptions) => A2UIWidget;
|