@japofc/baileys 2.1.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/NOTICE.md +111 -0
- package/README.md +1254 -0
- package/WAProto/index.d.ts +9 -0
- package/WAProto/index.js +142276 -0
- package/engine-requirements.js +13 -0
- package/lib/Builders/A2UI.d.ts +242 -0
- package/lib/Builders/A2UI.js +308 -0
- package/lib/Builders/AIRich.d.ts +105 -0
- package/lib/Builders/AIRich.js +2286 -0
- package/lib/Builders/Button.d.ts +103 -0
- package/lib/Builders/Button.js +883 -0
- package/lib/Builders/ButtonV2.d.ts +28 -0
- package/lib/Builders/ButtonV2.js +144 -0
- package/lib/Builders/ButtonV3.d.ts +25 -0
- package/lib/Builders/ButtonV3.js +184 -0
- package/lib/Builders/Carousel.d.ts +22 -0
- package/lib/Builders/Carousel.js +111 -0
- package/lib/Builders/JapBaileys.d.ts +34 -0
- package/lib/Builders/JapBaileys.js +108 -0
- package/lib/Builders/Poll.d.ts +25 -0
- package/lib/Builders/Poll.js +128 -0
- package/lib/Builders/index.d.ts +17 -0
- package/lib/Builders/index.js +11 -0
- package/lib/Builders/shared.d.ts +67 -0
- package/lib/Builders/shared.js +652 -0
- package/lib/Defaults/index.d.ts +154 -0
- package/lib/Defaults/index.js +178 -0
- package/lib/Framework/Bot.d.ts +61 -0
- package/lib/Framework/Bot.js +248 -0
- package/lib/Framework/Context.d.ts +21 -0
- package/lib/Framework/Context.js +89 -0
- package/lib/Framework/MediaManager.d.ts +17 -0
- package/lib/Framework/MediaManager.js +151 -0
- package/lib/Framework/SessionManager.d.ts +13 -0
- package/lib/Framework/SessionManager.js +33 -0
- package/lib/Framework/StatsManager.d.ts +26 -0
- package/lib/Framework/StatsManager.js +119 -0
- package/lib/Framework/Store/SQLiteStore.d.ts +11 -0
- package/lib/Framework/Store/SQLiteStore.js +73 -0
- package/lib/Framework/index.d.ts +9 -0
- package/lib/Framework/index.js +10 -0
- package/lib/Signal/Group/ciphertext-message.d.ts +9 -0
- package/lib/Signal/Group/ciphertext-message.js +11 -0
- package/lib/Signal/Group/group-session-builder.d.ts +7 -0
- package/lib/Signal/Group/group-session-builder.js +29 -0
- package/lib/Signal/Group/group_cipher.d.ts +10 -0
- package/lib/Signal/Group/group_cipher.js +81 -0
- package/lib/Signal/Group/index.d.ts +11 -0
- package/lib/Signal/Group/index.js +11 -0
- package/lib/Signal/Group/keyhelper.d.ts +6 -0
- package/lib/Signal/Group/keyhelper.js +17 -0
- package/lib/Signal/Group/sender-chain-key.d.ts +13 -0
- package/lib/Signal/Group/sender-chain-key.js +25 -0
- package/lib/Signal/Group/sender-key-distribution-message.d.ts +16 -0
- package/lib/Signal/Group/sender-key-distribution-message.js +62 -0
- package/lib/Signal/Group/sender-key-message.d.ts +18 -0
- package/lib/Signal/Group/sender-key-message.js +65 -0
- package/lib/Signal/Group/sender-key-name.d.ts +11 -0
- package/lib/Signal/Group/sender-key-name.js +47 -0
- package/lib/Signal/Group/sender-key-record.d.ts +12 -0
- package/lib/Signal/Group/sender-key-record.js +40 -0
- package/lib/Signal/Group/sender-key-state.d.ts +16 -0
- package/lib/Signal/Group/sender-key-state.js +83 -0
- package/lib/Signal/Group/sender-message-key.d.ts +11 -0
- package/lib/Signal/Group/sender-message-key.js +25 -0
- package/lib/Signal/libsignal.d.ts +54 -0
- package/lib/Signal/libsignal.js +430 -0
- package/lib/Signal/lid-mapping.d.ts +20 -0
- package/lib/Signal/lid-mapping.js +276 -0
- package/lib/Socket/Client/index.d.ts +2 -0
- package/lib/Socket/Client/index.js +2 -0
- package/lib/Socket/Client/types.d.ts +5 -0
- package/lib/Socket/Client/types.js +10 -0
- package/lib/Socket/Client/websocket.d.ts +12 -0
- package/lib/Socket/Client/websocket.js +53 -0
- package/lib/Socket/business.d.ts +395 -0
- package/lib/Socket/business.js +379 -0
- package/lib/Socket/chats.d.ts +136 -0
- package/lib/Socket/chats.js +1327 -0
- package/lib/Socket/communities.d.ts +562 -0
- package/lib/Socket/communities.js +439 -0
- package/lib/Socket/dugong.d.ts +117 -0
- package/lib/Socket/dugong.js +798 -0
- package/lib/Socket/groups.d.ts +285 -0
- package/lib/Socket/groups.js +354 -0
- package/lib/Socket/index.d.ts +585 -0
- package/lib/Socket/index.js +52 -0
- package/lib/Socket/messages-recv.d.ts +333 -0
- package/lib/Socket/messages-recv.js +1781 -0
- package/lib/Socket/messages-send.d.ts +353 -0
- package/lib/Socket/messages-send.js +1524 -0
- package/lib/Socket/mex.d.ts +1 -0
- package/lib/Socket/mex.js +41 -0
- package/lib/Socket/newsletter.d.ts +286 -0
- package/lib/Socket/newsletter.js +364 -0
- package/lib/Socket/socket.d.ts +60 -0
- package/lib/Socket/socket.js +1012 -0
- package/lib/Socket/username.d.ts +82 -0
- package/lib/Socket/username.js +159 -0
- package/lib/Store/index.d.ts +3 -0
- package/lib/Store/index.js +3 -0
- package/lib/Store/make-in-memory-store.d.ts +62 -0
- package/lib/Store/make-in-memory-store.js +428 -0
- package/lib/Store/make-ordered-dictionary.d.ts +12 -0
- package/lib/Store/make-ordered-dictionary.js +78 -0
- package/lib/Store/object-repository.d.ts +10 -0
- package/lib/Store/object-repository.js +23 -0
- package/lib/Types/Auth.d.ts +117 -0
- package/lib/Types/Auth.js +1 -0
- package/lib/Types/Bussines.d.ts +25 -0
- package/lib/Types/Call.d.ts +15 -0
- package/lib/Types/Call.js +1 -0
- package/lib/Types/Chat.d.ts +124 -0
- package/lib/Types/Chat.js +7 -0
- package/lib/Types/Contact.d.ts +26 -0
- package/lib/Types/Contact.js +1 -0
- package/lib/Types/Events.d.ts +256 -0
- package/lib/Types/Events.js +1 -0
- package/lib/Types/GroupMetadata.d.ts +71 -0
- package/lib/Types/GroupMetadata.js +1 -0
- package/lib/Types/Label.d.ts +5 -0
- package/lib/Types/Label.js +24 -0
- package/lib/Types/LabelAssociation.d.ts +6 -0
- package/lib/Types/LabelAssociation.js +6 -0
- package/lib/Types/Message.d.ts +62 -0
- package/lib/Types/Message.js +17 -0
- package/lib/Types/Mex.d.ts +2 -0
- package/lib/Types/Mex.js +38 -0
- package/lib/Types/Product.d.ts +79 -0
- package/lib/Types/Product.js +1 -0
- package/lib/Types/RichType.d.ts +2 -0
- package/lib/Types/RichType.js +22 -0
- package/lib/Types/Signal.d.ts +87 -0
- package/lib/Types/Signal.js +1 -0
- package/lib/Types/Socket.d.ts +138 -0
- package/lib/Types/Socket.js +1 -0
- package/lib/Types/State.d.ts +8 -0
- package/lib/Types/State.js +55 -0
- package/lib/Types/USync.d.ts +26 -0
- package/lib/Types/index.d.ts +68 -0
- package/lib/Types/index.js +27 -0
- package/lib/Utils/MessageBuilder.d.ts +2 -0
- package/lib/Utils/MessageBuilder.js +2 -0
- package/lib/Utils/PersistentStore.d.ts +60 -0
- package/lib/Utils/PersistentStore.js +592 -0
- package/lib/Utils/anti-delete.d.ts +74 -0
- package/lib/Utils/anti-delete.js +221 -0
- package/lib/Utils/auth-secure.d.ts +86 -0
- package/lib/Utils/auth-secure.js +764 -0
- package/lib/Utils/auth-utils.d.ts +57 -0
- package/lib/Utils/auth-utils.js +306 -0
- package/lib/Utils/auto-reply.d.ts +47 -0
- package/lib/Utils/auto-reply.js +157 -0
- package/lib/Utils/baileys-event-stream.d.ts +8 -0
- package/lib/Utils/baileys-event-stream.js +62 -0
- package/lib/Utils/browser-utils.d.ts +8 -0
- package/lib/Utils/browser-utils.js +27 -0
- package/lib/Utils/business.d.ts +49 -0
- package/lib/Utils/business.js +239 -0
- package/lib/Utils/button-helper-utils.d.ts +53 -0
- package/lib/Utils/button-helper-utils.js +312 -0
- package/lib/Utils/button-sender.d.ts +54 -0
- package/lib/Utils/button-sender.js +822 -0
- package/lib/Utils/chat-control.d.ts +159 -0
- package/lib/Utils/chat-control.js +230 -0
- package/lib/Utils/chat-history-helpers.d.ts +21 -0
- package/lib/Utils/chat-history-helpers.js +70 -0
- package/lib/Utils/chat-utils.d.ts +407 -0
- package/lib/Utils/chat-utils.js +885 -0
- package/lib/Utils/companion-reg-client-utils.d.ts +4 -0
- package/lib/Utils/companion-reg-client-utils.js +39 -0
- package/lib/Utils/crypto.d.ts +41 -0
- package/lib/Utils/crypto.js +147 -0
- package/lib/Utils/decode-wa-message.d.ts +83 -0
- package/lib/Utils/decode-wa-message.js +316 -0
- package/lib/Utils/event-buffer.d.ts +12 -0
- package/lib/Utils/event-buffer.js +635 -0
- package/lib/Utils/generics.d.ts +69 -0
- package/lib/Utils/generics.js +438 -0
- package/lib/Utils/history-share.d.ts +11 -0
- package/lib/Utils/history-share.js +70 -0
- package/lib/Utils/history.d.ts +90 -0
- package/lib/Utils/history.js +146 -0
- package/lib/Utils/humanizer.d.ts +12 -0
- package/lib/Utils/humanizer.js +73 -0
- package/lib/Utils/identity-change-handler.d.ts +13 -0
- package/lib/Utils/identity-change-handler.js +49 -0
- package/lib/Utils/index.d.ts +57 -0
- package/lib/Utils/index.js +56 -0
- package/lib/Utils/jid-tools.d.ts +6 -0
- package/lib/Utils/jid-tools.js +77 -0
- package/lib/Utils/link-preview.d.ts +12 -0
- package/lib/Utils/link-preview.js +84 -0
- package/lib/Utils/logger.d.ts +4 -0
- package/lib/Utils/logger.js +2 -0
- package/lib/Utils/lt-hash.d.ts +12 -0
- package/lib/Utils/lt-hash.js +26 -0
- package/lib/Utils/make-mutex.d.ts +6 -0
- package/lib/Utils/make-mutex.js +32 -0
- package/lib/Utils/media-messages.d.ts +18 -0
- package/lib/Utils/media-messages.js +70 -0
- package/lib/Utils/media-set.d.ts +13 -0
- package/lib/Utils/media-set.js +171 -0
- package/lib/Utils/message-kind.d.ts +32 -0
- package/lib/Utils/message-kind.js +136 -0
- package/lib/Utils/message-retry-manager.d.ts +79 -0
- package/lib/Utils/message-retry-manager.js +264 -0
- package/lib/Utils/message-search.d.ts +44 -0
- package/lib/Utils/message-search.js +188 -0
- package/lib/Utils/messages-media.d.ts +135 -0
- package/lib/Utils/messages-media.js +868 -0
- package/lib/Utils/messages.d.ts +44 -0
- package/lib/Utils/messages.js +2326 -0
- package/lib/Utils/meta-ai.d.ts +13 -0
- package/lib/Utils/meta-ai.js +74 -0
- package/lib/Utils/mini-app.d.ts +35 -0
- package/lib/Utils/mini-app.js +213 -0
- package/lib/Utils/noise-handler.d.ts +20 -0
- package/lib/Utils/noise-handler.js +200 -0
- package/lib/Utils/offline-node-processor.d.ts +9 -0
- package/lib/Utils/offline-node-processor.js +39 -0
- package/lib/Utils/past-participants.d.ts +14 -0
- package/lib/Utils/past-participants.js +40 -0
- package/lib/Utils/pre-key-manager.d.ts +25 -0
- package/lib/Utils/pre-key-manager.js +105 -0
- package/lib/Utils/process-message.d.ts +82 -0
- package/lib/Utils/process-message.js +808 -0
- package/lib/Utils/qr-render.d.ts +59 -0
- package/lib/Utils/qr-render.js +179 -0
- package/lib/Utils/qrcodegen.d.ts +79 -0
- package/lib/Utils/qrcodegen.js +848 -0
- package/lib/Utils/reporting-utils.d.ts +12 -0
- package/lib/Utils/reporting-utils.js +257 -0
- package/lib/Utils/rich-message-utils.d.ts +48 -0
- package/lib/Utils/rich-message-utils.js +444 -0
- package/lib/Utils/router.d.ts +37 -0
- package/lib/Utils/router.js +165 -0
- package/lib/Utils/scheduled-posts.d.ts +30 -0
- package/lib/Utils/scheduled-posts.js +110 -0
- package/lib/Utils/scheduling.d.ts +42 -0
- package/lib/Utils/scheduling.js +139 -0
- package/lib/Utils/signal.d.ts +82 -0
- package/lib/Utils/signal.js +200 -0
- package/lib/Utils/stanza-ack.d.ts +16 -0
- package/lib/Utils/stanza-ack.js +37 -0
- package/lib/Utils/status.d.ts +50 -0
- package/lib/Utils/status.js +107 -0
- package/lib/Utils/stickerpack.d.ts +51 -0
- package/lib/Utils/stickerpack.js +274 -0
- package/lib/Utils/sync-action-utils.d.ts +2 -0
- package/lib/Utils/sync-action-utils.js +48 -0
- package/lib/Utils/tag.d.ts +12 -0
- package/lib/Utils/tag.js +44 -0
- package/lib/Utils/tc-token-utils.d.ts +29 -0
- package/lib/Utils/tc-token-utils.js +162 -0
- package/lib/Utils/templates.d.ts +76 -0
- package/lib/Utils/templates.js +150 -0
- package/lib/Utils/transcribe.d.ts +12 -0
- package/lib/Utils/transcribe.js +107 -0
- package/lib/Utils/use-cache-manager-auth-state.d.ts +13 -0
- package/lib/Utils/use-cache-manager-auth-state.js +81 -0
- package/lib/Utils/use-multi-file-auth-state.d.ts +10 -0
- package/lib/Utils/use-multi-file-auth-state.js +166 -0
- package/lib/Utils/use-single-file-auth-state.d.ts +10 -0
- package/lib/Utils/use-single-file-auth-state.js +113 -0
- package/lib/Utils/use-sqlite-auth-state.d.ts +11 -0
- package/lib/Utils/use-sqlite-auth-state.js +169 -0
- package/lib/Utils/validate-connection.d.ts +42 -0
- package/lib/Utils/validate-connection.js +198 -0
- package/lib/Utils/vcard.d.ts +58 -0
- package/lib/Utils/vcard.js +104 -0
- package/lib/Utils/voice-note.d.ts +15 -0
- package/lib/Utils/voice-note.js +63 -0
- package/lib/VoIP/audio-feeder.d.ts +18 -0
- package/lib/VoIP/audio-feeder.js +160 -0
- package/lib/VoIP/call-recorder.d.ts +13 -0
- package/lib/VoIP/call-recorder.js +73 -0
- package/lib/VoIP/index.d.ts +86 -0
- package/lib/VoIP/index.js +611 -0
- package/lib/VoIP/relay-transport.d.ts +43 -0
- package/lib/VoIP/relay-transport.js +563 -0
- package/lib/VoIP/signaling.d.ts +26 -0
- package/lib/VoIP/signaling.js +662 -0
- package/lib/VoIP/types.d.ts +69 -0
- package/lib/VoIP/types.js +17 -0
- package/lib/VoIP/wasm-engine.d.ts +120 -0
- package/lib/VoIP/wasm-engine.js +1358 -0
- package/lib/VoIP/worker-bootstrap.d.ts +4 -0
- package/lib/VoIP/worker-bootstrap.js +1046 -0
- package/lib/WABinary/constants.d.ts +61 -0
- package/lib/WABinary/constants.js +1466 -0
- package/lib/WABinary/decode.d.ts +9 -0
- package/lib/WABinary/decode.js +261 -0
- package/lib/WABinary/encode.d.ts +2 -0
- package/lib/WABinary/encode.js +219 -0
- package/lib/WABinary/generic-utils.d.ts +74 -0
- package/lib/WABinary/generic-utils.js +276 -0
- package/lib/WABinary/index.d.ts +5 -0
- package/lib/WABinary/index.js +5 -0
- package/lib/WABinary/jid-utils.d.ts +28 -0
- package/lib/WABinary/jid-utils.js +95 -0
- package/lib/WABinary/types.d.ts +19 -0
- package/lib/WABinary/types.js +1 -0
- package/lib/WAM/BinaryInfo.d.ts +7 -0
- package/lib/WAM/BinaryInfo.js +9 -0
- package/lib/WAM/constants.d.ts +34926 -0
- package/lib/WAM/constants.js +22852 -0
- package/lib/WAM/encode.d.ts +1 -0
- package/lib/WAM/encode.js +149 -0
- package/lib/WAM/index.d.ts +3 -0
- package/lib/WAM/index.js +3 -0
- package/lib/WAUSync/Protocols/USyncContactProtocol.d.ts +36 -0
- package/lib/WAUSync/Protocols/USyncContactProtocol.js +51 -0
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.d.ts +29 -0
- package/lib/WAUSync/Protocols/USyncDeviceProtocol.js +62 -0
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.d.ts +12 -0
- package/lib/WAUSync/Protocols/USyncDisappearingModeProtocol.js +26 -0
- package/lib/WAUSync/Protocols/USyncStatusProtocol.d.ts +12 -0
- package/lib/WAUSync/Protocols/USyncStatusProtocol.js +37 -0
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.d.ts +9 -0
- package/lib/WAUSync/Protocols/USyncUsernameProtocol.js +24 -0
- package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.d.ts +38 -0
- package/lib/WAUSync/Protocols/UsyncBotProfileProtocol.js +50 -0
- package/lib/WAUSync/Protocols/UsyncLIDProtocol.d.ts +14 -0
- package/lib/WAUSync/Protocols/UsyncLIDProtocol.js +28 -0
- package/lib/WAUSync/Protocols/index.d.ts +7 -0
- package/lib/WAUSync/Protocols/index.js +7 -0
- package/lib/WAUSync/USyncQuery.d.ts +20 -0
- package/lib/WAUSync/USyncQuery.js +121 -0
- package/lib/WAUSync/USyncUser.d.ts +16 -0
- package/lib/WAUSync/USyncUser.js +30 -0
- package/lib/WAUSync/index.d.ts +3 -0
- package/lib/WAUSync/index.js +3 -0
- package/lib/assets/wasm/loader.d.ts +5 -0
- package/lib/assets/wasm/loader.js +5 -0
- package/lib/assets/wasm/whatsapp.wasm +0 -0
- package/lib/assets/wasm/worker-modules.d.ts +5 -0
- package/lib/assets/wasm/worker-modules.js +273 -0
- package/lib/index.d.ts +18 -0
- package/lib/index.js +23 -0
- package/package.json +180 -0
- package/postinstall-banner.js +82 -0
|
@@ -0,0 +1,2286 @@
|
|
|
1
|
+
import { BaseBuilder, Toolkit, extractIE, waitAllPromises, getSharp, getFfmpeg, botMetadataSignature, botMetadataCertificate, crypto, generateWAMessageFromContent, prepareWAMessageMedia, generateMessageIDV2 } from './shared.js';
|
|
2
|
+
class AIRich extends BaseBuilder {
|
|
3
|
+
#client;
|
|
4
|
+
|
|
5
|
+
constructor(client) {
|
|
6
|
+
if (!client) {
|
|
7
|
+
throw new Error('Socket is required');
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
super();
|
|
11
|
+
this.#client = client;
|
|
12
|
+
this._contextInfo = {};
|
|
13
|
+
this._submessages = [];
|
|
14
|
+
this._sections = [];
|
|
15
|
+
this._richResponseSources = [];
|
|
16
|
+
// JAP@Fix (bug 42 / inline image fallback): WA rejects rendering AIRichResponseInlineImageMetadata
|
|
17
|
+
// for third-party bots regardless of URL (confirmed empirically — Meta/WA CDN url with valid
|
|
18
|
+
// mediaKey still doesn't render, so it's a trust-chain gate, not a domain/encoding issue).
|
|
19
|
+
// Track every addInlineImage() call here so send() can fall back to a normal imageMessage.
|
|
20
|
+
this._inlineImages = [];
|
|
21
|
+
|
|
22
|
+
// JAP@Add 24-08-26 --- ported from temen's MessageBuilderV4.7 (setResponseId/setBotResponseId
|
|
23
|
+
// below), rewritten to fit this fork's conventions. build() used to always mint a fresh
|
|
24
|
+
// crypto.randomUUID() for both unifiedResponse.response_id and botMetadata.botResponseId on
|
|
25
|
+
// every call, with no way to reuse one — so a `sendEdit()`-style flow (rebuild the same
|
|
26
|
+
// message with updated content, same response_id, so WA patches it in place instead of
|
|
27
|
+
// showing a new message) was never actually possible despite being in the gist example.
|
|
28
|
+
// null here means "not pinned yet" — build() falls back to a fresh randomUUID() same as before
|
|
29
|
+
// when neither setResponseId() nor setBotResponseId() has been called.
|
|
30
|
+
this._responseId = null;
|
|
31
|
+
this._botResponseId = null;
|
|
32
|
+
|
|
33
|
+
// JAP@Add --- set by send()/sendEdit() after every relay so a follow-up sendEdit(), called
|
|
34
|
+
// with no args, knows which jid/message id to patch in place (matches temen's v4.7 API).
|
|
35
|
+
this._lastMessageKey = null;
|
|
36
|
+
|
|
37
|
+
// JAP@Add (v4.9.1) --- { id, insertAt } support for every add*()/set*() call, without
|
|
38
|
+
// touching each method's own body/signature. Every add*() call ends up pushing 0-N items
|
|
39
|
+
// onto _submessages and 0-N onto _sections (some push to both, some to just one — e.g.
|
|
40
|
+
// addSuggest only touches _submessages, addSection only touches _sections). A Proxy wraps
|
|
41
|
+
// every add*/set* call: it snapshots array lengths before calling the real method, lets the
|
|
42
|
+
// method push onto the tail as it always has, then — if the caller passed { insertAt } —
|
|
43
|
+
// peels those freshly-pushed items back off the tail and re-splices them right after the
|
|
44
|
+
// last item that belongs to the block named by insertAt. Blocks are tracked by *object
|
|
45
|
+
// reference*, not saved numeric index, so earlier insertions shifting the array around never
|
|
46
|
+
// invalidates a later insertAt lookup (indexOf on the reference always finds the live position).
|
|
47
|
+
//
|
|
48
|
+
// JAP@Note (bug 71, behavior — not fixed, documented) --- insertAt always inserts right after
|
|
49
|
+
// the ANCHOR's block, not after "whatever was most recently inserted there". Chaining (each new
|
|
50
|
+
// item gets its own id, and the next call's insertAt points at THAT id — exactly what the
|
|
51
|
+
// addText/addSuggest streaming-reveal example does) produces the expected order. But calling
|
|
52
|
+
// insertAt at the SAME static anchor id repeatedly, without giving each new item its own id to
|
|
53
|
+
// chain onto, inserts every one of them right after the original anchor — so the order comes out
|
|
54
|
+
// reversed relative to call order (confirmed by test: id:'x' then 3x insertAt:'x' with no id of
|
|
55
|
+
// their own on the new items produces [x, third, second, first], not [x, first, second, third]).
|
|
56
|
+
// Left as-is rather than "fixed": making insertAt self-advance (re-pointing the anchor's block at
|
|
57
|
+
// whatever was just inserted) would silently change what an id resolves to for any OTHER caller
|
|
58
|
+
// still holding that id for a later replace()/delete()/insertAt() — a subtler, harder-to-diagnose
|
|
59
|
+
// bug than the surprising-but-deterministic order this produces. Chain with fresh ids instead.
|
|
60
|
+
this._blocks = new Map(); // id -> { subItems: object[], secItems: object[] }
|
|
61
|
+
return new Proxy(this, {
|
|
62
|
+
get(target, prop, receiver) {
|
|
63
|
+
const orig = Reflect.get(target, prop, receiver);
|
|
64
|
+
if (typeof orig !== 'function') return orig;
|
|
65
|
+
|
|
66
|
+
// JAP@Fix 23-08-26 (part 2) --- the add*/set* filter below only wrapped methods whose
|
|
67
|
+
// name starts with "add"/"set". Everything else (send(), build(), ...) fell through to
|
|
68
|
+
// `return orig` unwrapped, so calling e.g. `richInstance.send(...)` still invoked the
|
|
69
|
+
// real method with `this` = the Proxy (`receiver`), hitting the exact same
|
|
70
|
+
// "Cannot read private member #client..." brand-check error the add*/set* fix was for —
|
|
71
|
+
// just one level up, in send()/build() themselves. Every function property now gets
|
|
72
|
+
// bound to `target` (the real instance) at minimum; add*/set* additionally get the
|
|
73
|
+
// insertAt/id bookkeeping below.
|
|
74
|
+
if (!/^(add|set)/.test(String(prop))) {
|
|
75
|
+
return (...args) => {
|
|
76
|
+
const result = orig.apply(target, args);
|
|
77
|
+
return result === target ? receiver : result;
|
|
78
|
+
};
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
return (...args) => {
|
|
82
|
+
const opts = args.find((a) => a && typeof a === 'object' && !Array.isArray(a) && !Buffer.isBuffer(a) && ('id' in a || 'insertAt' in a || 'replace' in a));
|
|
83
|
+
const id = opts?.id;
|
|
84
|
+
const insertAt = opts?.insertAt;
|
|
85
|
+
const replace = opts?.replace;
|
|
86
|
+
|
|
87
|
+
// JAP@Fix (bug 70) --- `id` reuse across two different add*/set* calls was silently
|
|
88
|
+
// accepted: target._blocks.set(id, ...) below just clobbers the previous registration,
|
|
89
|
+
// so the FIRST block with that id becomes an untracked ghost — still in _sections/
|
|
90
|
+
// _submessages (still renders), but no longer reachable via hasId/peek/delete/replace/
|
|
91
|
+
// insertAt (the id now only resolves to the second block). Confirmed by direct test:
|
|
92
|
+
// addText('first',{id:'dup'}); addText('second',{id:'dup'}) left both in the message
|
|
93
|
+
// but getIds() only ever had one 'dup', pointing at 'second'. Fail fast instead — same
|
|
94
|
+
// as re-registering the same id you're actively `replace`-ing (that's a legitimate
|
|
95
|
+
// "update this block, keep its id" call, not a collision).
|
|
96
|
+
if (id && target._blocks.has(id) && replace !== id) {
|
|
97
|
+
throw new Error(`add*/set*: id "${id}" is already registered — each id must be unique (pass { replace: "${id}" } to update that block instead, or use a different id)`);
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
const subBefore = target._submessages.length;
|
|
101
|
+
const secBefore = target._sections.length;
|
|
102
|
+
|
|
103
|
+
// JAP@Fix 23-08-26 --- was orig.apply(receiver, args): calling the real method bound to
|
|
104
|
+
// the Proxy itself (`receiver`) makes any `this.#client` access inside throw
|
|
105
|
+
// "Cannot read private member #client from an object whose class did not declare it",
|
|
106
|
+
// because a Proxy is never the branded instance a private field was declared on —
|
|
107
|
+
// this hit every add*() that touches #client via Toolkit.resolveMedia(this.#client, ...)
|
|
108
|
+
// (addProduct/addPost/addReels/addSource, and would eventually hit addImage/addVideo
|
|
109
|
+
// too once JIT/engine specifics changed). Binding to `target` (the real instance) instead
|
|
110
|
+
// fixes it for good; `target._submessages`/`target._sections` below are unaffected since
|
|
111
|
+
// they're plain properties, and `result === target ? receiver : result` still converts a
|
|
112
|
+
// `this`-return back to the Proxy so chaining (`.addX().addY()`) keeps working.
|
|
113
|
+
const result = orig.apply(target, args);
|
|
114
|
+
|
|
115
|
+
|
|
116
|
+
const subItems = target._submessages.splice(subBefore);
|
|
117
|
+
const secItems = target._sections.splice(secBefore);
|
|
118
|
+
|
|
119
|
+
if (insertAt) {
|
|
120
|
+
const anchor = target._blocks.get(insertAt);
|
|
121
|
+
if (!anchor) throw new Error(`insertAt: no block registered with id "${insertAt}" (register it by passing { id: "${insertAt}" } on an earlier add*() call)`);
|
|
122
|
+
|
|
123
|
+
const lastSub = anchor.subItems[anchor.subItems.length - 1];
|
|
124
|
+
const subIdx = lastSub ? target._submessages.indexOf(lastSub) + 1 : target._submessages.length;
|
|
125
|
+
target._submessages.splice(subIdx, 0, ...subItems);
|
|
126
|
+
|
|
127
|
+
const lastSec = anchor.secItems[anchor.secItems.length - 1];
|
|
128
|
+
const secIdx = lastSec ? target._sections.indexOf(lastSec) + 1 : target._sections.length;
|
|
129
|
+
target._sections.splice(secIdx, 0, ...secItems);
|
|
130
|
+
} else if (replace) {
|
|
131
|
+
// replace: delete the old block's items at their current positions,
|
|
132
|
+
// then insert new items at the same positions
|
|
133
|
+
const old = target._blocks.get(replace);
|
|
134
|
+
if (!old) throw new Error(`replace: no block registered with id "${replace}" (register it first with { id: "${replace}" })`);
|
|
135
|
+
|
|
136
|
+
let subIdx = old.subItems.length > 0 ? target._submessages.indexOf(old.subItems[0]) : target._submessages.length;
|
|
137
|
+
if (subIdx === -1) subIdx = target._submessages.length;
|
|
138
|
+
for (const item of old.subItems) {
|
|
139
|
+
const i = target._submessages.indexOf(item);
|
|
140
|
+
if (i !== -1) target._submessages.splice(i, 1);
|
|
141
|
+
}
|
|
142
|
+
target._submessages.splice(subIdx, 0, ...subItems);
|
|
143
|
+
|
|
144
|
+
let secIdx = old.secItems.length > 0 ? target._sections.indexOf(old.secItems[0]) : target._sections.length;
|
|
145
|
+
if (secIdx === -1) secIdx = target._sections.length;
|
|
146
|
+
for (const item of old.secItems) {
|
|
147
|
+
const i = target._sections.indexOf(item);
|
|
148
|
+
if (i !== -1) target._sections.splice(i, 1);
|
|
149
|
+
}
|
|
150
|
+
target._sections.splice(secIdx, 0, ...secItems);
|
|
151
|
+
|
|
152
|
+
target._blocks.delete(replace);
|
|
153
|
+
if (id) target._blocks.set(id, { subItems, secItems });
|
|
154
|
+
else target._blocks.set(replace, { subItems, secItems });
|
|
155
|
+
} else {
|
|
156
|
+
target._submessages.push(...subItems);
|
|
157
|
+
target._sections.push(...secItems);
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
if (id) target._blocks.set(id, { subItems, secItems });
|
|
161
|
+
|
|
162
|
+
return result === target ? receiver : result;
|
|
163
|
+
};
|
|
164
|
+
},
|
|
165
|
+
});
|
|
166
|
+
}
|
|
167
|
+
|
|
168
|
+
/** Flatten every primitive pushed into `_sections` so far into one array — lets you build a
|
|
169
|
+
* card set in one AIRich instance and re-embed it into another via addSection(AIRich.newLayout(...)). */
|
|
170
|
+
get items() {
|
|
171
|
+
return this._sections.flatMap((s) => {
|
|
172
|
+
const vm = s?.view_model;
|
|
173
|
+
if (!vm) return [];
|
|
174
|
+
return vm.primitives ?? (vm.primitive !== undefined ? [vm.primitive] : []);
|
|
175
|
+
});
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
/** Push a raw pre-built submessage block (escape hatch for shapes not covered by the add*() helpers). */
|
|
179
|
+
addSubmessage(submessage) {
|
|
180
|
+
const items = Array.isArray(submessage) ? submessage : [submessage];
|
|
181
|
+
|
|
182
|
+
for (const item of items) {
|
|
183
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item)) {
|
|
184
|
+
throw new TypeError('Submessage must be a plain object or array of plain objects');
|
|
185
|
+
}
|
|
186
|
+
|
|
187
|
+
this._submessages.push(item);
|
|
188
|
+
}
|
|
189
|
+
|
|
190
|
+
return this;
|
|
191
|
+
}
|
|
192
|
+
|
|
193
|
+
/** Push a raw pre-built section wrapper around one or more submessages. */
|
|
194
|
+
addSection(section) {
|
|
195
|
+
const items = Array.isArray(section) ? section : [section];
|
|
196
|
+
|
|
197
|
+
for (const item of items) {
|
|
198
|
+
if (typeof item !== 'object' || item === null || Array.isArray(item)) {
|
|
199
|
+
throw new TypeError('Section must be a plain object or array of plain objects');
|
|
200
|
+
}
|
|
201
|
+
|
|
202
|
+
this._sections.push(item);
|
|
203
|
+
}
|
|
204
|
+
|
|
205
|
+
return this;
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/** Add a text block. `[label](url)` becomes a hyperlink, `[](url)` a numbered citation, `[expr]<img-url>` a rendered latex expression — toggle each via the options. */
|
|
209
|
+
addText(text, { hyperlink = true, citation = true, latex = true } = {}) {
|
|
210
|
+
if (typeof text != 'string') {
|
|
211
|
+
throw new TypeError('Text must be a string');
|
|
212
|
+
}
|
|
213
|
+
|
|
214
|
+
const { text: extractedText, inline_entities } = extractIE(text, {
|
|
215
|
+
hyperlink,
|
|
216
|
+
citation,
|
|
217
|
+
latex,
|
|
218
|
+
});
|
|
219
|
+
|
|
220
|
+
this._submessages.push({
|
|
221
|
+
messageType: 2,
|
|
222
|
+
messageText: extractedText,
|
|
223
|
+
});
|
|
224
|
+
|
|
225
|
+
this._sections.push(
|
|
226
|
+
AIRich.newLayout('Single', {
|
|
227
|
+
text: extractedText,
|
|
228
|
+
...(inline_entities.length && {
|
|
229
|
+
inline_entities,
|
|
230
|
+
}),
|
|
231
|
+
__typename: 'GenAIMarkdownTextUXPrimitive',
|
|
232
|
+
})
|
|
233
|
+
);
|
|
234
|
+
|
|
235
|
+
return this;
|
|
236
|
+
}
|
|
237
|
+
|
|
238
|
+
/** Add a syntax-highlighted code block. @param {string} language e.g. 'javascript', 'python'. */
|
|
239
|
+
addCode(language, code) {
|
|
240
|
+
if (typeof language !== 'string' || typeof code !== 'string') {
|
|
241
|
+
throw new TypeError('Language and code must be a string');
|
|
242
|
+
}
|
|
243
|
+
|
|
244
|
+
const meta = AIRich.tokenizer(code, language);
|
|
245
|
+
|
|
246
|
+
this._submessages.push({
|
|
247
|
+
messageType: 5,
|
|
248
|
+
codeMetadata: {
|
|
249
|
+
codeLanguage: language,
|
|
250
|
+
codeBlocks: meta.codeBlock,
|
|
251
|
+
},
|
|
252
|
+
});
|
|
253
|
+
|
|
254
|
+
this._sections.push(
|
|
255
|
+
AIRich.newLayout('Single', {
|
|
256
|
+
language,
|
|
257
|
+
code_blocks: meta.unified_codeBlock,
|
|
258
|
+
__typename: 'GenAICodeUXPrimitive',
|
|
259
|
+
})
|
|
260
|
+
);
|
|
261
|
+
|
|
262
|
+
return this;
|
|
263
|
+
}
|
|
264
|
+
|
|
265
|
+
/** Add a table. @param {string[][]} table Row-major grid, first row treated as the header. */
|
|
266
|
+
addTable(table, { hyperlink = true, citation = true, latex = true } = {}) {
|
|
267
|
+
if (!Array.isArray(table)) {
|
|
268
|
+
throw new TypeError('Table must be an array');
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
const meta = AIRich.toTableMetadata(table, { hyperlink, citation, latex });
|
|
272
|
+
|
|
273
|
+
this._submessages.push({
|
|
274
|
+
messageType: 4,
|
|
275
|
+
tableMetadata: {
|
|
276
|
+
title: meta.title,
|
|
277
|
+
rows: meta.rows,
|
|
278
|
+
},
|
|
279
|
+
});
|
|
280
|
+
|
|
281
|
+
this._sections.push(
|
|
282
|
+
AIRich.newLayout('Single', {
|
|
283
|
+
rows: meta.unified_rows,
|
|
284
|
+
__typename: 'GenATableUXPrimitive',
|
|
285
|
+
})
|
|
286
|
+
);
|
|
287
|
+
|
|
288
|
+
return this;
|
|
289
|
+
}
|
|
290
|
+
|
|
291
|
+
/** Add a "Sources" strip. @param {string[]|string[][]} sources Flat list of urls, or `[title, url]` pairs. */
|
|
292
|
+
|
|
293
|
+
/** Build rich-response citation/link submessages using the same shape as Baileys' `links` content shortcut. */
|
|
294
|
+
addLinks(links = []) {
|
|
295
|
+
if (!Array.isArray(links)) throw new TypeError('links must be an array');
|
|
296
|
+
links.forEach((linkField, index) => {
|
|
297
|
+
if (!linkField || typeof linkField !== 'object') throw new TypeError('Each link must be an object');
|
|
298
|
+
const prefix = 'SS_' + index;
|
|
299
|
+
const url = linkField.url || '';
|
|
300
|
+
const text = String(linkField.text ?? '');
|
|
301
|
+
const sources = Array.isArray(linkField.sources) ? linkField.sources.map((sourceField) => ({
|
|
302
|
+
source_type: 'THIRD_PARTY',
|
|
303
|
+
source_display_name: sourceField?.displayName || sourceField?.title || 'Source',
|
|
304
|
+
source_subtitle: sourceField?.subtitle || '',
|
|
305
|
+
source_url: sourceField?.url || url,
|
|
306
|
+
})) : [];
|
|
307
|
+
const entity = {
|
|
308
|
+
key: prefix,
|
|
309
|
+
metadata: {
|
|
310
|
+
reference_id: index + 1,
|
|
311
|
+
reference_url: url,
|
|
312
|
+
reference_title: linkField.title || 'Source',
|
|
313
|
+
reference_display_name: linkField.displayName || linkField.title || 'Source',
|
|
314
|
+
sources,
|
|
315
|
+
__typename: 'GenAISearchCitationItem',
|
|
316
|
+
},
|
|
317
|
+
};
|
|
318
|
+
const section = AIRich.newLayout('Single', {
|
|
319
|
+
text: `${text} {{${prefix}}}${url}{{/${prefix}}}`,
|
|
320
|
+
inline_entities: [entity],
|
|
321
|
+
__typename: 'GenAIMarkdownTextUXPrimitive',
|
|
322
|
+
});
|
|
323
|
+
this._sections.push(section);
|
|
324
|
+
this._submessages.push({
|
|
325
|
+
messageType: 2,
|
|
326
|
+
messageText: `${text} {{${prefix}}}¹{{/${prefix}}} `,
|
|
327
|
+
inlineEntities: [entity],
|
|
328
|
+
});
|
|
329
|
+
});
|
|
330
|
+
return this;
|
|
331
|
+
}
|
|
332
|
+
|
|
333
|
+
/** Add a raw rich-response content-items carousel, matching Baileys' `items` field. */
|
|
334
|
+
addContentItems(items = []) {
|
|
335
|
+
if (!Array.isArray(items)) throw new TypeError('items must be an array');
|
|
336
|
+
this._submessages.push({
|
|
337
|
+
messageType: 9,
|
|
338
|
+
contentItemsMetadata: { itemsMetadata: items, contentType: 1 },
|
|
339
|
+
});
|
|
340
|
+
this._sections.push(AIRich.newLayout('Single', {
|
|
341
|
+
items,
|
|
342
|
+
content_type: 1,
|
|
343
|
+
__typename: 'GenAIContentItemsUXPrimitive',
|
|
344
|
+
}));
|
|
345
|
+
return this;
|
|
346
|
+
}
|
|
347
|
+
|
|
348
|
+
/** Add Baileys-compatible inline-video marker. WhatsApp's current rich-response helper carries this as a text marker. */
|
|
349
|
+
addInlineVideo() {
|
|
350
|
+
this._submessages.push({ messageType: 2, messageText: 'INLINE_VIDEO' });
|
|
351
|
+
this._sections.push(AIRich.newLayout('Single', {
|
|
352
|
+
text: 'INLINE_VIDEO',
|
|
353
|
+
__typename: 'GenAIMarkdownTextUXPrimitive',
|
|
354
|
+
}));
|
|
355
|
+
return this;
|
|
356
|
+
}
|
|
357
|
+
|
|
358
|
+
addSource(sources = [], { resolveUrl = false } = {}) {
|
|
359
|
+
// Accept 3 formats:
|
|
360
|
+
// 1. Array of objects: [{ icon, url, title, subtitle }] — from v4.7 example
|
|
361
|
+
// 2. Array of string arrays: [['iconUrl', 'url', 'text']]
|
|
362
|
+
// 3. Single string array (shorthand for format 2): ['iconUrl', 'url', 'text']
|
|
363
|
+
const isObjArray = Array.isArray(sources) && sources.every((item) => item && typeof item === 'object' && !Array.isArray(item));
|
|
364
|
+
const isStrArrayArray = Array.isArray(sources) && sources.every((item) => Array.isArray(item) && item.every((v) => typeof v === 'string'));
|
|
365
|
+
const isFlatStrArray = Array.isArray(sources) && sources.every((item) => typeof item === 'string');
|
|
366
|
+
|
|
367
|
+
if (!isObjArray && !isStrArrayArray && !isFlatStrArray) {
|
|
368
|
+
throw new TypeError('addSource(): pass an array of objects { icon, url, title, subtitle } or string arrays [iconUrl, url, text]');
|
|
369
|
+
}
|
|
370
|
+
|
|
371
|
+
let normalized;
|
|
372
|
+
if (isObjArray) {
|
|
373
|
+
normalized = sources.map((item) => ({
|
|
374
|
+
icon: item.icon ?? item.iconUrl ?? item.favicon ?? '',
|
|
375
|
+
url: item.url ?? '',
|
|
376
|
+
text: item.title ?? item.displayName ?? item.text ?? '',
|
|
377
|
+
subtitle: item.subtitle ?? 'AI',
|
|
378
|
+
}));
|
|
379
|
+
} else {
|
|
380
|
+
const arr = isFlatStrArray ? [sources] : sources;
|
|
381
|
+
normalized = arr.map(([icon = '', url = '', text = '']) => ({ icon, url, text, subtitle: 'AI' }));
|
|
382
|
+
}
|
|
383
|
+
|
|
384
|
+
const source = normalized.map(({ icon, url, text, subtitle }) => ({
|
|
385
|
+
source_type: 'THIRD_PARTY',
|
|
386
|
+
source_display_name: text,
|
|
387
|
+
source_subtitle: subtitle,
|
|
388
|
+
source_url: url,
|
|
389
|
+
favicon: {
|
|
390
|
+
url: Toolkit.resolveMedia(this.#client, icon, 'image', { resolveUrl }),
|
|
391
|
+
mime_type: 'image/jpeg',
|
|
392
|
+
width: 16,
|
|
393
|
+
height: 16,
|
|
394
|
+
},
|
|
395
|
+
}));
|
|
396
|
+
|
|
397
|
+
this._sections.push(
|
|
398
|
+
AIRich.newLayout('Single', {
|
|
399
|
+
sources: source,
|
|
400
|
+
__typename: 'GenAISearchResultPrimitive',
|
|
401
|
+
})
|
|
402
|
+
);
|
|
403
|
+
|
|
404
|
+
return this;
|
|
405
|
+
}
|
|
406
|
+
|
|
407
|
+
/** Add a horizontally-scrollable reel of image/video items. */
|
|
408
|
+
addReels(reelsItems = [], { resolveUrl = false } = {}) {
|
|
409
|
+
if (
|
|
410
|
+
!(
|
|
411
|
+
(reelsItems && typeof reelsItems === 'object' && !Array.isArray(reelsItems)) ||
|
|
412
|
+
(Array.isArray(reelsItems) && reelsItems.every((item) => item && typeof item === 'object' && !Array.isArray(item)))
|
|
413
|
+
)
|
|
414
|
+
) {
|
|
415
|
+
throw new TypeError('Reels items must be an object or an array of objects');
|
|
416
|
+
}
|
|
417
|
+
|
|
418
|
+
if (!Array.isArray(reelsItems)) {
|
|
419
|
+
reelsItems = [reelsItems];
|
|
420
|
+
}
|
|
421
|
+
|
|
422
|
+
const reels = reelsItems.map((item) => ({
|
|
423
|
+
...item,
|
|
424
|
+
_avatar: Toolkit.resolveMedia(this.#client, item.profileIconUrl ?? item.profile_url ?? item.profile ?? '', 'image', { resolveUrl }),
|
|
425
|
+
_thumbnail: Toolkit.resolveMedia(this.#client, item.thumbnailUrl ?? item.thumbnail ?? '', 'image', { resolveUrl }),
|
|
426
|
+
}));
|
|
427
|
+
|
|
428
|
+
this._submessages.push({
|
|
429
|
+
messageType: 9,
|
|
430
|
+
contentItemsMetadata: {
|
|
431
|
+
contentType: 1,
|
|
432
|
+
itemsMetadata: reels.map((item) => ({
|
|
433
|
+
reelItem: {
|
|
434
|
+
title: item.username ?? '',
|
|
435
|
+
profileIconUrl: item._avatar,
|
|
436
|
+
thumbnailUrl: item._thumbnail,
|
|
437
|
+
videoUrl: item.videoUrl ?? item.url ?? '',
|
|
438
|
+
},
|
|
439
|
+
})),
|
|
440
|
+
},
|
|
441
|
+
});
|
|
442
|
+
|
|
443
|
+
reels.forEach((item, idx) => {
|
|
444
|
+
this._richResponseSources.push({
|
|
445
|
+
provider: 'Evernight AI',
|
|
446
|
+
thumbnailCDNURL: item._thumbnail,
|
|
447
|
+
sourceProviderURL: item.videoUrl ?? item.url ?? '',
|
|
448
|
+
sourceQuery: '',
|
|
449
|
+
faviconCDNURL: item._avatar,
|
|
450
|
+
citationNumber: idx + 1,
|
|
451
|
+
sourceTitle: item.username ?? '',
|
|
452
|
+
});
|
|
453
|
+
});
|
|
454
|
+
|
|
455
|
+
this._sections.push(
|
|
456
|
+
AIRich.newLayout(
|
|
457
|
+
'HScroll',
|
|
458
|
+
reels.map((item) => ({
|
|
459
|
+
reels_url: item.videoUrl ?? item.url ?? '',
|
|
460
|
+
thumbnail_url: item._thumbnail,
|
|
461
|
+
creator: item.username ?? item.title ?? '',
|
|
462
|
+
avatar_url: item._avatar,
|
|
463
|
+
reels_title: item.reels_title ?? item.title ?? '',
|
|
464
|
+
likes_count: item.likes_count ?? item.like ?? 0,
|
|
465
|
+
shares_count: item.shares_count ?? item.share ?? 0,
|
|
466
|
+
view_count: item.view_count ?? item.view ?? 0,
|
|
467
|
+
reel_source: item.reel_source ?? item.source ?? 'IG',
|
|
468
|
+
is_verified: !!(item.is_verified || item.verified),
|
|
469
|
+
__typename: 'GenAIReelPrimitive',
|
|
470
|
+
}))
|
|
471
|
+
)
|
|
472
|
+
);
|
|
473
|
+
|
|
474
|
+
return this;
|
|
475
|
+
}
|
|
476
|
+
|
|
477
|
+
/** Add a full-width image (or grid of images if `imageUrl` is an array). */
|
|
478
|
+
/**
|
|
479
|
+
* @param {{ resolveUrl?: boolean, instant?: boolean|'only' }} [options]
|
|
480
|
+
* `instant: true` — sends BOTH: the GRID_IMAGE card (still shows WA's "can't verify"
|
|
481
|
+
* forwarded-download prompt, unavoidable per-design of botForwardedMessage) AND a plain
|
|
482
|
+
* (non-forwarded) imageMessage via send()'s inline-image fallback queue (`_inlineImages`,
|
|
483
|
+
* shared with addInlineImage()) that renders instantly with no prompt. Two images, by design.
|
|
484
|
+
* `instant: 'only'` — JAP@Add (v4.9.2): skips building the GRID_IMAGE card entirely (no
|
|
485
|
+
* submessage, no GenAIImaginePrimitive section) and queues ONLY the plain imageMessage.
|
|
486
|
+
* One image, no prompt, nothing to download — use this when you don't need the rich card,
|
|
487
|
+
* just the picture to show up immediately.
|
|
488
|
+
*/
|
|
489
|
+
addImage(imageUrl, { resolveUrl = false, instant = false } = {}) {
|
|
490
|
+
if (!(typeof imageUrl === 'string' || Buffer.isBuffer(imageUrl) || (Array.isArray(imageUrl) && imageUrl.every((v) => typeof v === 'string' || Buffer.isBuffer(v))))) {
|
|
491
|
+
throw new TypeError('imageUrl must be string | buffer | array of string/buffer');
|
|
492
|
+
}
|
|
493
|
+
if (instant !== false && instant !== true && instant !== 'only') {
|
|
494
|
+
throw new TypeError(`instant must be false, true, or 'only' — got ${JSON.stringify(instant)}`);
|
|
495
|
+
}
|
|
496
|
+
|
|
497
|
+
const list = Array.isArray(imageUrl)
|
|
498
|
+
? imageUrl.map((v) => {
|
|
499
|
+
const url = Toolkit.resolveMedia(this.#client, v, 'image', { resolveUrl });
|
|
500
|
+
return {
|
|
501
|
+
imagePreviewUrl: url,
|
|
502
|
+
imageHighResUrl: url,
|
|
503
|
+
sourceUrl: url,
|
|
504
|
+
};
|
|
505
|
+
})
|
|
506
|
+
: (() => {
|
|
507
|
+
const url = Toolkit.resolveMedia(this.#client, imageUrl, 'image', { resolveUrl });
|
|
508
|
+
return [
|
|
509
|
+
{
|
|
510
|
+
imagePreviewUrl: url,
|
|
511
|
+
imageHighResUrl: url,
|
|
512
|
+
sourceUrl: url,
|
|
513
|
+
},
|
|
514
|
+
];
|
|
515
|
+
})();
|
|
516
|
+
|
|
517
|
+
const buildCard = instant !== 'only';
|
|
518
|
+
|
|
519
|
+
if (buildCard) {
|
|
520
|
+
this._submessages.push({
|
|
521
|
+
messageType: 1,
|
|
522
|
+
gridImageMetadata: {
|
|
523
|
+
gridImageUrl: {
|
|
524
|
+
imagePreviewUrl: list[0]?.imagePreviewUrl,
|
|
525
|
+
},
|
|
526
|
+
imageUrls: list,
|
|
527
|
+
},
|
|
528
|
+
});
|
|
529
|
+
}
|
|
530
|
+
|
|
531
|
+
list.forEach(({ imagePreviewUrl }) => {
|
|
532
|
+
if (buildCard) {
|
|
533
|
+
this._sections.push(
|
|
534
|
+
AIRich.newLayout('Single', {
|
|
535
|
+
media: {
|
|
536
|
+
url: imagePreviewUrl,
|
|
537
|
+
mime_type: 'image/png',
|
|
538
|
+
},
|
|
539
|
+
imagine_type: 'IMAGE',
|
|
540
|
+
status: { status: 'READY' },
|
|
541
|
+
__typename: 'GenAIImaginePrimitive',
|
|
542
|
+
})
|
|
543
|
+
);
|
|
544
|
+
}
|
|
545
|
+
|
|
546
|
+
if (instant) {
|
|
547
|
+
this._inlineImages.push({ url: imagePreviewUrl, caption: undefined });
|
|
548
|
+
}
|
|
549
|
+
});
|
|
550
|
+
|
|
551
|
+
return this;
|
|
552
|
+
}
|
|
553
|
+
|
|
554
|
+
// JAP@Fix 15-08-26 (bug 41) --- addImage() only builds GRID_IMAGE (messageType 1).
|
|
555
|
+
// There was no helper for standalone INLINE_IMAGE (messageType 3): callers were manually
|
|
556
|
+
// pushing addSubmessage() (correct proto shape) + addSection() (WRONG shape — reused the
|
|
557
|
+
// GRID_IMAGE/GenAIImaginePrimitive section schema instead of GenAIInlineImageUXPrimitive),
|
|
558
|
+
// which broke client-side unifiedResponse rendering even though the submessage itself was fine.
|
|
559
|
+
// Mirrors RichSubMessageType.INLINE_IMAGE handling in rich-message-utils.js's toUnified().
|
|
560
|
+
/** Add an image inline with the surrounding text flow (falls back to a plain imageMessage on send() if the client can't render inline images — see skipImageFallback). */
|
|
561
|
+
addInlineImage(imageUrl, { text = '', alignment = 'center', tapLinkUrl = '', resolveUrl = false } = {}) {
|
|
562
|
+
if (!(typeof imageUrl === 'string' || Buffer.isBuffer(imageUrl) || (imageUrl && typeof imageUrl === 'object'))) {
|
|
563
|
+
throw new TypeError('imageUrl must be string | buffer | { imagePreviewUrl, imageHighResUrl, sourceUrl }');
|
|
564
|
+
}
|
|
565
|
+
|
|
566
|
+
const ALIGNMENT_ENUM = { leading: 0, trailing: 1, center: 2 };
|
|
567
|
+
const ALIGNMENT_NAME = ['AI_RICH_RESPONSE_IMAGE_LAYOUT_LEADING_ALIGNED', 'AI_RICH_RESPONSE_IMAGE_LAYOUT_TRAILING_ALIGNED', 'AI_RICH_RESPONSE_IMAGE_LAYOUT_CENTER_ALIGNED'];
|
|
568
|
+
const alignmentNum = typeof alignment === 'number' ? alignment : (ALIGNMENT_ENUM[String(alignment).toLowerCase()] ?? ALIGNMENT_ENUM.center);
|
|
569
|
+
|
|
570
|
+
const url =
|
|
571
|
+
imageUrl && typeof imageUrl === 'object'
|
|
572
|
+
? {
|
|
573
|
+
imagePreviewUrl: imageUrl.imagePreviewUrl || imageUrl.url,
|
|
574
|
+
imageHighResUrl: imageUrl.imageHighResUrl || imageUrl.url,
|
|
575
|
+
sourceUrl: imageUrl.sourceUrl || imageUrl.url,
|
|
576
|
+
}
|
|
577
|
+
: (() => {
|
|
578
|
+
const resolved = Toolkit.resolveMedia(this.#client, imageUrl, 'image', { resolveUrl });
|
|
579
|
+
return { imagePreviewUrl: resolved, imageHighResUrl: resolved, sourceUrl: resolved };
|
|
580
|
+
})();
|
|
581
|
+
|
|
582
|
+
this._submessages.push({
|
|
583
|
+
messageType: 3,
|
|
584
|
+
imageMetadata: {
|
|
585
|
+
imageUrl: url,
|
|
586
|
+
imageText: text,
|
|
587
|
+
alignment: alignmentNum,
|
|
588
|
+
tapLinkUrl,
|
|
589
|
+
},
|
|
590
|
+
});
|
|
591
|
+
|
|
592
|
+
this._sections.push(
|
|
593
|
+
AIRich.newLayout('Single', {
|
|
594
|
+
image_url: {
|
|
595
|
+
image_preview_url: url.imagePreviewUrl || '',
|
|
596
|
+
image_high_res_url: url.imageHighResUrl || '',
|
|
597
|
+
source_url: url.sourceUrl || '',
|
|
598
|
+
},
|
|
599
|
+
image_text: text,
|
|
600
|
+
alignment: ALIGNMENT_NAME[alignmentNum],
|
|
601
|
+
tap_link_url: tapLinkUrl,
|
|
602
|
+
__typename: 'GenAIInlineImageUXPrimitive',
|
|
603
|
+
})
|
|
604
|
+
);
|
|
605
|
+
|
|
606
|
+
// JAP@Fix (bug 42): stash for the imageMessage fallback in send()
|
|
607
|
+
this._inlineImages.push({
|
|
608
|
+
url: url.sourceUrl || url.imageHighResUrl || url.imagePreviewUrl,
|
|
609
|
+
caption: text || undefined,
|
|
610
|
+
});
|
|
611
|
+
|
|
612
|
+
return this;
|
|
613
|
+
}
|
|
614
|
+
|
|
615
|
+
// JAP@Perf 15-08-26 --- autoFill defaults to false (arslan-baileys behavior): skips the
|
|
616
|
+
// fetch-full-video + ffmpeg-frame-extraction + duration-parse round trip per video, which
|
|
617
|
+
// was the main source of blurose's slower response time. Pass { autoFill: true } to opt
|
|
618
|
+
// back into the complete/slow path (real thumbnail + duration + file_length).
|
|
619
|
+
// JAP@Fix 23-08-26 --- addVideo() had no resolveUrl option at all (unlike addImage()), so the
|
|
620
|
+
// video url always stayed a raw external link, which stock WA clients show a "download" state
|
|
621
|
+
// for before rendering. Mirrors addImage()'s { resolveUrl } — when true, the url is uploaded to
|
|
622
|
+
// WA's own media server first via Toolkit.toUrl() so it renders instantly like WA-native media.
|
|
623
|
+
/** Add a video block. */
|
|
624
|
+
addVideo(videoUrl, { autoFill = false, resolveUrl = false } = {}) {
|
|
625
|
+
const isObjectVideo = (v) => v && typeof v === 'object' && v.url;
|
|
626
|
+
|
|
627
|
+
const isValidPrimitive =
|
|
628
|
+
typeof videoUrl === 'string' ||
|
|
629
|
+
Buffer.isBuffer(videoUrl) ||
|
|
630
|
+
isObjectVideo(videoUrl) ||
|
|
631
|
+
(Array.isArray(videoUrl) && videoUrl.every((v) => typeof v === 'string' || Buffer.isBuffer(v) || isObjectVideo(v)));
|
|
632
|
+
|
|
633
|
+
if (!isValidPrimitive) {
|
|
634
|
+
throw new TypeError('videoUrl must be string | buffer | object | array');
|
|
635
|
+
}
|
|
636
|
+
|
|
637
|
+
const items = Array.isArray(videoUrl) ? videoUrl : [videoUrl];
|
|
638
|
+
|
|
639
|
+
this._submessages.push({
|
|
640
|
+
messageType: 2,
|
|
641
|
+
messageText: '[ Video tidak dapat dimuat ]',
|
|
642
|
+
});
|
|
643
|
+
|
|
644
|
+
items.forEach((item) => {
|
|
645
|
+
const isObject = isObjectVideo(item);
|
|
646
|
+
|
|
647
|
+
const url = isObject
|
|
648
|
+
? Toolkit.resolveMedia(this.#client, item.url ?? '', 'video', { resolveUrl })
|
|
649
|
+
: Toolkit.resolveMedia(this.#client, item, 'video', { resolveUrl });
|
|
650
|
+
|
|
651
|
+
const bufferPromise = autoFill ? Promise.resolve(url).then((u) => Toolkit.fetchBuffer(u)) : null;
|
|
652
|
+
|
|
653
|
+
const file_length = isObject && item.file_length != null ? item.file_length : autoFill ? bufferPromise.then((b) => b?.length ?? 0) : 0;
|
|
654
|
+
|
|
655
|
+
const duration =
|
|
656
|
+
isObject && item.duration != null
|
|
657
|
+
? item.duration
|
|
658
|
+
: autoFill
|
|
659
|
+
? bufferPromise.then((b) =>
|
|
660
|
+
Toolkit.getMp4Duration(b, {
|
|
661
|
+
silent: true,
|
|
662
|
+
})
|
|
663
|
+
)
|
|
664
|
+
: 0;
|
|
665
|
+
|
|
666
|
+
const thumbnail =
|
|
667
|
+
isObject && item.thumbnail
|
|
668
|
+
? Toolkit.resolveMedia(this.#client, item.thumbnail, 'image', {
|
|
669
|
+
result: 'base64',
|
|
670
|
+
resize: true,
|
|
671
|
+
width: 300,
|
|
672
|
+
height: 300,
|
|
673
|
+
})
|
|
674
|
+
: autoFill
|
|
675
|
+
? bufferPromise
|
|
676
|
+
? bufferPromise.then((b) =>
|
|
677
|
+
Toolkit.getMp4Preview(b, {
|
|
678
|
+
time: 0,
|
|
679
|
+
result: 'base64',
|
|
680
|
+
})
|
|
681
|
+
)
|
|
682
|
+
: null
|
|
683
|
+
: null;
|
|
684
|
+
|
|
685
|
+
this._sections.push(
|
|
686
|
+
AIRich.newLayout('Single', {
|
|
687
|
+
media: {
|
|
688
|
+
url,
|
|
689
|
+
mime_type: isObject ? (item.mime_type ?? 'video/mp4') : 'video/mp4',
|
|
690
|
+
file_length,
|
|
691
|
+
duration,
|
|
692
|
+
},
|
|
693
|
+
imagine_type: 'ANIMATE',
|
|
694
|
+
status: { status: 'READY' },
|
|
695
|
+
thumbnail: {
|
|
696
|
+
raw_media: thumbnail,
|
|
697
|
+
},
|
|
698
|
+
__typename: 'GenAIImaginePrimitive',
|
|
699
|
+
})
|
|
700
|
+
);
|
|
701
|
+
});
|
|
702
|
+
|
|
703
|
+
return this;
|
|
704
|
+
}
|
|
705
|
+
|
|
706
|
+
/** Add an inline product card (or array of cards). Each item needs at least a `title`. */
|
|
707
|
+
addProduct(data = {}, { resolveUrl = false } = {}) {
|
|
708
|
+
if (!((data && typeof data === 'object' && !Array.isArray(data)) || (Array.isArray(data) && data.every((item) => item && typeof item === 'object' && !Array.isArray(item))))) {
|
|
709
|
+
throw new TypeError('Product items must be an object or an array of objects');
|
|
710
|
+
}
|
|
711
|
+
|
|
712
|
+
const itemsToCheck = Array.isArray(data) ? data : [data];
|
|
713
|
+
const missingTitleAt = itemsToCheck.findIndex((item) => !item.title);
|
|
714
|
+
if (missingTitleAt !== -1) {
|
|
715
|
+
throw new TypeError(`addProduct() item[${missingTitleAt}] is missing a required "title"`);
|
|
716
|
+
}
|
|
717
|
+
|
|
718
|
+
this._submessages.push({
|
|
719
|
+
messageType: 2,
|
|
720
|
+
messageText: '[ Produk tidak dapat dimuat ]',
|
|
721
|
+
});
|
|
722
|
+
|
|
723
|
+
const items = Array.isArray(data) ? data : [data];
|
|
724
|
+
|
|
725
|
+
const product = items.map((item) => ({
|
|
726
|
+
title: item.title,
|
|
727
|
+
brand: item.brand,
|
|
728
|
+
price: item.price,
|
|
729
|
+
sale_price: item.sale_price,
|
|
730
|
+
product_url: item.product_url ?? item.url,
|
|
731
|
+
image: {
|
|
732
|
+
url: Toolkit.resolveMedia(this.#client, item.image_url ?? item.image, 'image', { resolveUrl }),
|
|
733
|
+
},
|
|
734
|
+
additional_images: [
|
|
735
|
+
{
|
|
736
|
+
url: Toolkit.resolveMedia(this.#client, item.icon_url ?? item.icon, 'image', { resolveUrl }),
|
|
737
|
+
},
|
|
738
|
+
],
|
|
739
|
+
__typename: 'GenAIProductItemCardPrimitive',
|
|
740
|
+
}));
|
|
741
|
+
|
|
742
|
+
this._sections.push(AIRich.newLayout(Array.isArray(data) ? 'HScroll' : 'Single', Array.isArray(data) ? product : product[0]));
|
|
743
|
+
|
|
744
|
+
return this;
|
|
745
|
+
}
|
|
746
|
+
|
|
747
|
+
/** Add an inline social-post style card (or array of cards). */
|
|
748
|
+
addPost(data = {}, { resolveUrl = false } = {}) {
|
|
749
|
+
if (!((data && typeof data === 'object' && !Array.isArray(data)) || (Array.isArray(data) && data.every((item) => item && typeof item === 'object' && !Array.isArray(item))))) {
|
|
750
|
+
throw new TypeError('Post items must be an object or an array of objects');
|
|
751
|
+
}
|
|
752
|
+
|
|
753
|
+
const posts = Array.isArray(data) ? data : [data];
|
|
754
|
+
|
|
755
|
+
this._submessages.push({
|
|
756
|
+
messageType: 2,
|
|
757
|
+
messageText: '[ Postingan tidak dapat dimuat ]',
|
|
758
|
+
});
|
|
759
|
+
|
|
760
|
+
const primitives = posts.map((p) => ({
|
|
761
|
+
title: p.title ?? '',
|
|
762
|
+
subtitle: p.subtitle ?? '',
|
|
763
|
+
username: p.username ?? '',
|
|
764
|
+
profile_picture_url: Toolkit.resolveMedia(this.#client, p.profile_picture_url ?? p.profile_url ?? p.profile ?? '', 'image', { resolveUrl }),
|
|
765
|
+
is_verified: !!(p.is_verified || p.verified),
|
|
766
|
+
thumbnail_url: Toolkit.resolveMedia(this.#client, p.thumbnail_url ?? p.thumbnail ?? '', 'image', { resolveUrl }),
|
|
767
|
+
post_caption: p.post_caption ?? p.caption ?? '',
|
|
768
|
+
likes_count: p.likes_count ?? p.like ?? 0,
|
|
769
|
+
comments_count: p.comments_count ?? p.comment ?? 0,
|
|
770
|
+
shares_count: p.shares_count ?? p.share ?? 0,
|
|
771
|
+
post_url: p.post_url ?? p.url ?? '',
|
|
772
|
+
post_deeplink: p.post_deeplink ?? p.deeplink ?? '',
|
|
773
|
+
source_app: p.source_app || p.source || 'INSTAGRAM',
|
|
774
|
+
footer_label: p.footer_label ?? p.footer ?? '',
|
|
775
|
+
footer_icon: Toolkit.resolveMedia(this.#client, p.footer_icon ?? p.icon ?? '', 'image', { resolveUrl }),
|
|
776
|
+
is_carousel: posts.length > 1,
|
|
777
|
+
orientation: p.orientation ?? 'LANDSCAPE',
|
|
778
|
+
post_type: p.post_type ?? 'VIDEO',
|
|
779
|
+
__typename: 'GenAIPostPrimitive',
|
|
780
|
+
}));
|
|
781
|
+
|
|
782
|
+
this._sections.push(AIRich.newLayout('HScroll', primitives));
|
|
783
|
+
|
|
784
|
+
return this;
|
|
785
|
+
}
|
|
786
|
+
|
|
787
|
+
// JAP@Add 24-08-26 --- ported from temen's MessageBuilderV4.7 (setResponseId/setBotResponseId/
|
|
788
|
+
// refreshResponseId/refreshBotResponseId), rewritten for this fork. Pins the two ids build()
|
|
789
|
+
// generates (see constructor comment) so a rebuilt message can reuse the same response_id/
|
|
790
|
+
// botResponseId — needed for editing an already-sent AIRich message in place.
|
|
791
|
+
|
|
792
|
+
/** Pin `unifiedResponse.response_id` to a specific value instead of a fresh random one each build() — needed to re-send an edited version of an already-sent message in place. */
|
|
793
|
+
setResponseId(id) {
|
|
794
|
+
if (typeof id !== 'string' || !id) throw new TypeError('setResponseId(id) requires a non-empty string');
|
|
795
|
+
this._responseId = id;
|
|
796
|
+
return this;
|
|
797
|
+
}
|
|
798
|
+
|
|
799
|
+
/** Un-pin `unifiedResponse.response_id`, generating a fresh crypto.randomUUID() immediately (not deferred to the next build()). */
|
|
800
|
+
refreshResponseId() {
|
|
801
|
+
this._responseId = crypto.randomUUID();
|
|
802
|
+
return this;
|
|
803
|
+
}
|
|
804
|
+
|
|
805
|
+
/** Pin `botMetadata.botResponseId` to a specific value instead of a fresh random one each build(). */
|
|
806
|
+
setBotResponseId(id) {
|
|
807
|
+
if (typeof id !== 'string' || !id) throw new TypeError('setBotResponseId(id) requires a non-empty string');
|
|
808
|
+
this._botResponseId = id;
|
|
809
|
+
return this;
|
|
810
|
+
}
|
|
811
|
+
|
|
812
|
+
/** Un-pin `botMetadata.botResponseId`, generating a fresh crypto.randomUUID() immediately. */
|
|
813
|
+
refreshBotResponseId() {
|
|
814
|
+
this._botResponseId = crypto.randomUUID();
|
|
815
|
+
return this;
|
|
816
|
+
}
|
|
817
|
+
|
|
818
|
+
// JAP@Add 25-08-26 --- ported from temen's MessageBuilderV4.7 (hasId/getIds/peek/delete).
|
|
819
|
+
// That fork tracked every block in a unified `_nodes` array so query/delete-by-id was free;
|
|
820
|
+
// this fork instead tracks blocks in the `_blocks` Map (id -> {subItems, secItems}, populated
|
|
821
|
+
// by the constructor's Proxy on every add*/set* call that passes {id}) but never exposed a way
|
|
822
|
+
// to query or undo one after the fact — you could insertAt an id but never inspect, check, or
|
|
823
|
+
// remove it. These 4 read/delete that same Map, so no changes to the Proxy itself were needed.
|
|
824
|
+
|
|
825
|
+
/** Check whether a block id was registered by an earlier `add*()`/`set*()` call passing `{id}`. */
|
|
826
|
+
hasId(id) {
|
|
827
|
+
return typeof id === 'string' && this._blocks.has(id);
|
|
828
|
+
}
|
|
829
|
+
|
|
830
|
+
/** List every block id registered so far, in no particular order. */
|
|
831
|
+
getIds() {
|
|
832
|
+
return [...this._blocks.keys()];
|
|
833
|
+
}
|
|
834
|
+
|
|
835
|
+
/** Inspect a registered block without modifying it. Returns `null` if `id` isn't registered. */
|
|
836
|
+
peek(id) {
|
|
837
|
+
const block = this._blocks.get(id);
|
|
838
|
+
if (!block) return null;
|
|
839
|
+
|
|
840
|
+
return { id, sections: [...block.secItems], submessages: [...block.subItems] };
|
|
841
|
+
}
|
|
842
|
+
|
|
843
|
+
/** Remove a previously-added block (by the `id` passed to its `add*()`/`set*()` call) from the message. Throws if `id` isn't registered. */
|
|
844
|
+
delete(id) {
|
|
845
|
+
const block = this._blocks.get(id);
|
|
846
|
+
if (!block) throw new Error(`delete(id): no block registered with id "${id}"`);
|
|
847
|
+
|
|
848
|
+
for (const item of block.subItems) {
|
|
849
|
+
const idx = this._submessages.indexOf(item);
|
|
850
|
+
if (idx !== -1) this._submessages.splice(idx, 1);
|
|
851
|
+
}
|
|
852
|
+
for (const item of block.secItems) {
|
|
853
|
+
const idx = this._sections.indexOf(item);
|
|
854
|
+
if (idx !== -1) this._sections.splice(idx, 1);
|
|
855
|
+
}
|
|
856
|
+
|
|
857
|
+
this._blocks.delete(id);
|
|
858
|
+
return this;
|
|
859
|
+
}
|
|
860
|
+
|
|
861
|
+
/** Add a small metadata-style text line (`GenAIMetadataTextPrimitive`) — same visual style as the auto-appended footer/`addTip()`'s callout, but insertable anywhere and without `addTip()`'s icon prefix. */
|
|
862
|
+
addMetadata(text) {
|
|
863
|
+
if (typeof text !== 'string' || !text) throw new TypeError('addMetadata(text) requires a non-empty string');
|
|
864
|
+
|
|
865
|
+
this._submessages.push({
|
|
866
|
+
messageType: 2,
|
|
867
|
+
messageText: text,
|
|
868
|
+
});
|
|
869
|
+
|
|
870
|
+
this._sections.push(
|
|
871
|
+
AIRich.newLayout('Single', {
|
|
872
|
+
text,
|
|
873
|
+
__typename: 'GenAIMetadataTextPrimitive',
|
|
874
|
+
})
|
|
875
|
+
);
|
|
876
|
+
|
|
877
|
+
return this;
|
|
878
|
+
}
|
|
879
|
+
|
|
880
|
+
/** Add a small "tip" callout banner. @param {string} text */
|
|
881
|
+
addTip(text) {
|
|
882
|
+
if (typeof text !== 'string' || !text) {
|
|
883
|
+
throw new TypeError('addTip(text) requires a non-empty string');
|
|
884
|
+
}
|
|
885
|
+
|
|
886
|
+
this._submessages.push({
|
|
887
|
+
messageType: 2,
|
|
888
|
+
messageText: text,
|
|
889
|
+
});
|
|
890
|
+
|
|
891
|
+
this._sections.push(
|
|
892
|
+
AIRich.newLayout('Single', {
|
|
893
|
+
text,
|
|
894
|
+
__typename: 'GenAIMetadataTextPrimitive',
|
|
895
|
+
})
|
|
896
|
+
);
|
|
897
|
+
|
|
898
|
+
return this;
|
|
899
|
+
}
|
|
900
|
+
|
|
901
|
+
// JAP@Add 22-08-26 (v4.7) --- addHeading/addWidget/addFooterAction: 3 primitives
|
|
902
|
+
// reverse-engineered from captured Meta-AI-in-WhatsApp traffic that this project's own crm/snip
|
|
903
|
+
// tooling (see rich-message-utils.js) dumps for study. Not in any public Baileys schema, so
|
|
904
|
+
// unknown enum values (kind/state on addWidget's ctas) are passed through as observed rather
|
|
905
|
+
// than guessed at, and documented as experimental below.
|
|
906
|
+
// JAP@Fix 25-08-26 --- removed addImageCard(): its GenAIImagePrimitive/preview_image+full_image
|
|
907
|
+
// shape was mis-reverse-engineered (not a real WA schema) and crashed the client renderer on
|
|
908
|
+
// arrival. addImage() already covers static image cards correctly — use that instead.
|
|
909
|
+
|
|
910
|
+
/** Add a large heading-style text block (`FOATextPrimitive`) — visually distinct from `addText()`'s regular paragraph text. */
|
|
911
|
+
addHeading(text) {
|
|
912
|
+
if (typeof text !== 'string' || !text) {
|
|
913
|
+
throw new TypeError('addHeading(text) requires a non-empty string');
|
|
914
|
+
}
|
|
915
|
+
|
|
916
|
+
this._submessages.push({
|
|
917
|
+
messageType: 2,
|
|
918
|
+
messageText: text,
|
|
919
|
+
});
|
|
920
|
+
|
|
921
|
+
this._sections.push(
|
|
922
|
+
AIRich.newLayout('Single', {
|
|
923
|
+
text,
|
|
924
|
+
__typename: 'FOATextPrimitive',
|
|
925
|
+
})
|
|
926
|
+
);
|
|
927
|
+
|
|
928
|
+
return this;
|
|
929
|
+
}
|
|
930
|
+
|
|
931
|
+
/**
|
|
932
|
+
* Add a "3P extension" widget card (`GenAI3PExtWidgetPrimitive`) — a small panel with a title and
|
|
933
|
+
* a row of tappable CTA chips. Per captured traffic these CTAs call back into a tool (`tool_call_id`)
|
|
934
|
+
* rather than opening a url; `kind`/`state` semantics beyond the observed `'OTHER'`/`'PENDING'`
|
|
935
|
+
* defaults aren't publicly documented, so treat this as experimental.
|
|
936
|
+
*
|
|
937
|
+
* JAP@Add (v4.8) --- accepts an `{ layout }` override so consecutive `addWidget()` calls can
|
|
938
|
+
* pick different renderings (e.g. one `HScroll` row, one `ActionRow` stack) instead of always
|
|
939
|
+
* inferring HScroll-for-array/Single-for-object from the shape of `data`. Also accepts either
|
|
940
|
+
* `ctas` (original key, matches the wire field) or `actions` (alias) on each item — whichever
|
|
941
|
+
* is present is used; `ctas` wins if both are somehow given.
|
|
942
|
+
* @param {Record<string, any>|Record<string, any>[]} data `{ title, ctas|actions: [{ label, tool_call_id?, kind?, state?, toast? }] }` (single or array).
|
|
943
|
+
* @param {{layout?: 'Single'|'HScroll'|'ActionRow'|string}} [options] `layout` overrides the default single/array inference.
|
|
944
|
+
*/
|
|
945
|
+
addWidget(data = {}, { layout } = {}) {
|
|
946
|
+
const items = Array.isArray(data) ? data : [data];
|
|
947
|
+
|
|
948
|
+
// JAP@Fix (bug 44) --- layout: 'Single' forces `widgets[0]` below (a "Single" layout's
|
|
949
|
+
// view_model can only ever hold one `primitive`, never a `primitives` array — see
|
|
950
|
+
// newLayout()). Previously an explicit { layout: 'Single' } combined with a multi-item
|
|
951
|
+
// array silently dropped every item past the first with no error. Fail loud instead.
|
|
952
|
+
if (layout === 'Single' && items.length > 1) {
|
|
953
|
+
throw new TypeError(`addWidget(): layout "Single" can only hold one widget (got ${items.length}) — use "HScroll"/"ActionRow" (or omit layout) for multiple`);
|
|
954
|
+
}
|
|
955
|
+
|
|
956
|
+
items.forEach((item, i) => {
|
|
957
|
+
// header.title or top-level title required
|
|
958
|
+
const hasTitle = item?.title || item?.header?.title;
|
|
959
|
+
if (!hasTitle) {
|
|
960
|
+
throw new TypeError(`addWidget() item[${i}] is missing a required "title" (or "header.title")`);
|
|
961
|
+
}
|
|
962
|
+
const ctas = item.ctas ?? item.actions;
|
|
963
|
+
if (!Array.isArray(ctas) || !ctas.length) {
|
|
964
|
+
throw new TypeError(`addWidget() item[${i}] requires a non-empty "ctas" (or "actions") array`);
|
|
965
|
+
}
|
|
966
|
+
});
|
|
967
|
+
|
|
968
|
+
this._submessages.push({
|
|
969
|
+
messageType: 2,
|
|
970
|
+
messageText: items.map((item) => item.header?.title ?? item.title).join(', '),
|
|
971
|
+
});
|
|
972
|
+
|
|
973
|
+
// JAP@Fix (bug 45) --- auto tool_call_id used to be `idx` scoped per-item (ctas.map's own
|
|
974
|
+
// index), resetting to 0 for every widget item. Two items (or two separate addWidget()
|
|
975
|
+
// calls) that both omit tool_call_id/id ended up minting the identical auto id ("00"),
|
|
976
|
+
// so a CTA tap could route to the wrong widget's tool call. Track the counter on the
|
|
977
|
+
// instance instead so every auto-generated id is unique for this AIRich's lifetime.
|
|
978
|
+
this._widgetCtaCounter ??= 0;
|
|
979
|
+
|
|
980
|
+
const widgets = items.map((item) => {
|
|
981
|
+
const ctas = item.ctas ?? item.actions;
|
|
982
|
+
// header accepts either a string title (legacy) or an object { title, subtitle }
|
|
983
|
+
const headerTitle = item.header?.title ?? item.title;
|
|
984
|
+
const headerSubtitle = item.header?.subtitle ?? item.subtitle ?? undefined;
|
|
985
|
+
return {
|
|
986
|
+
header: {
|
|
987
|
+
title: headerTitle,
|
|
988
|
+
...(headerSubtitle !== undefined && { subtitle: headerSubtitle }),
|
|
989
|
+
__typename: 'GenAI3PExtWidgetStandardHeader',
|
|
990
|
+
},
|
|
991
|
+
body: {
|
|
992
|
+
sections: item.sections ?? [],
|
|
993
|
+
ctas: ctas.map((cta) => ({
|
|
994
|
+
label: cta.label ?? '',
|
|
995
|
+
state: cta.state ?? 'PENDING',
|
|
996
|
+
kind: cta.kind ?? 'OTHER',
|
|
997
|
+
tool_call_id: cta.tool_call_id ?? cta.id ?? String(this._widgetCtaCounter++).padStart(2, '0'),
|
|
998
|
+
...(cta.toast !== false && {
|
|
999
|
+
toast: { label: typeof cta.toast === 'string' ? cta.toast : headerTitle, __typename: 'GenAI3PExtWidgetToast' },
|
|
1000
|
+
}),
|
|
1001
|
+
__typename: 'GenAI3PExtWidgetCTA',
|
|
1002
|
+
})),
|
|
1003
|
+
__typename: item.body_typename ?? 'GenAI3PExtCalendarEventList',
|
|
1004
|
+
},
|
|
1005
|
+
__typename: 'GenAI3PExtWidgetPrimitive',
|
|
1006
|
+
};
|
|
1007
|
+
});
|
|
1008
|
+
|
|
1009
|
+
const resolvedLayout = layout ?? (Array.isArray(data) ? 'HScroll' : 'Single');
|
|
1010
|
+
const asArray = resolvedLayout !== 'Single';
|
|
1011
|
+
|
|
1012
|
+
this._sections.push(AIRich.newLayout(resolvedLayout, asArray ? widgets : widgets[0]));
|
|
1013
|
+
|
|
1014
|
+
return this;
|
|
1015
|
+
}
|
|
1016
|
+
|
|
1017
|
+
/**
|
|
1018
|
+
* Add footer action link(s) (`GenAIFooterActionPrimitive`) — e.g. "Join our WhatsApp Group/Channel"
|
|
1019
|
+
* chips shown below the response, separate from `setFooter()`'s plain text footer.
|
|
1020
|
+
* @param {{text: string, url: string, type?: string}|{text: string, url: string, type?: string}[]} actions
|
|
1021
|
+
*/
|
|
1022
|
+
addFooterAction(actions) {
|
|
1023
|
+
const items = Array.isArray(actions) ? actions : [actions];
|
|
1024
|
+
|
|
1025
|
+
items.forEach((item, i) => {
|
|
1026
|
+
if (!item?.text || !item?.url) {
|
|
1027
|
+
throw new TypeError(`addFooterAction() item[${i}] requires both "text" and "url"`);
|
|
1028
|
+
}
|
|
1029
|
+
});
|
|
1030
|
+
|
|
1031
|
+
const primitives = items.map((item) => ({
|
|
1032
|
+
cta_text: item.text,
|
|
1033
|
+
cta_type: item.type ?? 'OPEN_URL',
|
|
1034
|
+
cta_url: item.url,
|
|
1035
|
+
__typename: 'GenAIFooterActionPrimitive',
|
|
1036
|
+
}));
|
|
1037
|
+
|
|
1038
|
+
this._sections.push(AIRich.newLayout('HScroll', primitives));
|
|
1039
|
+
|
|
1040
|
+
return this;
|
|
1041
|
+
}
|
|
1042
|
+
|
|
1043
|
+
// JAP@Add (v4.8) --- 8 primitives from the 20-item reference test script that had no
|
|
1044
|
+
// add*() helper yet (Divider/Spacer/Task/ProgressStatus/ThinkingStatus/QuotaUpsell/FOABloks
|
|
1045
|
+
// have no dedicated AIRichResponseSubMessageType — WA carries them purely in the
|
|
1046
|
+
// unifiedResponse view-model JSON, so their submessage falls back to plain AI_RICH_RESPONSE_TEXT
|
|
1047
|
+
// like addTip/addHeading already do. Latex is the one exception: it has a real proto type
|
|
1048
|
+
// (AI_RICH_RESPONSE_LATEX = 8, confirmed in WAProto) with its own latexMetadata, so that one
|
|
1049
|
+
// gets a proper submessage instead of the text fallback.
|
|
1050
|
+
|
|
1051
|
+
/** Add a plain horizontal divider line (`GenAIDividerPrimitive`, no content). */
|
|
1052
|
+
addDivider() {
|
|
1053
|
+
this._submessages.push({ messageType: 2, messageText: '---' });
|
|
1054
|
+
this._sections.push(AIRich.newLayout('Single', { __typename: 'GenAIDividerPrimitive' }));
|
|
1055
|
+
return this;
|
|
1056
|
+
}
|
|
1057
|
+
|
|
1058
|
+
/** Add blank vertical spacing (`GenAISpacerPrimitive`). @param {number} [spacing=1] Spacing unit, per observed traffic. */
|
|
1059
|
+
addSpacer(spacing = 1) {
|
|
1060
|
+
if (typeof spacing !== 'number' || spacing < 0) {
|
|
1061
|
+
throw new TypeError('addSpacer(spacing) requires a non-negative number');
|
|
1062
|
+
}
|
|
1063
|
+
this._submessages.push({ messageType: 2, messageText: `spasi ${spacing}` });
|
|
1064
|
+
this._sections.push(AIRich.newLayout('Single', { spacing, __typename: 'GenAISpacerPrimitive' }));
|
|
1065
|
+
return this;
|
|
1066
|
+
}
|
|
1067
|
+
|
|
1068
|
+
/**
|
|
1069
|
+
* Add a rendered LaTeX expression (`GenAILatexUXPrimitive`), with a real `AI_RICH_RESPONSE_LATEX`
|
|
1070
|
+
* submessage (unlike most primitives in this block, this one has a proper proto type).
|
|
1071
|
+
* @param {string} expression LaTeX source, e.g. `'$$E = mc^2$$'`.
|
|
1072
|
+
*/
|
|
1073
|
+
addLatex(expression) {
|
|
1074
|
+
if (typeof expression !== 'string' || !expression) {
|
|
1075
|
+
throw new TypeError('addLatex(expression) requires a non-empty string');
|
|
1076
|
+
}
|
|
1077
|
+
this._submessages.push({
|
|
1078
|
+
messageType: 8,
|
|
1079
|
+
latexMetadata: { text: expression, expressions: [{ latexExpression: expression }] },
|
|
1080
|
+
});
|
|
1081
|
+
this._sections.push(AIRich.newLayout('Single', { latex_expression: expression, __typename: 'GenAILatexUXPrimitive' }));
|
|
1082
|
+
return this;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
/**
|
|
1086
|
+
* Add a task/checklist card (`GenAITaskPrimitive`).
|
|
1087
|
+
* @param {{task_id?: string, title: string, subtitle?: string, status?: string}} data
|
|
1088
|
+
*/
|
|
1089
|
+
addTask(data = {}) {
|
|
1090
|
+
if (!data?.title) {
|
|
1091
|
+
throw new TypeError('addTask() requires a "title"');
|
|
1092
|
+
}
|
|
1093
|
+
this._submessages.push({ messageType: 2, messageText: `Tugas: ${data.title}` });
|
|
1094
|
+
this._sections.push(
|
|
1095
|
+
AIRich.newLayout('Single', {
|
|
1096
|
+
task_id: data.task_id ?? '',
|
|
1097
|
+
title: data.title,
|
|
1098
|
+
subtitle: data.subtitle ?? '',
|
|
1099
|
+
status: data.status ?? 'IN_PROGRESS',
|
|
1100
|
+
__typename: 'GenAITaskPrimitive',
|
|
1101
|
+
})
|
|
1102
|
+
);
|
|
1103
|
+
// Safety net: GenAITaskPrimitive is a custom AI-only component the stock WA client
|
|
1104
|
+
// doesn't render visibly. Append a plain text section so the task is still visible.
|
|
1105
|
+
// Set data.textFallback = false to skip.
|
|
1106
|
+
if (data.textFallback !== false) {
|
|
1107
|
+
const fallbackText = data.subtitle ? `${data.title} — ${data.subtitle}` : data.title;
|
|
1108
|
+
this._sections.push(AIRich.newLayout('Single', { text: `Tugas: ${fallbackText}`, __typename: 'FOATextPrimitive' }));
|
|
1109
|
+
}
|
|
1110
|
+
return this;
|
|
1111
|
+
}
|
|
1112
|
+
|
|
1113
|
+
/**
|
|
1114
|
+
* Add a "searching/working" progress banner (`GenAIBotProgressStatusPrimitive`) — a one-shot
|
|
1115
|
+
* status chip (unlike `addSuggest`, this isn't tappable). Distinct from `addThinkingStatus()`'s
|
|
1116
|
+
* icon/typename.
|
|
1117
|
+
* @param {string} title
|
|
1118
|
+
* @param {{icon?: string, is_in_progress?: boolean}} [options]
|
|
1119
|
+
*/
|
|
1120
|
+
addProgressStatus(title, { icon = 'SEARCH', is_in_progress = true, target_secondary_screen_id, target_secondary_screen_tab_id } = {}) {
|
|
1121
|
+
if (typeof title !== 'string' || !title) {
|
|
1122
|
+
throw new TypeError('addProgressStatus(title) requires a non-empty string');
|
|
1123
|
+
}
|
|
1124
|
+
this._submessages.push({ messageType: 2, messageText: title });
|
|
1125
|
+
const primitive = {
|
|
1126
|
+
title,
|
|
1127
|
+
icon,
|
|
1128
|
+
is_in_progress,
|
|
1129
|
+
meta_search_apps: [],
|
|
1130
|
+
__typename: 'GenAIBotProgressStatusPrimitive',
|
|
1131
|
+
};
|
|
1132
|
+
// NOTE: these two fields must be OMITTED when unset, not sent as `null` —
|
|
1133
|
+
// an explicit null here was reproducibly crashing the WA client renderer
|
|
1134
|
+
// on group-open/media-download. Only include when the caller actually passes one.
|
|
1135
|
+
if (target_secondary_screen_id != null) primitive.target_secondary_screen_id = target_secondary_screen_id;
|
|
1136
|
+
if (target_secondary_screen_tab_id != null) primitive.target_secondary_screen_tab_id = target_secondary_screen_tab_id;
|
|
1137
|
+
this._sections.push(AIRich.newLayout('Single', primitive));
|
|
1138
|
+
return this;
|
|
1139
|
+
}
|
|
1140
|
+
|
|
1141
|
+
/** Add a "thinking" status banner (`GenAIBotThinkingStatusPrimitive`). See `addProgressStatus()`. */
|
|
1142
|
+
addThinkingStatus(title, { icon = 'THINKING', is_in_progress = true, target_secondary_screen_id, target_secondary_screen_tab_id, textFallback = true } = {}) {
|
|
1143
|
+
if (typeof title !== 'string' || !title) {
|
|
1144
|
+
throw new TypeError('addThinkingStatus(title) requires a non-empty string');
|
|
1145
|
+
}
|
|
1146
|
+
this._submessages.push({ messageType: 2, messageText: title });
|
|
1147
|
+
const primitive = {
|
|
1148
|
+
title,
|
|
1149
|
+
icon,
|
|
1150
|
+
is_in_progress,
|
|
1151
|
+
meta_search_apps: [],
|
|
1152
|
+
__typename: 'GenAIBotThinkingStatusPrimitive',
|
|
1153
|
+
};
|
|
1154
|
+
// Same crash-avoidance rule as addProgressStatus(): omit, never null.
|
|
1155
|
+
if (target_secondary_screen_id != null) primitive.target_secondary_screen_id = target_secondary_screen_id;
|
|
1156
|
+
if (target_secondary_screen_tab_id != null) primitive.target_secondary_screen_tab_id = target_secondary_screen_tab_id;
|
|
1157
|
+
this._sections.push(AIRich.newLayout('Single', primitive));
|
|
1158
|
+
// Safety net: stock WA client doesn't render this primitive's own view (it's meant
|
|
1159
|
+
// as a transient spinner in the official app), so the card shows blank when forwarded.
|
|
1160
|
+
// Append a plain text section so the title is still visible. Set { textFallback: false } to skip.
|
|
1161
|
+
if (textFallback) {
|
|
1162
|
+
this._sections.push(AIRich.newLayout('Single', { text: title, __typename: 'FOATextPrimitive' }));
|
|
1163
|
+
}
|
|
1164
|
+
return this;
|
|
1165
|
+
}
|
|
1166
|
+
|
|
1167
|
+
/**
|
|
1168
|
+
* Add a subscription-quota-limit upsell card (`GenAIMetaSubsQuotaUpsellPrimitive`).
|
|
1169
|
+
* @param {{title: string, body?: string, body_line1?: string, body_line2?: string, buttons?: {label: string, action?: string, deeplink?: string}[]}} data
|
|
1170
|
+
*/
|
|
1171
|
+
addQuotaUpsell(data = {}) {
|
|
1172
|
+
if (!data?.title) {
|
|
1173
|
+
throw new TypeError('addQuotaUpsell() requires a "title"');
|
|
1174
|
+
}
|
|
1175
|
+
this._submessages.push({ messageType: 2, messageText: data.title });
|
|
1176
|
+
this._sections.push(
|
|
1177
|
+
AIRich.newLayout('Single', {
|
|
1178
|
+
title: data.title,
|
|
1179
|
+
body: data.body ?? '',
|
|
1180
|
+
body_line1: data.body_line1 ?? '',
|
|
1181
|
+
body_line2: data.body_line2 ?? '',
|
|
1182
|
+
buttons: (data.buttons ?? []).map((b) => ({
|
|
1183
|
+
label: b.label ?? '',
|
|
1184
|
+
action: b.action ?? 'OPEN_DEEPLINK',
|
|
1185
|
+
deeplink: b.deeplink ?? '',
|
|
1186
|
+
})),
|
|
1187
|
+
__typename: 'GenAIMetaSubsQuotaUpsellPrimitive',
|
|
1188
|
+
})
|
|
1189
|
+
);
|
|
1190
|
+
return this;
|
|
1191
|
+
}
|
|
1192
|
+
|
|
1193
|
+
/**
|
|
1194
|
+
* Add a raw Bloks payload (`FOABloksPrimitive`) — Meta's internal UI-description format.
|
|
1195
|
+
* Escape hatch: field meaning beyond what's passed through is undocumented, so this is the
|
|
1196
|
+
* most experimental primitive in this block; pass whatever your captured traffic shows.
|
|
1197
|
+
* @param {{type: string, data: string, uuid?: string, initial_response?: any, versioning_id?: string}} data
|
|
1198
|
+
*/
|
|
1199
|
+
addBloks(data = {}) {
|
|
1200
|
+
if (!data?.type) {
|
|
1201
|
+
throw new TypeError('addBloks() requires a "type"');
|
|
1202
|
+
}
|
|
1203
|
+
this._submessages.push({ messageType: 2, messageText: 'Bloks' });
|
|
1204
|
+
const primitive = {
|
|
1205
|
+
type: data.type,
|
|
1206
|
+
data: data.data ?? '{}',
|
|
1207
|
+
uuid: data.uuid ?? '',
|
|
1208
|
+
versioning_id: data.versioning_id ?? '',
|
|
1209
|
+
__typename: 'FOABloksPrimitive',
|
|
1210
|
+
};
|
|
1211
|
+
// Omit initial_response entirely when unset — same null-field crash as addProgressStatus/addThinkingStatus.
|
|
1212
|
+
if (data.initial_response != null) primitive.initial_response = data.initial_response;
|
|
1213
|
+
this._sections.push(AIRich.newLayout('Single', primitive));
|
|
1214
|
+
// Safety net: FOABloksPrimitive needs a real, client-registered Bloks screen to render
|
|
1215
|
+
// anything — arbitrary/placeholder payloads show up blank. Append a plain text section
|
|
1216
|
+
// so the card isn't empty. Set data.textFallback = false to skip.
|
|
1217
|
+
if (data.textFallback !== false) {
|
|
1218
|
+
this._sections.push(AIRich.newLayout('Single', { text: `Bloks: ${data.type}`, __typename: 'FOATextPrimitive' }));
|
|
1219
|
+
}
|
|
1220
|
+
return this;
|
|
1221
|
+
}
|
|
1222
|
+
|
|
1223
|
+
/** Add tappable follow-up suggestion chips below the message. @param {string|string[]} suggestion */
|
|
1224
|
+
addSuggest(suggestion, { scroll = true, layout } = {}) {
|
|
1225
|
+
if (!(typeof suggestion === 'string' || (Array.isArray(suggestion) && suggestion.every((v) => typeof v === 'string')))) {
|
|
1226
|
+
throw new TypeError('Suggestion must be a string or array of strings');
|
|
1227
|
+
}
|
|
1228
|
+
|
|
1229
|
+
const suggest = Array.isArray(suggestion)
|
|
1230
|
+
? suggestion.map((text) => ({
|
|
1231
|
+
prompt_text: text,
|
|
1232
|
+
prompt_type: 'SUGGESTED_PROMPT',
|
|
1233
|
+
__typename: 'GenAIFollowUpSuggestionPillPrimitive',
|
|
1234
|
+
}))
|
|
1235
|
+
: [
|
|
1236
|
+
{
|
|
1237
|
+
prompt_text: suggestion,
|
|
1238
|
+
prompt_type: 'SUGGESTED_PROMPT',
|
|
1239
|
+
__typename: 'GenAIFollowUpSuggestionPillPrimitive',
|
|
1240
|
+
},
|
|
1241
|
+
];
|
|
1242
|
+
|
|
1243
|
+
const type = layout ?? (suggest.length === 1 ? 'Single' : scroll ? 'HScroll' : 'ActionRow');
|
|
1244
|
+
|
|
1245
|
+
this._sections.push(AIRich.newLayout(type, type === 'Single' ? suggest[0] : suggest, { __typename: 'GenAIUnifiedResponseSection' }));
|
|
1246
|
+
|
|
1247
|
+
return this;
|
|
1248
|
+
}
|
|
1249
|
+
|
|
1250
|
+
/** @returns {Promise<Record<string, any>>} The generated AI-rich message content (without wrapping/sending it). */
|
|
1251
|
+
async build({ forwarded = true, notification = false, includesUnifiedResponse = true, includesSubmessages = true, quoted, quotedParticipant, ...options } = {}) {
|
|
1252
|
+
const forward = forwarded
|
|
1253
|
+
? {
|
|
1254
|
+
forwardingScore: 1,
|
|
1255
|
+
isForwarded: true,
|
|
1256
|
+
forwardedAiBotMessageInfo: { botJid: '0@bot' },
|
|
1257
|
+
forwardOrigin: 4,
|
|
1258
|
+
}
|
|
1259
|
+
: {};
|
|
1260
|
+
|
|
1261
|
+
const notif = notification
|
|
1262
|
+
? {
|
|
1263
|
+
sessionTransparencyMetadata: {
|
|
1264
|
+
disclaimerText: '~ Ahmad tumbuh kembang',
|
|
1265
|
+
hcaId: `hca_${Date.now()}`,
|
|
1266
|
+
sessionTransparencyType: 1,
|
|
1267
|
+
},
|
|
1268
|
+
}
|
|
1269
|
+
: {};
|
|
1270
|
+
|
|
1271
|
+
const qObj = quoted
|
|
1272
|
+
? {
|
|
1273
|
+
stanzaId: quoted?.key?.id || quoted?.id,
|
|
1274
|
+
participant: quotedParticipant || quoted?.key?.participant || quoted?.key?.remoteJid,
|
|
1275
|
+
quotedType: 0,
|
|
1276
|
+
quotedMessage: typeof quoted === 'object' && quoted !== null ? (quoted.message ?? quoted) : undefined,
|
|
1277
|
+
}
|
|
1278
|
+
: {};
|
|
1279
|
+
|
|
1280
|
+
const sections = this._footer
|
|
1281
|
+
? [
|
|
1282
|
+
...(await waitAllPromises(this._sections)),
|
|
1283
|
+
AIRich.newLayout('Single', {
|
|
1284
|
+
text: this._footer,
|
|
1285
|
+
__typename: 'GenAIMetadataTextPrimitive',
|
|
1286
|
+
}),
|
|
1287
|
+
]
|
|
1288
|
+
: [...(await waitAllPromises(this._sections))];
|
|
1289
|
+
|
|
1290
|
+
// JAP@Merge 15-08-26 --- Neither blurose nor arslan sign the bot metadata with
|
|
1291
|
+
// verificationMetadata (proofs/certificateChain). Backported from this project's own
|
|
1292
|
+
// rich-message-utils.js botMetadataSignature/botMetadataCertificate helpers, plus a
|
|
1293
|
+
// botResponseId tying the signed metadata to unifiedResponse.response_id.
|
|
1294
|
+
// JAP@Fix 24-08-26 --- was `const responseId = crypto.randomUUID()` shared for BOTH
|
|
1295
|
+
// unifiedResponse.response_id and botMetadata.botResponseId, generated fresh every build()
|
|
1296
|
+
// with no override. Now each has its own id, pinned via setResponseId()/setBotResponseId()
|
|
1297
|
+
// if the caller set one (for sendEdit()-style in-place message updates), otherwise still
|
|
1298
|
+
// defaults to a fresh randomUUID() per build() exactly like before.
|
|
1299
|
+
const responseId = this._responseId ?? crypto.randomUUID();
|
|
1300
|
+
const botResponseId = this._botResponseId ?? crypto.randomUUID();
|
|
1301
|
+
|
|
1302
|
+
return {
|
|
1303
|
+
messageContextInfo: {
|
|
1304
|
+
deviceListMetadata: {},
|
|
1305
|
+
deviceListMetadataVersion: 2,
|
|
1306
|
+
botMetadata: {
|
|
1307
|
+
messageDisclaimerText: this._title,
|
|
1308
|
+
richResponseSourcesMetadata: { sources: this._richResponseSources },
|
|
1309
|
+
botResponseId: botResponseId,
|
|
1310
|
+
verificationMetadata: {
|
|
1311
|
+
proofs: [
|
|
1312
|
+
{
|
|
1313
|
+
certificateChain: [botMetadataCertificate(), botMetadataCertificate(892)],
|
|
1314
|
+
version: 1,
|
|
1315
|
+
useCase: 1,
|
|
1316
|
+
signature: botMetadataSignature(),
|
|
1317
|
+
},
|
|
1318
|
+
],
|
|
1319
|
+
},
|
|
1320
|
+
...notif,
|
|
1321
|
+
},
|
|
1322
|
+
},
|
|
1323
|
+
...this._extraPayload,
|
|
1324
|
+
botForwardedMessage: {
|
|
1325
|
+
message: {
|
|
1326
|
+
richResponseMessage: {
|
|
1327
|
+
messageType: 1,
|
|
1328
|
+
submessages: includesSubmessages ? await waitAllPromises(this._submessages) : [],
|
|
1329
|
+
unifiedResponse: {
|
|
1330
|
+
data: includesUnifiedResponse ? Buffer.from(JSON.stringify({ response_id: responseId, sections })).toString('base64') : '',
|
|
1331
|
+
},
|
|
1332
|
+
contextInfo: {
|
|
1333
|
+
...forward,
|
|
1334
|
+
...qObj,
|
|
1335
|
+
...this._contextInfo,
|
|
1336
|
+
},
|
|
1337
|
+
},
|
|
1338
|
+
},
|
|
1339
|
+
},
|
|
1340
|
+
};
|
|
1341
|
+
}
|
|
1342
|
+
|
|
1343
|
+
// JAP@Fix (bug 42 / inline image fallback) --- WA won't render AIRichResponseInlineImageMetadata
|
|
1344
|
+
// for bot-sent messages (confirmed: even a valid WA-CDN url with mediaKey stays blank), so any
|
|
1345
|
+
// image added via addInlineImage() is sent here as a normal imageMessage instead. Pass
|
|
1346
|
+
// { skipImageFallback: true } to opt out and send only the (image-less-looking) rich card.
|
|
1347
|
+
// JAP@Fix: don't spread relayMessage-shaped `options` into sendMessage()'s options param —
|
|
1348
|
+
// the two calls expect different option shapes, so the fallback now only forwards `quoted`
|
|
1349
|
+
// (the one option that clearly applies to both) instead of blindly spreading everything.
|
|
1350
|
+
/** Build and send this AI-rich message. @param {string} jid Destination chat/group jid. @param {boolean} [skipImageFallback] Skip auto-resending inline images as a plain imageMessage. */
|
|
1351
|
+
async send(jid, { forwarded, notification, includesUnifiedResponse, includesSubmessages, skipImageFallback = false, quoted, messageId, ...options } = {}) {
|
|
1352
|
+
const msg = await this.build({ forwarded, notification, includesUnifiedResponse, includesSubmessages, quoted, ...options });
|
|
1353
|
+
|
|
1354
|
+
if (!skipImageFallback && this._inlineImages.length) {
|
|
1355
|
+
for (const { url, caption } of this._inlineImages) {
|
|
1356
|
+
try {
|
|
1357
|
+
await this.#client.sendMessage(jid, { image: { url }, caption }, quoted ? { quoted } : {});
|
|
1358
|
+
} catch (err) {
|
|
1359
|
+
// JAP@Fix: don't let a fallback image failure block the actual rich card from sending
|
|
1360
|
+
this.#client.logger?.warn?.({ err, url }, 'inline image fallback failed, continuing with rich card');
|
|
1361
|
+
}
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
|
|
1365
|
+
// JAP@Add --- pin our own messageId (instead of letting relayMessage mint one internally)
|
|
1366
|
+
// so we know exactly which id was sent, and stash it as _lastMessageKey. That's what lets
|
|
1367
|
+
// sendEdit() be called with no args afterwards and still know which message to patch.
|
|
1368
|
+
messageId = messageId || generateMessageIDV2();
|
|
1369
|
+
|
|
1370
|
+
await this.#client.relayMessage(jid, msg, { messageId, ...options });
|
|
1371
|
+
|
|
1372
|
+
this._lastMessageKey = { remoteJid: jid, fromMe: true, id: messageId };
|
|
1373
|
+
|
|
1374
|
+
return { key: this._lastMessageKey, message: msg };
|
|
1375
|
+
}
|
|
1376
|
+
|
|
1377
|
+
/**
|
|
1378
|
+
* Build a `protocolMessage` (type EDIT) that patches an already-sent AIRich message in place.
|
|
1379
|
+
* @param {string} targetJid Chat the original message lives in.
|
|
1380
|
+
* @param {string} targetId `key.id` of the original message (the id `send()`/`sendEdit()` returned).
|
|
1381
|
+
* @param {object} [opts] Pass `{ msg }` to reuse an already-built content object instead of rebuilding via build().
|
|
1382
|
+
*/
|
|
1383
|
+
async buildEdit(targetJid, targetId, { msg, messageId, ...options } = {}) {
|
|
1384
|
+
const editedMessage = msg || (await this.build({ ...options }));
|
|
1385
|
+
|
|
1386
|
+
if (!editedMessage) {
|
|
1387
|
+
throw new Error('buildEdit: no message content to edit (build() returned nothing)');
|
|
1388
|
+
}
|
|
1389
|
+
|
|
1390
|
+
return generateWAMessageFromContent(
|
|
1391
|
+
targetJid,
|
|
1392
|
+
{
|
|
1393
|
+
protocolMessage: {
|
|
1394
|
+
key: {
|
|
1395
|
+
remoteJid: targetJid,
|
|
1396
|
+
fromMe: true,
|
|
1397
|
+
id: targetId,
|
|
1398
|
+
},
|
|
1399
|
+
type: 14, // MESSAGE_EDIT
|
|
1400
|
+
editedMessage,
|
|
1401
|
+
},
|
|
1402
|
+
},
|
|
1403
|
+
{ messageId: messageId || generateMessageIDV2(), ...options }
|
|
1404
|
+
);
|
|
1405
|
+
}
|
|
1406
|
+
|
|
1407
|
+
/**
|
|
1408
|
+
* Rebuild this AIRich message's current content and patch it into an already-sent message in place
|
|
1409
|
+
* (WA edits the bubble instead of showing a new one). With no args, edits the message from the last
|
|
1410
|
+
* send()/sendEdit() call — that's the flow `.addX(...); await rich.sendEdit();` relies on.
|
|
1411
|
+
* @param {string} [jid] Defaults to the jid from the last send()/sendEdit().
|
|
1412
|
+
* @param {string} [id] Defaults to the message id from the last send()/sendEdit().
|
|
1413
|
+
*/
|
|
1414
|
+
async sendEdit(jid, id, { msg, messageId, additionalNodes = [], ...options } = {}) {
|
|
1415
|
+
jid = jid ?? this._lastMessageKey?.remoteJid;
|
|
1416
|
+
id = id ?? this._lastMessageKey?.id;
|
|
1417
|
+
|
|
1418
|
+
if (!jid) {
|
|
1419
|
+
throw new Error('sendEdit: no jid — pass one explicitly, or call send() first');
|
|
1420
|
+
}
|
|
1421
|
+
|
|
1422
|
+
if (!id) {
|
|
1423
|
+
throw new Error('sendEdit: no message id — pass one explicitly, or call send() first');
|
|
1424
|
+
}
|
|
1425
|
+
|
|
1426
|
+
const msgEdit = await this.buildEdit(jid, id, {
|
|
1427
|
+
msg,
|
|
1428
|
+
messageId: messageId || generateMessageIDV2(),
|
|
1429
|
+
...options,
|
|
1430
|
+
});
|
|
1431
|
+
|
|
1432
|
+
await this.#client.relayMessage(jid, msgEdit.message, {
|
|
1433
|
+
messageId: msgEdit.key.id,
|
|
1434
|
+
additionalNodes,
|
|
1435
|
+
});
|
|
1436
|
+
|
|
1437
|
+
// JAP@Note --- deliberately NOT overwriting _lastMessageKey with msgEdit.key here: the
|
|
1438
|
+
// protocolMessage envelope has its own id, but the message the user actually sees (and the
|
|
1439
|
+
// one future sendEdit() calls need to keep patching) is still `id`/`jid` above.
|
|
1440
|
+
return msgEdit;
|
|
1441
|
+
}
|
|
1442
|
+
|
|
1443
|
+
/** Tokenize `code` into `{ type, value }` spans for syntax highlighting. Covers JS/TS/Python/Java and more; unsupported languages fall back to a single plain-text token. */
|
|
1444
|
+
static tokenizer(code, lang = 'javascript') {
|
|
1445
|
+
const keywordsMap = {
|
|
1446
|
+
javascript: new Set([
|
|
1447
|
+
'break',
|
|
1448
|
+
'case',
|
|
1449
|
+
'catch',
|
|
1450
|
+
'continue',
|
|
1451
|
+
'debugger',
|
|
1452
|
+
'delete',
|
|
1453
|
+
'do',
|
|
1454
|
+
'else',
|
|
1455
|
+
'finally',
|
|
1456
|
+
'for',
|
|
1457
|
+
'function',
|
|
1458
|
+
'if',
|
|
1459
|
+
'in',
|
|
1460
|
+
'instanceof',
|
|
1461
|
+
'new',
|
|
1462
|
+
'return',
|
|
1463
|
+
'switch',
|
|
1464
|
+
'this',
|
|
1465
|
+
'throw',
|
|
1466
|
+
'try',
|
|
1467
|
+
'typeof',
|
|
1468
|
+
'var',
|
|
1469
|
+
'void',
|
|
1470
|
+
'while',
|
|
1471
|
+
'with',
|
|
1472
|
+
'true',
|
|
1473
|
+
'false',
|
|
1474
|
+
'null',
|
|
1475
|
+
'undefined',
|
|
1476
|
+
'class',
|
|
1477
|
+
'const',
|
|
1478
|
+
'let',
|
|
1479
|
+
'super',
|
|
1480
|
+
'extends',
|
|
1481
|
+
'export',
|
|
1482
|
+
'import',
|
|
1483
|
+
'yield',
|
|
1484
|
+
'static',
|
|
1485
|
+
'constructor',
|
|
1486
|
+
'async',
|
|
1487
|
+
'await',
|
|
1488
|
+
'get',
|
|
1489
|
+
'set',
|
|
1490
|
+
]),
|
|
1491
|
+
|
|
1492
|
+
typescript: new Set([
|
|
1493
|
+
'abstract',
|
|
1494
|
+
'any',
|
|
1495
|
+
'as',
|
|
1496
|
+
'asserts',
|
|
1497
|
+
'bigint',
|
|
1498
|
+
'boolean',
|
|
1499
|
+
'declare',
|
|
1500
|
+
'enum',
|
|
1501
|
+
'implements',
|
|
1502
|
+
'infer',
|
|
1503
|
+
'interface',
|
|
1504
|
+
'is',
|
|
1505
|
+
'keyof',
|
|
1506
|
+
'module',
|
|
1507
|
+
'namespace',
|
|
1508
|
+
'never',
|
|
1509
|
+
'readonly',
|
|
1510
|
+
'require',
|
|
1511
|
+
'number',
|
|
1512
|
+
'object',
|
|
1513
|
+
'override',
|
|
1514
|
+
'private',
|
|
1515
|
+
'protected',
|
|
1516
|
+
'public',
|
|
1517
|
+
'satisfies',
|
|
1518
|
+
'string',
|
|
1519
|
+
'symbol',
|
|
1520
|
+
'type',
|
|
1521
|
+
'unknown',
|
|
1522
|
+
'using',
|
|
1523
|
+
'from',
|
|
1524
|
+
'break',
|
|
1525
|
+
'case',
|
|
1526
|
+
'catch',
|
|
1527
|
+
'continue',
|
|
1528
|
+
'do',
|
|
1529
|
+
'else',
|
|
1530
|
+
'finally',
|
|
1531
|
+
'for',
|
|
1532
|
+
'function',
|
|
1533
|
+
'if',
|
|
1534
|
+
'new',
|
|
1535
|
+
'return',
|
|
1536
|
+
'switch',
|
|
1537
|
+
'this',
|
|
1538
|
+
'throw',
|
|
1539
|
+
'try',
|
|
1540
|
+
'var',
|
|
1541
|
+
'void',
|
|
1542
|
+
'while',
|
|
1543
|
+
'class',
|
|
1544
|
+
'const',
|
|
1545
|
+
'let',
|
|
1546
|
+
'extends',
|
|
1547
|
+
'import',
|
|
1548
|
+
'export',
|
|
1549
|
+
'async',
|
|
1550
|
+
'await',
|
|
1551
|
+
]),
|
|
1552
|
+
|
|
1553
|
+
python: new Set([
|
|
1554
|
+
'False',
|
|
1555
|
+
'None',
|
|
1556
|
+
'True',
|
|
1557
|
+
'and',
|
|
1558
|
+
'as',
|
|
1559
|
+
'assert',
|
|
1560
|
+
'async',
|
|
1561
|
+
'await',
|
|
1562
|
+
'break',
|
|
1563
|
+
'class',
|
|
1564
|
+
'continue',
|
|
1565
|
+
'def',
|
|
1566
|
+
'del',
|
|
1567
|
+
'elif',
|
|
1568
|
+
'else',
|
|
1569
|
+
'except',
|
|
1570
|
+
'finally',
|
|
1571
|
+
'for',
|
|
1572
|
+
'from',
|
|
1573
|
+
'global',
|
|
1574
|
+
'if',
|
|
1575
|
+
'import',
|
|
1576
|
+
'in',
|
|
1577
|
+
'is',
|
|
1578
|
+
'lambda',
|
|
1579
|
+
'nonlocal',
|
|
1580
|
+
'not',
|
|
1581
|
+
'or',
|
|
1582
|
+
'pass',
|
|
1583
|
+
'raise',
|
|
1584
|
+
'return',
|
|
1585
|
+
'try',
|
|
1586
|
+
'while',
|
|
1587
|
+
'with',
|
|
1588
|
+
'yield',
|
|
1589
|
+
]),
|
|
1590
|
+
|
|
1591
|
+
java: new Set([
|
|
1592
|
+
'abstract',
|
|
1593
|
+
'assert',
|
|
1594
|
+
'boolean',
|
|
1595
|
+
'break',
|
|
1596
|
+
'byte',
|
|
1597
|
+
'case',
|
|
1598
|
+
'catch',
|
|
1599
|
+
'char',
|
|
1600
|
+
'class',
|
|
1601
|
+
'const',
|
|
1602
|
+
'continue',
|
|
1603
|
+
'default',
|
|
1604
|
+
'do',
|
|
1605
|
+
'double',
|
|
1606
|
+
'else',
|
|
1607
|
+
'enum',
|
|
1608
|
+
'extends',
|
|
1609
|
+
'final',
|
|
1610
|
+
'finally',
|
|
1611
|
+
'float',
|
|
1612
|
+
'for',
|
|
1613
|
+
'goto',
|
|
1614
|
+
'if',
|
|
1615
|
+
'implements',
|
|
1616
|
+
'import',
|
|
1617
|
+
'instanceof',
|
|
1618
|
+
'int',
|
|
1619
|
+
'interface',
|
|
1620
|
+
'long',
|
|
1621
|
+
'native',
|
|
1622
|
+
'new',
|
|
1623
|
+
'package',
|
|
1624
|
+
'private',
|
|
1625
|
+
'protected',
|
|
1626
|
+
'public',
|
|
1627
|
+
'return',
|
|
1628
|
+
'short',
|
|
1629
|
+
'static',
|
|
1630
|
+
'strictfp',
|
|
1631
|
+
'super',
|
|
1632
|
+
'switch',
|
|
1633
|
+
'synchronized',
|
|
1634
|
+
'this',
|
|
1635
|
+
'throw',
|
|
1636
|
+
'throws',
|
|
1637
|
+
'transient',
|
|
1638
|
+
'try',
|
|
1639
|
+
'void',
|
|
1640
|
+
'volatile',
|
|
1641
|
+
'while',
|
|
1642
|
+
]),
|
|
1643
|
+
|
|
1644
|
+
golang: new Set([
|
|
1645
|
+
'break',
|
|
1646
|
+
'case',
|
|
1647
|
+
'chan',
|
|
1648
|
+
'const',
|
|
1649
|
+
'continue',
|
|
1650
|
+
'default',
|
|
1651
|
+
'defer',
|
|
1652
|
+
'else',
|
|
1653
|
+
'fallthrough',
|
|
1654
|
+
'for',
|
|
1655
|
+
'func',
|
|
1656
|
+
'go',
|
|
1657
|
+
'goto',
|
|
1658
|
+
'if',
|
|
1659
|
+
'import',
|
|
1660
|
+
'interface',
|
|
1661
|
+
'map',
|
|
1662
|
+
'package',
|
|
1663
|
+
'range',
|
|
1664
|
+
'return',
|
|
1665
|
+
'select',
|
|
1666
|
+
'struct',
|
|
1667
|
+
'switch',
|
|
1668
|
+
'type',
|
|
1669
|
+
'var',
|
|
1670
|
+
]),
|
|
1671
|
+
|
|
1672
|
+
c: new Set([
|
|
1673
|
+
'auto',
|
|
1674
|
+
'break',
|
|
1675
|
+
'case',
|
|
1676
|
+
'char',
|
|
1677
|
+
'const',
|
|
1678
|
+
'continue',
|
|
1679
|
+
'default',
|
|
1680
|
+
'do',
|
|
1681
|
+
'double',
|
|
1682
|
+
'else',
|
|
1683
|
+
'enum',
|
|
1684
|
+
'extern',
|
|
1685
|
+
'float',
|
|
1686
|
+
'for',
|
|
1687
|
+
'goto',
|
|
1688
|
+
'if',
|
|
1689
|
+
'int',
|
|
1690
|
+
'long',
|
|
1691
|
+
'register',
|
|
1692
|
+
'return',
|
|
1693
|
+
'short',
|
|
1694
|
+
'signed',
|
|
1695
|
+
'sizeof',
|
|
1696
|
+
'static',
|
|
1697
|
+
'struct',
|
|
1698
|
+
'switch',
|
|
1699
|
+
'typedef',
|
|
1700
|
+
'union',
|
|
1701
|
+
'unsigned',
|
|
1702
|
+
'void',
|
|
1703
|
+
'volatile',
|
|
1704
|
+
'while',
|
|
1705
|
+
]),
|
|
1706
|
+
|
|
1707
|
+
cpp: new Set([
|
|
1708
|
+
'alignas',
|
|
1709
|
+
'alignof',
|
|
1710
|
+
'and',
|
|
1711
|
+
'auto',
|
|
1712
|
+
'bool',
|
|
1713
|
+
'break',
|
|
1714
|
+
'case',
|
|
1715
|
+
'catch',
|
|
1716
|
+
'class',
|
|
1717
|
+
'const',
|
|
1718
|
+
'constexpr',
|
|
1719
|
+
'continue',
|
|
1720
|
+
'delete',
|
|
1721
|
+
'do',
|
|
1722
|
+
'double',
|
|
1723
|
+
'else',
|
|
1724
|
+
'enum',
|
|
1725
|
+
'explicit',
|
|
1726
|
+
'export',
|
|
1727
|
+
'extern',
|
|
1728
|
+
'false',
|
|
1729
|
+
'float',
|
|
1730
|
+
'for',
|
|
1731
|
+
'friend',
|
|
1732
|
+
'if',
|
|
1733
|
+
'inline',
|
|
1734
|
+
'int',
|
|
1735
|
+
'long',
|
|
1736
|
+
'mutable',
|
|
1737
|
+
'namespace',
|
|
1738
|
+
'new',
|
|
1739
|
+
'noexcept',
|
|
1740
|
+
'nullptr',
|
|
1741
|
+
'operator',
|
|
1742
|
+
'private',
|
|
1743
|
+
'protected',
|
|
1744
|
+
'public',
|
|
1745
|
+
'return',
|
|
1746
|
+
'short',
|
|
1747
|
+
'signed',
|
|
1748
|
+
'sizeof',
|
|
1749
|
+
'static',
|
|
1750
|
+
'struct',
|
|
1751
|
+
'switch',
|
|
1752
|
+
'template',
|
|
1753
|
+
'this',
|
|
1754
|
+
'throw',
|
|
1755
|
+
'true',
|
|
1756
|
+
'try',
|
|
1757
|
+
'typedef',
|
|
1758
|
+
'typename',
|
|
1759
|
+
'union',
|
|
1760
|
+
'unsigned',
|
|
1761
|
+
'using',
|
|
1762
|
+
'virtual',
|
|
1763
|
+
'void',
|
|
1764
|
+
'while',
|
|
1765
|
+
]),
|
|
1766
|
+
|
|
1767
|
+
php: new Set([
|
|
1768
|
+
'abstract',
|
|
1769
|
+
'and',
|
|
1770
|
+
'array',
|
|
1771
|
+
'as',
|
|
1772
|
+
'break',
|
|
1773
|
+
'callable',
|
|
1774
|
+
'case',
|
|
1775
|
+
'catch',
|
|
1776
|
+
'class',
|
|
1777
|
+
'clone',
|
|
1778
|
+
'const',
|
|
1779
|
+
'continue',
|
|
1780
|
+
'declare',
|
|
1781
|
+
'default',
|
|
1782
|
+
'do',
|
|
1783
|
+
'echo',
|
|
1784
|
+
'else',
|
|
1785
|
+
'elseif',
|
|
1786
|
+
'empty',
|
|
1787
|
+
'enddeclare',
|
|
1788
|
+
'endfor',
|
|
1789
|
+
'endforeach',
|
|
1790
|
+
'endif',
|
|
1791
|
+
'endswitch',
|
|
1792
|
+
'endwhile',
|
|
1793
|
+
'extends',
|
|
1794
|
+
'final',
|
|
1795
|
+
'finally',
|
|
1796
|
+
'fn',
|
|
1797
|
+
'for',
|
|
1798
|
+
'foreach',
|
|
1799
|
+
'function',
|
|
1800
|
+
'global',
|
|
1801
|
+
'goto',
|
|
1802
|
+
'if',
|
|
1803
|
+
'implements',
|
|
1804
|
+
'include',
|
|
1805
|
+
'include_once',
|
|
1806
|
+
'instanceof',
|
|
1807
|
+
'interface',
|
|
1808
|
+
'match',
|
|
1809
|
+
'namespace',
|
|
1810
|
+
'new',
|
|
1811
|
+
'null',
|
|
1812
|
+
'or',
|
|
1813
|
+
'private',
|
|
1814
|
+
'protected',
|
|
1815
|
+
'public',
|
|
1816
|
+
'require',
|
|
1817
|
+
'require_once',
|
|
1818
|
+
'return',
|
|
1819
|
+
'static',
|
|
1820
|
+
'switch',
|
|
1821
|
+
'throw',
|
|
1822
|
+
'trait',
|
|
1823
|
+
'try',
|
|
1824
|
+
'use',
|
|
1825
|
+
'var',
|
|
1826
|
+
'while',
|
|
1827
|
+
'yield',
|
|
1828
|
+
]),
|
|
1829
|
+
|
|
1830
|
+
rust: new Set([
|
|
1831
|
+
'as',
|
|
1832
|
+
'break',
|
|
1833
|
+
'const',
|
|
1834
|
+
'continue',
|
|
1835
|
+
'crate',
|
|
1836
|
+
'else',
|
|
1837
|
+
'enum',
|
|
1838
|
+
'extern',
|
|
1839
|
+
'false',
|
|
1840
|
+
'fn',
|
|
1841
|
+
'for',
|
|
1842
|
+
'if',
|
|
1843
|
+
'impl',
|
|
1844
|
+
'in',
|
|
1845
|
+
'let',
|
|
1846
|
+
'loop',
|
|
1847
|
+
'match',
|
|
1848
|
+
'mod',
|
|
1849
|
+
'move',
|
|
1850
|
+
'mut',
|
|
1851
|
+
'pub',
|
|
1852
|
+
'ref',
|
|
1853
|
+
'return',
|
|
1854
|
+
'self',
|
|
1855
|
+
'Self',
|
|
1856
|
+
'static',
|
|
1857
|
+
'struct',
|
|
1858
|
+
'super',
|
|
1859
|
+
'trait',
|
|
1860
|
+
'true',
|
|
1861
|
+
'type',
|
|
1862
|
+
'unsafe',
|
|
1863
|
+
'use',
|
|
1864
|
+
'where',
|
|
1865
|
+
'while',
|
|
1866
|
+
]),
|
|
1867
|
+
|
|
1868
|
+
html: new Set([
|
|
1869
|
+
'html',
|
|
1870
|
+
'head',
|
|
1871
|
+
'body',
|
|
1872
|
+
'div',
|
|
1873
|
+
'span',
|
|
1874
|
+
'p',
|
|
1875
|
+
'a',
|
|
1876
|
+
'img',
|
|
1877
|
+
'video',
|
|
1878
|
+
'audio',
|
|
1879
|
+
'script',
|
|
1880
|
+
'style',
|
|
1881
|
+
'link',
|
|
1882
|
+
'meta',
|
|
1883
|
+
'form',
|
|
1884
|
+
'input',
|
|
1885
|
+
'button',
|
|
1886
|
+
'table',
|
|
1887
|
+
'tr',
|
|
1888
|
+
'td',
|
|
1889
|
+
'th',
|
|
1890
|
+
'ul',
|
|
1891
|
+
'ol',
|
|
1892
|
+
'li',
|
|
1893
|
+
'section',
|
|
1894
|
+
'article',
|
|
1895
|
+
'header',
|
|
1896
|
+
'footer',
|
|
1897
|
+
'nav',
|
|
1898
|
+
'main',
|
|
1899
|
+
]),
|
|
1900
|
+
|
|
1901
|
+
bash: new Set([
|
|
1902
|
+
'if',
|
|
1903
|
+
'then',
|
|
1904
|
+
'else',
|
|
1905
|
+
'elif',
|
|
1906
|
+
'fi',
|
|
1907
|
+
'for',
|
|
1908
|
+
'while',
|
|
1909
|
+
'do',
|
|
1910
|
+
'done',
|
|
1911
|
+
'case',
|
|
1912
|
+
'esac',
|
|
1913
|
+
'function',
|
|
1914
|
+
'in',
|
|
1915
|
+
'select',
|
|
1916
|
+
'until',
|
|
1917
|
+
'break',
|
|
1918
|
+
'continue',
|
|
1919
|
+
'return',
|
|
1920
|
+
'export',
|
|
1921
|
+
'readonly',
|
|
1922
|
+
'local',
|
|
1923
|
+
'declare',
|
|
1924
|
+
]),
|
|
1925
|
+
|
|
1926
|
+
markdown: new Set(['#', '##', '###', '####', '#####', '######']),
|
|
1927
|
+
};
|
|
1928
|
+
|
|
1929
|
+
if (!lang || lang === 'txt' || lang === 'text' || lang === 'plaintext') {
|
|
1930
|
+
return {
|
|
1931
|
+
codeBlock: [
|
|
1932
|
+
{
|
|
1933
|
+
codeContent: code,
|
|
1934
|
+
highlightType: 0,
|
|
1935
|
+
},
|
|
1936
|
+
],
|
|
1937
|
+
unified_codeBlock: [
|
|
1938
|
+
{
|
|
1939
|
+
content: code,
|
|
1940
|
+
type: 'DEFAULT',
|
|
1941
|
+
},
|
|
1942
|
+
],
|
|
1943
|
+
};
|
|
1944
|
+
}
|
|
1945
|
+
|
|
1946
|
+
const TYPE_MAP = {
|
|
1947
|
+
0: 'DEFAULT',
|
|
1948
|
+
1: 'KEYWORD',
|
|
1949
|
+
2: 'METHOD',
|
|
1950
|
+
3: 'STR',
|
|
1951
|
+
4: 'NUMBER',
|
|
1952
|
+
5: 'COMMENT',
|
|
1953
|
+
};
|
|
1954
|
+
|
|
1955
|
+
const keywords = keywordsMap[lang.toLowerCase()] || new Set();
|
|
1956
|
+
const tokens = [];
|
|
1957
|
+
|
|
1958
|
+
let i = 0;
|
|
1959
|
+
|
|
1960
|
+
const push = (content, type) => {
|
|
1961
|
+
if (!content) return;
|
|
1962
|
+
|
|
1963
|
+
const last = tokens[tokens.length - 1];
|
|
1964
|
+
|
|
1965
|
+
if (last && last.highlightType === type) {
|
|
1966
|
+
last.codeContent += content;
|
|
1967
|
+
} else {
|
|
1968
|
+
tokens.push({
|
|
1969
|
+
codeContent: content,
|
|
1970
|
+
highlightType: type,
|
|
1971
|
+
});
|
|
1972
|
+
}
|
|
1973
|
+
};
|
|
1974
|
+
|
|
1975
|
+
const isIdentifier = (char) => {
|
|
1976
|
+
switch (lang.toLowerCase()) {
|
|
1977
|
+
case 'css':
|
|
1978
|
+
return /[a-zA-Z0-9_$-]/.test(char);
|
|
1979
|
+
|
|
1980
|
+
case 'html':
|
|
1981
|
+
return /[a-zA-Z0-9_$:-]/.test(char);
|
|
1982
|
+
|
|
1983
|
+
default:
|
|
1984
|
+
return /[a-zA-Z0-9_$]/.test(char);
|
|
1985
|
+
}
|
|
1986
|
+
};
|
|
1987
|
+
|
|
1988
|
+
while (i < code.length) {
|
|
1989
|
+
const c = code[i];
|
|
1990
|
+
|
|
1991
|
+
if (/\s/.test(c)) {
|
|
1992
|
+
let s = i;
|
|
1993
|
+
|
|
1994
|
+
while (i < code.length && /\s/.test(code[i])) {
|
|
1995
|
+
i++;
|
|
1996
|
+
}
|
|
1997
|
+
|
|
1998
|
+
push(code.slice(s, i), 0);
|
|
1999
|
+
continue;
|
|
2000
|
+
}
|
|
2001
|
+
|
|
2002
|
+
if ((c === '/' && code[i + 1] === '/') || (c === '#' && ['python', 'bash'].includes(lang))) {
|
|
2003
|
+
let s = i;
|
|
2004
|
+
|
|
2005
|
+
while (i < code.length && code[i] !== '\n') {
|
|
2006
|
+
i++;
|
|
2007
|
+
}
|
|
2008
|
+
|
|
2009
|
+
push(code.slice(s, i), 5);
|
|
2010
|
+
continue;
|
|
2011
|
+
}
|
|
2012
|
+
|
|
2013
|
+
if (c === '"' || c === "'" || c === '`') {
|
|
2014
|
+
let s = i;
|
|
2015
|
+
const q = c;
|
|
2016
|
+
|
|
2017
|
+
i++;
|
|
2018
|
+
|
|
2019
|
+
while (i < code.length) {
|
|
2020
|
+
if (code[i] === '\\' && i + 1 < code.length) {
|
|
2021
|
+
i += 2;
|
|
2022
|
+
} else if (code[i] === q) {
|
|
2023
|
+
i++;
|
|
2024
|
+
break;
|
|
2025
|
+
} else {
|
|
2026
|
+
i++;
|
|
2027
|
+
}
|
|
2028
|
+
}
|
|
2029
|
+
|
|
2030
|
+
push(code.slice(s, i), 3);
|
|
2031
|
+
continue;
|
|
2032
|
+
}
|
|
2033
|
+
|
|
2034
|
+
if (/[0-9]/.test(c)) {
|
|
2035
|
+
let s = i;
|
|
2036
|
+
|
|
2037
|
+
while (i < code.length && /[0-9._]/.test(code[i])) {
|
|
2038
|
+
i++;
|
|
2039
|
+
}
|
|
2040
|
+
|
|
2041
|
+
push(code.slice(s, i), 4);
|
|
2042
|
+
continue;
|
|
2043
|
+
}
|
|
2044
|
+
|
|
2045
|
+
if (/[a-zA-Z_$]/.test(c)) {
|
|
2046
|
+
let s = i;
|
|
2047
|
+
|
|
2048
|
+
while (i < code.length && isIdentifier(code[i])) {
|
|
2049
|
+
i++;
|
|
2050
|
+
}
|
|
2051
|
+
|
|
2052
|
+
const word = code.slice(s, i);
|
|
2053
|
+
|
|
2054
|
+
let type = 0;
|
|
2055
|
+
|
|
2056
|
+
if (keywords.has(word)) {
|
|
2057
|
+
type = 1;
|
|
2058
|
+
} else if (lang === 'css') {
|
|
2059
|
+
let j = i;
|
|
2060
|
+
|
|
2061
|
+
while (j < code.length && /\s/.test(code[j])) {
|
|
2062
|
+
j++;
|
|
2063
|
+
}
|
|
2064
|
+
|
|
2065
|
+
if (code[j] === ':') {
|
|
2066
|
+
type = 1;
|
|
2067
|
+
}
|
|
2068
|
+
} else if (lang === 'html') {
|
|
2069
|
+
let p = s - 1;
|
|
2070
|
+
|
|
2071
|
+
while (p >= 0 && /\s/.test(code[p])) {
|
|
2072
|
+
p--;
|
|
2073
|
+
}
|
|
2074
|
+
|
|
2075
|
+
if (code[p] === '<' || (code[p] === '/' && code[p - 1] === '<')) {
|
|
2076
|
+
type = 1;
|
|
2077
|
+
}
|
|
2078
|
+
}
|
|
2079
|
+
|
|
2080
|
+
if (type === 0) {
|
|
2081
|
+
let j = i;
|
|
2082
|
+
|
|
2083
|
+
while (j < code.length && /\s/.test(code[j])) {
|
|
2084
|
+
j++;
|
|
2085
|
+
}
|
|
2086
|
+
|
|
2087
|
+
if (code[j] === '(') {
|
|
2088
|
+
type = 2;
|
|
2089
|
+
}
|
|
2090
|
+
}
|
|
2091
|
+
|
|
2092
|
+
push(word, type);
|
|
2093
|
+
continue;
|
|
2094
|
+
}
|
|
2095
|
+
|
|
2096
|
+
push(c, 0);
|
|
2097
|
+
i++;
|
|
2098
|
+
}
|
|
2099
|
+
|
|
2100
|
+
return {
|
|
2101
|
+
codeBlock: tokens,
|
|
2102
|
+
unified_codeBlock: tokens.map((t) => ({
|
|
2103
|
+
content: t.codeContent,
|
|
2104
|
+
type: TYPE_MAP[t.highlightType],
|
|
2105
|
+
})),
|
|
2106
|
+
};
|
|
2107
|
+
}
|
|
2108
|
+
|
|
2109
|
+
/** Convert a raw `string[][]` grid into the table metadata shape addTable()/addText() produce internally. */
|
|
2110
|
+
static toTableMetadata(arr, { hyperlink = true, citation = true, latex = true } = {}) {
|
|
2111
|
+
if (!Array.isArray(arr) || !arr.every((row) => Array.isArray(row) && row.every((cell) => typeof cell === 'string'))) {
|
|
2112
|
+
throw new TypeError('Table must be a nested array of strings');
|
|
2113
|
+
}
|
|
2114
|
+
|
|
2115
|
+
const [header, ...rows] = arr;
|
|
2116
|
+
|
|
2117
|
+
const maxLen = Math.max(header.length, ...rows.map((r) => r.length));
|
|
2118
|
+
|
|
2119
|
+
const normalize = (r) => [...r, ...Array(maxLen - r.length).fill('')];
|
|
2120
|
+
|
|
2121
|
+
const unified_rows = [
|
|
2122
|
+
{
|
|
2123
|
+
is_header: true,
|
|
2124
|
+
cells: normalize(header),
|
|
2125
|
+
},
|
|
2126
|
+
...rows.map((r) => ({
|
|
2127
|
+
is_header: false,
|
|
2128
|
+
cells: normalize(r),
|
|
2129
|
+
})),
|
|
2130
|
+
].map((row) => {
|
|
2131
|
+
const markdown_cells = row.cells.map((cell) => {
|
|
2132
|
+
const extracted = extractIE(cell, { hyperlink, citation, latex });
|
|
2133
|
+
|
|
2134
|
+
return {
|
|
2135
|
+
text: extracted.text,
|
|
2136
|
+
...(extracted.inline_entities.length ? { inline_entities: extracted.inline_entities } : {}),
|
|
2137
|
+
};
|
|
2138
|
+
});
|
|
2139
|
+
|
|
2140
|
+
return {
|
|
2141
|
+
...row,
|
|
2142
|
+
...(markdown_cells.some((c) => c.inline_entities?.length) ? { markdown_cells } : {}),
|
|
2143
|
+
};
|
|
2144
|
+
});
|
|
2145
|
+
|
|
2146
|
+
const rowsMeta = unified_rows.map((r) => ({
|
|
2147
|
+
items: r.cells,
|
|
2148
|
+
...(r.is_header ? { isHeading: true } : {}),
|
|
2149
|
+
}));
|
|
2150
|
+
|
|
2151
|
+
return {
|
|
2152
|
+
title: '',
|
|
2153
|
+
rows: rowsMeta,
|
|
2154
|
+
unified_rows,
|
|
2155
|
+
};
|
|
2156
|
+
}
|
|
2157
|
+
|
|
2158
|
+
/**
|
|
2159
|
+
* Add an "AI is generating..." placeholder card (`GenAIImaginePrimitive` with status
|
|
2160
|
+
* GENERATING) — distinct from addImage()/addVideo() which always send status READY.
|
|
2161
|
+
* Use this to show a pending-generation state before the real media is ready.
|
|
2162
|
+
* @param {{ imagine_type?: 'IMAGE'|'ANIMATE', estimated_completion_time?: number }} [options]
|
|
2163
|
+
*/
|
|
2164
|
+
addGenerating({ imagine_type = 'IMAGE', estimated_completion_time, textFallback = true } = {}) {
|
|
2165
|
+
this._submessages.push({ messageType: 2, messageText: '[ Sedang diproses... ]' });
|
|
2166
|
+
this._sections.push(
|
|
2167
|
+
AIRich.newLayout('Single', {
|
|
2168
|
+
media: { url: '', mime_type: imagine_type === 'ANIMATE' ? 'video/mp4' : 'image/png' },
|
|
2169
|
+
imagine_type,
|
|
2170
|
+
status: {
|
|
2171
|
+
status: 'GENERATING',
|
|
2172
|
+
estimated_completion_time: estimated_completion_time ?? Math.floor(Date.now() / 1000) + 30,
|
|
2173
|
+
},
|
|
2174
|
+
__typename: 'GenAIImaginePrimitive',
|
|
2175
|
+
})
|
|
2176
|
+
);
|
|
2177
|
+
// JAP@Fix 23-08-26 (v4.8) --- media.url kosong + status GENERATING gak punya renderer
|
|
2178
|
+
// visual instan di stock WA client; sebelumnya cuma diem sampe WA nge-timeout sendiri
|
|
2179
|
+
// dan nampilin fallback bawaannya ("Saat ini, saya tidak bisa membuat gambar itu...").
|
|
2180
|
+
// Same fix class kayak addTask/addBloks: append FOATextPrimitive biar ada fallback
|
|
2181
|
+
// instan, gak perlu nunggu timeout WA. Set { textFallback: false } buat skip.
|
|
2182
|
+
if (textFallback) {
|
|
2183
|
+
this._sections.push(AIRich.newLayout('Single', { text: '[ Sedang diproses... ]', __typename: 'FOATextPrimitive' }));
|
|
2184
|
+
}
|
|
2185
|
+
return this;
|
|
2186
|
+
}
|
|
2187
|
+
|
|
2188
|
+
/**
|
|
2189
|
+
* Send a support-ticket marker message (`messageContextInfo.supportPayload`) — a plain
|
|
2190
|
+
* conversation message tagged as an AI/support-bot ticket, distinct from richResponseMessage.
|
|
2191
|
+
* @param {import('../../WAProto/index.js').WASocket} client
|
|
2192
|
+
* @param {string} jid
|
|
2193
|
+
* @param {string} text
|
|
2194
|
+
* @param {{ ticketId?: string, isAiMessage?: boolean, shouldShowSystemMessage?: boolean, version?: number }} [options]
|
|
2195
|
+
*/
|
|
2196
|
+
static async sendSupportPayload(client, jid, text, { ticketId = crypto.randomUUID(), isAiMessage = true, shouldShowSystemMessage = true, version = 1 } = {}) {
|
|
2197
|
+
if (!client) throw new Error('Socket is required');
|
|
2198
|
+
if (typeof text !== 'string' || !text) throw new TypeError('sendSupportPayload(client, jid, text) requires a non-empty string text');
|
|
2199
|
+
|
|
2200
|
+
const msg = {
|
|
2201
|
+
conversation: text,
|
|
2202
|
+
messageContextInfo: {
|
|
2203
|
+
messageSecret: crypto.randomBytes(32),
|
|
2204
|
+
supportPayload: JSON.stringify({
|
|
2205
|
+
version,
|
|
2206
|
+
is_ai_message: isAiMessage,
|
|
2207
|
+
should_show_system_message: shouldShowSystemMessage,
|
|
2208
|
+
ticket_id: ticketId,
|
|
2209
|
+
}),
|
|
2210
|
+
},
|
|
2211
|
+
};
|
|
2212
|
+
|
|
2213
|
+
return client.relayMessage(jid, msg, {
|
|
2214
|
+
additionalNodes: [
|
|
2215
|
+
{ tag: 'bot', attrs: { biz_bot: '1' } },
|
|
2216
|
+
{ tag: 'biz', attrs: {} },
|
|
2217
|
+
],
|
|
2218
|
+
});
|
|
2219
|
+
}
|
|
2220
|
+
|
|
2221
|
+
/**
|
|
2222
|
+
* Send an image and video as one paired-media unit (image sent first, video linked to it via
|
|
2223
|
+
* `messageAssociation`). Distinct from a plain album — the client treats them as a single group.
|
|
2224
|
+
* @param {import('../../WAProto/index.js').WASocket} client
|
|
2225
|
+
* @param {string} jid
|
|
2226
|
+
* @param {{ image: string|Buffer, video: string|Buffer }} media
|
|
2227
|
+
*/
|
|
2228
|
+
static async sendPairedMedia(client, jid, { image, video } = {}) {
|
|
2229
|
+
if (!client) throw new Error('Socket is required');
|
|
2230
|
+
if (!image || !video) throw new TypeError('sendPairedMedia() requires both "image" and "video"');
|
|
2231
|
+
|
|
2232
|
+
const imagePrepared = await prepareWAMessageMedia(
|
|
2233
|
+
{ image: typeof image === 'string' ? { url: image } : image },
|
|
2234
|
+
{ upload: client.waUploadToServer }
|
|
2235
|
+
);
|
|
2236
|
+
const videoPrepared = await prepareWAMessageMedia(
|
|
2237
|
+
{ video: typeof video === 'string' ? { url: video } : video },
|
|
2238
|
+
{ upload: client.waUploadToServer }
|
|
2239
|
+
);
|
|
2240
|
+
|
|
2241
|
+
const imageMsg = generateWAMessageFromContent(
|
|
2242
|
+
jid,
|
|
2243
|
+
{
|
|
2244
|
+
imageMessage: {
|
|
2245
|
+
...imagePrepared.imageMessage,
|
|
2246
|
+
contextInfo: { pairedMediaType: 5, statusSourceType: 0 },
|
|
2247
|
+
},
|
|
2248
|
+
},
|
|
2249
|
+
{}
|
|
2250
|
+
);
|
|
2251
|
+
|
|
2252
|
+
await client.relayMessage(jid, imageMsg.message, { messageId: imageMsg.key.id });
|
|
2253
|
+
|
|
2254
|
+
await client.relayMessage(
|
|
2255
|
+
jid,
|
|
2256
|
+
{
|
|
2257
|
+
videoMessage: {
|
|
2258
|
+
...videoPrepared.videoMessage,
|
|
2259
|
+
contextInfo: { pairedMediaType: 6, statusSourceType: 0 },
|
|
2260
|
+
},
|
|
2261
|
+
messageContextInfo: {
|
|
2262
|
+
messageAssociation: { associationType: 12, parentMessageKey: imageMsg.key },
|
|
2263
|
+
},
|
|
2264
|
+
},
|
|
2265
|
+
{}
|
|
2266
|
+
);
|
|
2267
|
+
|
|
2268
|
+
return imageMsg.key;
|
|
2269
|
+
}
|
|
2270
|
+
|
|
2271
|
+
/** Build a raw submessage layout block by name — escape hatch for layouts not covered by the add*() helpers. */
|
|
2272
|
+
static newLayout(name, data, extra = {}) {
|
|
2273
|
+
return {
|
|
2274
|
+
...extra,
|
|
2275
|
+
view_model: {
|
|
2276
|
+
[Array.isArray(data) ? 'primitives' : 'primitive']: data,
|
|
2277
|
+
__typename: `GenAI${name}LayoutViewModel`,
|
|
2278
|
+
},
|
|
2279
|
+
};
|
|
2280
|
+
}
|
|
2281
|
+
}
|
|
2282
|
+
|
|
2283
|
+
/** Thin no-op subclass of `AIRich` — kept as a legacy alias for code that references `ORich` by name. */
|
|
2284
|
+
class ORich extends AIRich {}
|
|
2285
|
+
|
|
2286
|
+
export { AIRich, ORich };
|