aunubaileys 1.0.43 → 1.0.45

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/README.md CHANGED
@@ -64,6 +64,16 @@ Everything else is pure upstream. When upstream moves, we rebase the patch layer
64
64
 
65
65
 
66
66
 
67
+ ### 🆕 What's new in 1.0.45 / Baru di 1.0.45
68
+
69
+ - 📞 **VOIP call support (from plogme, original implementation)** — one combined release from an 8-package sweep (raiden-baileys, @neoxr/wb, @zervonex/baileys, @itsliaaa/starseal, alya-baileys, @ryuu-reinzz/baileys, @cogitator-ai/channels, plogme — all audited clean). New socket methods: `offerCall(jid, isVideo?)` and `groupCall(groupJid, participants, isVideo?)` build the `<offer>` with a per-device encrypted call-key envelope, `cancelGroupCall(groupJid, callId)`, and `acceptCall(callId, callFrom)` / `terminateCall(callId, callFrom, reason?)` join the existing `rejectCall`. Pure stanza builders are exported from `lib/Utils/call-utils.js` so the wire shapes are unit-testable. The stanza shapes follow the WhatsApp call protocol; the code is an original AunuBaileys implementation (plogme's license forbids copying its code). This is the signalling layer — two-way VOIP media is out of scope. plogme's voip-rekey decoder was **not** ported: the 6.7.24 proto has no `E2eRekeyPayload`.
70
+ - 🌐 **HTML messages — EXPERIMENTAL (Aunu's call):** content `{ html, trustedSources }` adds an `FOAHtmlPrimitiveDemoDONOTUSE` section to the unified response (or use `prepareHtmlMessage(...)`), and `{ richHtml: { html, title, tabs, ... } }` / `prepareRichHtmlMessage(...)` builds an interactive screen as an embedded screen (`GenAIaeacdsnwHtmlPrimitive` inside `FOAIDNixelButtonSheets` tabs) — the surface interactive HTML+JS lives on. The library transports the payload intact; whether the receiving WhatsApp client renders/executes it depends on the client version and is not guaranteed.
71
+ - 🏷️ **`addStickerExif(webpBuffer, { packId?, packname?, author?, emojis?, categories? })`** (the @itsliaaa/starseal idea): writes sticker EXIF metadata into a WebP buffer — an existing EXIF chunk is replaced, not stacked; non-WebP input is rejected.
72
+
73
+ ### 🆕 What's new in 1.0.44 / Baru di 1.0.44
74
+
75
+ - 🖼️ **A third image backend: `@napi-rs/image`** in `getImageProcessingLibrary`, between sharp and jimp (priority: sharp → @napi-rs/image → jimp). `extractImageThumb` and `generateProfilePicture` gain a napi branch (Transformer: metadata + resize + quality-50 JPEG, matching the existing branches), and the package now declares `@napi-rs/image ^1.12.0` as an optional peer dependency. Consumers without sharp get a fast native path instead of falling back to jimp. Follows @itsliaaa/baileys.
76
+
67
77
  ### 🆕 What's new in 1.0.43 / Baru di 1.0.43
68
78
 
69
79
  - 🎬 **Video thumbnails now invoke FFmpeg via `spawn` with an argument array** instead of a shell-executed command string (`exec`). `extractVideoThumb` builds the invocation as data rather than shell text, so a file path can no longer smuggle shell commands into it; the function signature and behaviour are unchanged (file output, same caller in `generateThumbnail`). The pattern follows @itsliaaa/baileys; their pipe-to-buffer redesign was **not** adopted because it would change this function's public behaviour.
@@ -7,6 +7,8 @@ export declare const makeMessagesRecvSocket: (config: SocketConfig) => {
7
7
  sendMessageAck: ({ tag, attrs, content }: BinaryNode, errorCode?: number) => Promise<void>;
8
8
  sendRetryRequest: (node: BinaryNode, forceIncludeKeys?: boolean) => Promise<void>;
9
9
  rejectCall: (callId: string, callFrom: string) => Promise<void>;
10
+ acceptCall: (callId: string, callFrom: string) => Promise<void>;
11
+ terminateCall: (callId: string, callFrom: string, reason?: string) => Promise<void>;
10
12
  fetchMessageHistory: (count: number, oldestMsgKey: WAMessageKey, oldestMsgTimestamp: number | Long) => Promise<string>;
11
13
  requestPlaceholderResend: (messageKey: WAMessageKey) => Promise<string | undefined>;
12
14
  getPrivacyTokens: (jids: string[]) => Promise<any>;
@@ -5,7 +5,7 @@ import Long from 'long';
5
5
  import { proto } from '../../WAProto/index.js';
6
6
  import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT } from '../Defaults/index.js';
7
7
  import { WAMessageStatus, WAMessageStubType } from '../Types/index.js';
8
- import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, parseRetryReceiptSession, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
8
+ import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, buildCallControlStanza, parseRetryReceiptSession, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
9
9
  import { makeMutex } from '../Utils/make-mutex.js';
10
10
  import { sentMessageCacheFor } from '../Utils/sent-message-cache.js';
11
11
  import { areJidsSameUser, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidStatusBroadcast, isJidUser, isLidUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
@@ -67,6 +67,28 @@ export const makeMessagesRecvSocket = (config) => {
67
67
  logger.debug({ recv: { tag, attrs }, sent: stanza.attrs }, 'sent ack');
68
68
  await sendNode(stanza);
69
69
  };
70
+ // AunuBaileys: terima panggilan masuk (node kontrol <accept>) — pasangan
71
+ // rejectCall bawaan; media VOIP-nya sendiri di luar cakupan protokol chat.
72
+ const acceptCall = async (callId, callFrom) => {
73
+ await query(buildCallControlStanza({
74
+ from: authState.creds.me.id,
75
+ to: callFrom,
76
+ action: 'accept',
77
+ callId,
78
+ callCreator: callFrom
79
+ }));
80
+ };
81
+ // AunuBaileys: akhiri panggilan yang sedang berjalan (<terminate>).
82
+ const terminateCall = async (callId, callFrom, reason = 'user-terminated') => {
83
+ await query(buildCallControlStanza({
84
+ from: authState.creds.me.id,
85
+ to: callFrom,
86
+ action: 'terminate',
87
+ callId,
88
+ callCreator: callFrom,
89
+ reason
90
+ }));
91
+ };
70
92
  const rejectCall = async (callId, callFrom) => {
71
93
  const stanza = {
72
94
  tag: 'call',
@@ -1201,6 +1223,8 @@ export const makeMessagesRecvSocket = (config) => {
1201
1223
  sendMessageAck,
1202
1224
  sendRetryRequest,
1203
1225
  rejectCall,
1226
+ acceptCall,
1227
+ terminateCall,
1204
1228
  fetchMessageHistory,
1205
1229
  requestPlaceholderResend
1206
1230
  };
@@ -23,6 +23,9 @@ export declare const makeMessagesSocket: (config: SocketConfig) => {
23
23
  getUSyncDevices: (jids: string[], useCache: boolean, ignoreZeroDevices: boolean) => Promise<JidWithDevice[]>;
24
24
  updateMediaMessage: (message: proto.IWebMessageInfo) => Promise<proto.IWebMessageInfo>;
25
25
  sendMessage: (jid: string, content: AnyMessageContent, options?: MiscMessageGenerationOptions) => Promise<proto.WebMessageInfo | undefined>;
26
+ offerCall: (toJid: string, isVideo?: boolean) => Promise<{ id: string; to: string }>;
27
+ groupCall: (groupJid: string, participants?: string[], isVideo?: boolean) => Promise<{ id: string; to: string }>;
28
+ cancelGroupCall: (groupJid: string, callId: string) => Promise<{ id: string; to: string }>;
26
29
  newsletterCreate: (name: string, description?: string) => Promise<import("../Types/index.js").NewsletterMetadata>;
27
30
  newsletterUpdate: (jid: string, updates: import("../Types/index.js").NewsletterUpdate) => Promise<unknown>;
28
31
  newsletterSubscribers: (jid: string) => Promise<{
@@ -2,7 +2,7 @@ import NodeCache from '@cacheable/node-cache';
2
2
  import { Boom } from '@hapi/boom';
3
3
  import { proto } from '../../WAProto/index.js';
4
4
  import { BIZ_BOT_SUPPORT_PAYLOAD, DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
5
- import { aggregateMessageKeysNotFromMe, assertMediaContent, bindWaitForEvent, decryptMediaRetryData, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, delay, extractDeviceJids, generateMessageIDV2, generateWAMessage, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, normalizeMessageContent, parseAndInjectE2ESessions, unixTimestampSeconds, shouldIncludeBizBinaryNode } from '../Utils/index.js';
5
+ import { aggregateMessageKeysNotFromMe, assertMediaContent, bindWaitForEvent, decryptMediaRetryData, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, delay, extractDeviceJids, generateMessageIDV2, buildCallOfferContent, buildGroupCallCancelStanza, buildGroupCallStanza, createCallKey, newCallId, generateWAMessage, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, normalizeMessageContent, parseAndInjectE2ESessions, unixTimestampSeconds, shouldIncludeBizBinaryNode } from '../Utils/index.js';
6
6
  import { sentMessageCacheFor } from '../Utils/sent-message-cache.js';
7
7
  import { getUrlInfo } from '../Utils/link-preview.js';
8
8
  import { areJidsSameUser, getBinaryNodeChild, getBinaryNodeChildren, getBizBinaryNode, isJidGroup, isJidNewsletter, isJidUser, isLidUser, jidDecode, jidEncode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
@@ -609,11 +609,92 @@ export const makeMessagesSocket = (config) => {
609
609
  };
610
610
  const waUploadToServer = getWAUploadToServer(config, refreshMediaConn);
611
611
  const waitForMsgMediaUpdate = bindWaitForEvent(ev, 'messages.media-update');
612
+ // AunuBaileys: mulai panggilan suara/video 1:1 — susun <offer> dengan
613
+ // amplop callKey terenkripsi per perangkat tujuan (destination), sama
614
+ // seperti jalur fan-out relayMessage. Mengembalikan { id, to }.
615
+ const offerCall = async (toJid, isVideo = false) => {
616
+ const callId = newCallId();
617
+ const offerContent = buildCallOfferContent(isVideo);
618
+ const devices = (await getUSyncDevices([toJid], true, false)).map(({ user, device }) => jidEncode(user, 's.whatsapp.net', device));
619
+ await assertSessions(devices, true);
620
+ const { nodes: destinations, shouldIncludeDeviceIdentity } = await createParticipantNodes(devices, {
621
+ call: { callKey: createCallKey() }
622
+ }, { count: '0' });
623
+ offerContent.push({ tag: 'destination', attrs: {}, content: destinations });
624
+ if (shouldIncludeDeviceIdentity) {
625
+ offerContent.push({
626
+ tag: 'device-identity',
627
+ attrs: {},
628
+ content: encodeSignedDeviceIdentity(authState.creds.account, true)
629
+ });
630
+ }
631
+ const stanza = {
632
+ tag: 'call',
633
+ attrs: {
634
+ id: generateMessageIDV2(authState.creds.me?.id),
635
+ to: toJid
636
+ },
637
+ content: [
638
+ {
639
+ tag: 'offer',
640
+ attrs: {
641
+ 'call-id': callId,
642
+ 'call-creator': authState.creds.me.id
643
+ },
644
+ content: offerContent
645
+ }
646
+ ]
647
+ };
648
+ await query(stanza);
649
+ return { id: callId, to: toJid };
650
+ };
651
+ // AunuBaileys: mulai panggilan grup — <offer type="group"> ke JID grup,
652
+ // destination dienkripsi untuk perangkat para peserta yang di-ring.
653
+ const groupCall = async (groupJid, participants = [], isVideo = false) => {
654
+ const callId = newCallId();
655
+ const offerContent = buildCallOfferContent(isVideo);
656
+ const devices = (await getUSyncDevices(participants, true, false)).map(({ user, device }) => jidEncode(user, 's.whatsapp.net', device));
657
+ await assertSessions(devices, true);
658
+ const { nodes: destinations, shouldIncludeDeviceIdentity } = await createParticipantNodes(devices, {
659
+ call: { callKey: createCallKey() }
660
+ }, { count: '0' });
661
+ offerContent.push({ tag: 'destination', attrs: {}, content: destinations });
662
+ if (shouldIncludeDeviceIdentity) {
663
+ offerContent.push({
664
+ tag: 'device-identity',
665
+ attrs: {},
666
+ content: encodeSignedDeviceIdentity(authState.creds.account, true)
667
+ });
668
+ }
669
+ const stanza = buildGroupCallStanza({
670
+ id: generateMessageIDV2(authState.creds.me?.id),
671
+ to: groupJid,
672
+ callId,
673
+ callCreator: authState.creds.me.id,
674
+ offerContent
675
+ });
676
+ await query(stanza);
677
+ return { id: callId, to: groupJid };
678
+ };
679
+ // AunuBaileys: batalkan/akhiri panggilan grup yang dimulai groupCall.
680
+ const cancelGroupCall = async (groupJid, callId) => {
681
+ const stanza = buildGroupCallCancelStanza({
682
+ from: authState.creds.me.id,
683
+ to: groupJid,
684
+ callId,
685
+ callCreator: authState.creds.me.id
686
+ });
687
+ await query(stanza);
688
+ return { id: callId, to: groupJid };
689
+ };
612
690
  return {
613
691
  ...sock,
614
692
  getPrivacyTokens,
615
693
  assertSessions,
616
694
  relayMessage,
695
+ offerCall,
696
+ groupCall,
697
+ cancelGroupCall,
617
698
  sendReceipt,
618
699
  sendReceipts,
619
700
  readMessages,
@@ -269,6 +269,9 @@ export type AnyMessageContent = (AnyRegularMessageContent | {
269
269
  footerText?: string;
270
270
  disclaimerText?: string;
271
271
  noHeading?: boolean;
272
+ html?: string;
273
+ trustedSources?: string[];
274
+ richHtml?: { html?: string; title?: string; tabHeader?: string; url?: string; trustedSources?: string[]; tabs?: any[] };
272
275
  };
273
276
  export type GroupMetadataParticipants = Pick<GroupMetadata, 'participants'>;
274
277
  type MinimalRelayOptions = {
@@ -0,0 +1,25 @@
1
+ import type { BinaryNode } from '../WABinary/index.js';
2
+ export declare const newCallId: () => string;
3
+ export declare const createCallKey: () => Uint8Array;
4
+ export declare const buildCallOfferContent: (isVideo?: boolean) => BinaryNode[];
5
+ export declare const buildCallControlStanza: (opts: {
6
+ from: string;
7
+ to: string;
8
+ action: 'accept' | 'terminate' | 'reject';
9
+ callId: string;
10
+ callCreator: string;
11
+ reason?: string;
12
+ }) => BinaryNode;
13
+ export declare const buildGroupCallStanza: (opts: {
14
+ id: string;
15
+ to: string;
16
+ callId: string;
17
+ callCreator: string;
18
+ offerContent: BinaryNode[];
19
+ }) => BinaryNode;
20
+ export declare const buildGroupCallCancelStanza: (opts: {
21
+ from: string;
22
+ to: string;
23
+ callId: string;
24
+ callCreator: string;
25
+ }) => BinaryNode;
@@ -0,0 +1,81 @@
1
+ import { randomBytes } from 'crypto';
2
+ // AunuBaileys: pembangun stanza panggilan (VOIP) — fungsi murni, tanpa state
3
+ // socket, supaya bentuk wire-nya bisa diuji langsung. Bentuk stanza mengikuti
4
+ // protokol panggilan WhatsApp (node <call> dengan anak offer/accept/reject/
5
+ // terminate), implementasi orisinal AunuBaileys.
6
+ /** ID panggilan baru: 32 karakter hex huruf besar. */
7
+ export const newCallId = () => randomBytes(16).toString('hex').toUpperCase();
8
+ /** Kunci media panggilan baru (32 byte) untuk amplop destination terenkripsi. */
9
+ export const createCallKey = () => new Uint8Array(randomBytes(32));
10
+ /**
11
+ * Isi node <offer> untuk panggilan 1:1 maupun grup: codec audio opus
12
+ * (16 kHz + 8 kHz), video vp8 opsional, net, capability, dan encopt.
13
+ * Node destination/device-identity ditambahkan pemanggil setelah sesi siap.
14
+ */
15
+ export const buildCallOfferContent = (isVideo = false) => {
16
+ const content = [
17
+ { tag: 'audio', attrs: { enc: 'opus', rate: '16000' }, content: undefined },
18
+ { tag: 'audio', attrs: { enc: 'opus', rate: '8000' }, content: undefined }
19
+ ];
20
+ if (isVideo) {
21
+ content.push({
22
+ tag: 'video',
23
+ attrs: {
24
+ enc: 'vp8',
25
+ dec: 'vp8',
26
+ orientation: '0',
27
+ screen_width: '1920',
28
+ screen_height: '1080',
29
+ device_orientation: '0'
30
+ },
31
+ content: undefined
32
+ });
33
+ }
34
+ content.push({ tag: 'net', attrs: { medium: '3' }, content: undefined });
35
+ content.push({ tag: 'capability', attrs: { ver: '1' }, content: new Uint8Array([1, 4, 255, 131, 207, 4]) });
36
+ content.push({ tag: 'encopt', attrs: { keygen: '2' }, content: undefined });
37
+ return content;
38
+ };
39
+ /** Stanza <call> berisi satu node kontrol (accept / terminate / reject). */
40
+ export const buildCallControlStanza = ({ from, to, action, callId, callCreator, reason }) => ({
41
+ tag: 'call',
42
+ attrs: { from, to },
43
+ content: [
44
+ {
45
+ tag: action,
46
+ attrs: {
47
+ 'call-id': callId,
48
+ 'call-creator': callCreator,
49
+ ...(reason ? { reason } : {}),
50
+ count: '0'
51
+ },
52
+ content: undefined
53
+ }
54
+ ]
55
+ });
56
+ /** Stanza <call><offer type="group" group-jid=..> untuk memulai panggilan grup. */
57
+ export const buildGroupCallStanza = ({ id, to, callId, callCreator, offerContent }) => ({
58
+ tag: 'call',
59
+ attrs: { id, to },
60
+ content: [
61
+ {
62
+ tag: 'offer',
63
+ attrs: {
64
+ 'call-id': callId,
65
+ 'call-creator': callCreator,
66
+ type: 'group',
67
+ 'group-jid': to
68
+ },
69
+ content: offerContent
70
+ }
71
+ ]
72
+ });
73
+ /** Stanza <call><terminate> untuk membatalkan/mengakhiri panggilan grup. */
74
+ export const buildGroupCallCancelStanza = ({ from, to, callId, callCreator }) => buildCallControlStanza({
75
+ from,
76
+ to,
77
+ action: 'terminate',
78
+ callId,
79
+ callCreator,
80
+ reason: 'user-terminated'
81
+ });
@@ -3,6 +3,7 @@ export * from './decode-wa-message.js';
3
3
  export * from './messages.js';
4
4
  export * from './messages-media.js';
5
5
  export * from './rich-message-utils.js';
6
+ export * from './call-utils.js';
6
7
  export * from './validate-connection.js';
7
8
  export * from './crypto.js';
8
9
  export * from './signal.js';
@@ -110,4 +110,11 @@ export declare const decryptMediaRetryData: ({ ciphertext, iv }: {
110
110
  export declare const getStatusCodeForMediaRetry: (code: number) => 200 | 412 | 404 | 418;
111
111
  export {};
112
112
  //# sourceMappingURL=messages-media.d.ts.map
113
+ export declare const addStickerExif: (webpBuffer: Buffer, metadata?: {
114
+ packId?: string;
115
+ packname?: string;
116
+ author?: string;
117
+ emojis?: string[];
118
+ categories?: string[];
119
+ }) => Buffer;
113
120
  export declare const getImageProcessingLibrary: () => Promise<Record<string, any>>;
@@ -16,10 +16,14 @@ import { generateMessageIDV2 } from './generics.js';
16
16
  const getTmpFilesDirectory = () => tmpdir();
17
17
  export const getImageProcessingLibrary = async () => {
18
18
  //@ts-ignore
19
- const [jimp, sharp] = await Promise.all([import('jimp').catch(() => { }), import('sharp').catch(() => { })]);
19
+ const [jimp, sharp, image] = await Promise.all([import('jimp').catch(() => { }), import('sharp').catch(() => { }), import('@napi-rs/image').catch(() => { })]);
20
20
  if (sharp) {
21
21
  return { sharp };
22
22
  }
23
+ // AunuBaileys: backend @napi-rs/image (dari @itsliaaa/baileys) — di atas jimp
24
+ if (image) {
25
+ return { image };
26
+ }
23
27
  if (jimp) {
24
28
  return { jimp };
25
29
  }
@@ -127,6 +131,22 @@ export const extractImageThumb = async (bufferOrFilePath, width = 32) => {
127
131
  }
128
132
  };
129
133
  }
134
+ else if ('image' in lib && lib.image?.Transformer) {
135
+ // AunuBaileys: backend @napi-rs/image
136
+ if (!Buffer.isBuffer(bufferOrFilePath)) {
137
+ bufferOrFilePath = await fs.readFile(bufferOrFilePath);
138
+ }
139
+ const img = new lib.image.Transformer(bufferOrFilePath);
140
+ const dimensions = await img.metadata();
141
+ const buffer = await img.resize(width, undefined, 0).jpeg(50);
142
+ return {
143
+ buffer,
144
+ original: {
145
+ width: dimensions.width,
146
+ height: dimensions.height
147
+ }
148
+ };
149
+ }
130
150
  else if ('jimp' in lib && (typeof lib.jimp?.Jimp === 'object' || typeof lib.jimp?.Jimp === 'function')) {
131
151
  // AunuBaileys: jimp v1 mengekspos Jimp sebagai class (function), bukan object
132
152
  const jimp = await lib.jimp.Jimp.read(bufferOrFilePath);
@@ -170,6 +190,10 @@ export const generateProfilePicture = async (mediaUpload, dimensions) => {
170
190
  })
171
191
  .toBuffer();
172
192
  }
193
+ else if ('image' in lib && lib.image?.Transformer) {
194
+ // AunuBaileys: backend @napi-rs/image
195
+ img = new lib.image.Transformer(buffer).resize(w, h, 0).jpeg(50);
196
+ }
173
197
  else if ('jimp' in lib && (typeof lib.jimp?.Jimp === 'object' || typeof lib.jimp?.Jimp === 'function')) {
174
198
  // AunuBaileys: jimp v1 mengekspos Jimp sebagai class (function), bukan object
175
199
  const jimp = await lib.jimp.Jimp.read(buffer);
@@ -659,4 +683,49 @@ const MEDIA_RETRY_STATUS_MAP = {
659
683
  [proto.MediaRetryNotification.ResultType.NOT_FOUND]: 404,
660
684
  [proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418
661
685
  };
686
+ /**
687
+ * AunuBaileys: tulis metadata EXIF stiker (nama pack, penerbit, emoji) ke
688
+ * buffer WebP — format chunk EXIF standar ekosistem WhatsApp (header TIFF
689
+ * mini + JSON metadata). Chunk EXIF lama diganti, bukan ditumpuk. Mengikuti
690
+ * gagasan @itsliaaa/starseal; implementasi orisinal di sini.
691
+ */
692
+ export const addStickerExif = (webpBuffer, metadata = {}) => {
693
+ if (!Buffer.isBuffer(webpBuffer) || webpBuffer.length < 12 ||
694
+ webpBuffer.toString('ascii', 0, 4) !== 'RIFF' || webpBuffer.toString('ascii', 8, 12) !== 'WEBP') {
695
+ throw new Error('addStickerExif: input bukan buffer WebP (RIFF) yang valid');
696
+ }
697
+ const json = {
698
+ 'sticker-pack-id': metadata.packId || 'aunubaileys',
699
+ 'sticker-pack-name': metadata.packname || '',
700
+ 'sticker-pack-publisher': metadata.author || '',
701
+ 'emojis': Array.isArray(metadata.emojis) ? metadata.emojis : ['🖤'],
702
+ ...(metadata.categories ? { 'categories': metadata.categories } : {})
703
+ };
704
+ const jsonBytes = Buffer.from(JSON.stringify(json), 'utf8');
705
+ const header = Buffer.from([0x49, 0x49, 0x2A, 0x00, 0x08, 0x00, 0x00, 0x00, 0x01, 0x00, 0x41, 0x57, 0x07, 0x00, 0x00, 0x00, 0x00, 0x00, 0x16, 0x00, 0x00, 0x00]);
706
+ header.writeUInt32LE(jsonBytes.length, 14);
707
+ const exifPayload = Buffer.concat([header, jsonBytes]);
708
+ // pisahkan chunk-chunk RIFF yang ada (selain EXIF lama)
709
+ const chunks = [];
710
+ let offset = 12;
711
+ while (offset + 8 <= webpBuffer.length) {
712
+ const type = webpBuffer.toString('ascii', offset, offset + 4);
713
+ const size = webpBuffer.readUInt32LE(offset + 4);
714
+ const total = 8 + size + (size % 2);
715
+ if (type !== 'EXIF') {
716
+ chunks.push(webpBuffer.subarray(offset, offset + total));
717
+ }
718
+ offset += total;
719
+ }
720
+ const exifChunk = Buffer.alloc(8 + exifPayload.length + (exifPayload.length % 2));
721
+ exifChunk.write('EXIF', 0, 'ascii');
722
+ exifChunk.writeUInt32LE(exifPayload.length, 4);
723
+ exifPayload.copy(exifChunk, 8);
724
+ const body = Buffer.concat([Buffer.from('WEBP', 'ascii'), ...chunks, exifChunk]);
725
+ const out = Buffer.alloc(8 + body.length);
726
+ out.write('RIFF', 0, 'ascii');
727
+ out.writeUInt32LE(body.length, 4);
728
+ body.copy(out, 8);
729
+ return out;
730
+ };
662
731
  //# sourceMappingURL=messages-media.js.map
@@ -462,7 +462,7 @@ export const generateWAMessageContent = async (message, options) => {
462
462
  var _a, _b;
463
463
  let m = {};
464
464
  // AunuBaileys: rich response — blok kode / tabel / tautan (dari @itsliaaa/baileys)
465
- if ('code' in message || 'links' in message || 'table' in message || 'richResponse' in message) {
465
+ if ('code' in message || 'links' in message || 'table' in message || 'richResponse' in message || 'html' in message || 'richHtml' in message) {
466
466
  m = prepareRichResponseMessage(message);
467
467
  }
468
468
  else if ('text' in message) {
@@ -4,5 +4,9 @@ export declare const tokenizeCode: (code: string, language?: string) => {
4
4
  codeContent: string;
5
5
  }[];
6
6
  export declare const toUnified: (submessages: any[], uuid?: string) => any;
7
+ export declare const buildFoaHtmlSection: (html: string, trustedSources?: string[]) => any;
8
+ export declare const prepareHtmlMessage: (content: { html?: string; trustedSources?: string[] }) => any;
9
+ export declare const buildRichHtmlScreen: (options: any) => any;
10
+ export declare const prepareRichHtmlMessage: (options: any) => any;
7
11
  export declare const prepareRichResponseMessage: (content: any) => any;
8
12
  export declare const wrapToBotForwardedMessage: (richResponseMessage: proto.AIRichResponseMessage) => any;
@@ -99,7 +99,97 @@ export const toUnified = (submessages, uuid) => ({
99
99
  return submessage;
100
100
  })
101
101
  });
102
+ // AunuBaileys: seksi layar HTML (primitive FOAHtmlPrimitiveDemoDONOTUSE milik
103
+ // klien WhatsApp) untuk unified response — payload HTML mentah + daftar
104
+ // sumber tepercaya opsional. EKSPERIMENTAL (keputusan Aunu 11 Nov 2026):
105
+ // apakah klien WhatsApp penerima merender HTML/JS-nya bergantung versi
106
+ // klien dan tidak dijamin library.
107
+ export const buildFoaHtmlSection = (html, trustedSources = []) => ({
108
+ __typename: 'GenAIUnifiedResponseSection',
109
+ view_model: {
110
+ __typename: 'GenAISingleLayoutViewModel',
111
+ primitive: {
112
+ __typename: 'FOAHtmlPrimitiveDemoDONOTUSE',
113
+ trusted_sources: Array.isArray(trustedSources) ? trustedSources : [],
114
+ payload: String(html ?? '').trim()
115
+ }
116
+ }
117
+ });
118
+ // AunuBaileys: pesan HTML mandiri — rich response yang unified-nya hanya
119
+ // berisi seksi HTML (submessages kosong, mengikuti bentuk sumbernya).
120
+ export const prepareHtmlMessage = (content = {}) => prepareRichResponseMessage({
121
+ html: content.html,
122
+ trustedSources: content.trustedSources
123
+ });
124
+ // AunuBaileys: layar HTML interaktif sebagai embedded screen di unified
125
+ // response — primitive GenAIaeacdsnwHtmlPrimitive di dalam tab
126
+ // FOAIDNixelButtonSheets; permukaan mini-app tempat HTML+JS interaktif
127
+ // (game dsb.) tinggal, berbeda dari seksi FOA pada pesan HTML biasa.
128
+ // EKSPERIMENTAL (keputusan Aunu 11 Nov 2026): render bergantung klien.
129
+ export const buildRichHtmlScreen = (options = {}) => {
130
+ const { html = '', trustedSources = [], url = '', title = 'Preview', tabHeader = 'HTML', tabs } = options;
131
+ const tabItems = Array.isArray(tabs) && tabs.length
132
+ ? tabs
133
+ : [{ id: 'tab_0', tab_header: tabHeader, html }];
134
+ const embeddedTabs = tabItems.map((tab, index) => ({
135
+ id: tab.id || `tab_${index}`,
136
+ tab_header: tab.tab_header || tab.title || `HTML ${index + 1}`,
137
+ sections: [{
138
+ __typename: 'GenAIUnifiedResponseSection',
139
+ view_model: {
140
+ __typename: 'GenAISingleLayoutViewModel',
141
+ primitive: {
142
+ __typename: 'GenAIaeacdsnwHtmlPrimitive',
143
+ payload: String(tab.html ?? tab.payload ?? html).trim(),
144
+ url: tab.url || url,
145
+ trusted_sources: tab.trustedSources || trustedSources
146
+ }
147
+ }
148
+ }],
149
+ step_entries: []
150
+ }));
151
+ return {
152
+ title,
153
+ embeddedScreen: {
154
+ title,
155
+ content: [{
156
+ __typename: 'FOAIDNixelButtonSheets',
157
+ tabs: embeddedTabs
158
+ }]
159
+ }
160
+ };
161
+ };
162
+ // AunuBaileys: pesan layar HTML interaktif — unified membawa embedded_screens,
163
+ // submessage-nya satu teks judul bergaya kutipan.
164
+ export const prepareRichHtmlMessage = (options = {}) => {
165
+ const { title, embeddedScreen } = buildRichHtmlScreen(options);
166
+ const unified = {
167
+ response_id: randomUUID(),
168
+ sections: [{
169
+ view_model: {
170
+ primitive: { text: `> ${title}`, __typename: 'GenAIMarkdownTextUXPrimitive' },
171
+ __typename: 'GenAISingleLayoutViewModel'
172
+ }
173
+ }],
174
+ embedded_screens: [embeddedScreen]
175
+ };
176
+ const richResponseMessage = proto.AIRichResponseMessage.create({
177
+ messageType: proto.AIRichResponseMessage.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD,
178
+ submessages: [{ messageType: RichSubMessageType.TEXT, messageText: `> ${title}` }],
179
+ unifiedResponse: { data: TEXT_ENCODER.encode(JSON.stringify(unified)) },
180
+ contextInfo: {
181
+ isForwarded: true,
182
+ forwardingScore: 1,
183
+ forwardedAiBotMessageInfo: { botJid: '867051314767696@bot' },
184
+ forwardOrigin: 4
185
+ }
186
+ });
187
+ return wrapToBotForwardedMessage(richResponseMessage);
188
+ };
102
189
  export const prepareRichResponseMessage = (content) => {
190
+ if (content.richHtml) {
191
+ return prepareRichHtmlMessage(content.richHtml);
192
+ }
103
193
  let { alignment, code, contentText, disclaimerText, footerText, headerText, imageText, inlineImage, inlineVideo, items, language, latex, links, noHeading, posts, products, suggested, richResponse, table, tapLinkUrl, title } = content;
104
194
  let submessages = [];
105
195
  if (Array.isArray(richResponse)) {
@@ -311,7 +401,14 @@ export const prepareRichResponseMessage = (content) => {
311
401
  }
312
402
  }
313
403
  const uuid = randomUUID();
404
+ const extraSections = [];
405
+ if (content.html) {
406
+ extraSections.push(buildFoaHtmlSection(content.html, content.trustedSources));
407
+ }
314
408
  const unified = toUnified(submessages, uuid);
409
+ if (extraSections.length) {
410
+ unified.sections.push(...extraSections);
411
+ }
315
412
  const richResponseMessage = proto.AIRichResponseMessage.create({
316
413
  submessages,
317
414
  messageType: proto.AIRichResponseMessage.AIRichResponseMessageType.AI_RICH_RESPONSE_TYPE_STANDARD,
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aunubaileys",
3
3
  "type": "module",
4
- "version": "1.0.43",
4
+ "version": "1.0.45",
5
5
  "description": "AunuBaileys \u2014 a WhatsApp Web API for bots that never sleep. Full upstream power with the beloved in-memory store restored and pairing that just works. Forged by AunuCore.",
6
6
  "keywords": [
7
7
  "whatsapp",
@@ -83,7 +83,8 @@
83
83
  "audio-decode": "^2.1.3",
84
84
  "jimp": "^1.6.0",
85
85
  "link-preview-js": "^3.0.0",
86
- "sharp": "*"
86
+ "sharp": "*",
87
+ "@napi-rs/image": "^1.12.0"
87
88
  },
88
89
  "peerDependenciesMeta": {
89
90
  "audio-decode": {