aunubaileys 1.0.44 β†’ 1.0.46

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.46 / Baru di 1.0.46
68
+
69
+ - 🩹 **The "Over 2000 messages into the future" flood now heals itself.** Session-layer decrypt failures β€” libsignal's aggregate "No matching sessions found for message" / "No session record" / "No sessions available", which includes the unfillable counter gap left by regressed session state (e.g. sessions restored from a backup while the sender kept sending; observed live in the Tenka deployment on 11 Nov 2026, on the same LID address as the old B-01 finding) β€” are now recognised from `messageStubParameters` and trigger an **immediate E2E session reset** instead of waiting for 3 failures: for DMs and groups alike (groups also drop the author's sender key), including the session record under its alternate PN↔LID address form, with the retry receipt carrying fresh keys. The old self-heal missed this flood because it waited 3 strikes, in groups only touched the sender key while the broken layer is the E2E session, and deletion could miss the aliased record. Other error classes (ordinary Bad MAC) keep the exact 1.0.14/1.0.17 3-strike behaviour. New exported classifier: `isUnrecoverableSessionError(text)`; the `messages.decrypt-failure` event gains an `errorClass` field (`'session'`/`'other'`) and `healed` now reflects a reset that actually happened on that message.
70
+
71
+ ### πŸ†• What's new in 1.0.45 / Baru di 1.0.45
72
+
73
+ - πŸ“ž **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`.
74
+ - 🌐 **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.
75
+ - 🏷️ **`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.
76
+
67
77
  ### πŸ†• What's new in 1.0.44 / Baru di 1.0.44
68
78
 
69
79
  - πŸ–ΌοΈ **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.
@@ -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, isUnrecoverableSessionError, 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',
@@ -736,17 +758,47 @@ export const makeMessagesRecvSocket = (config) => {
736
758
  const addr = signalRepository.jidToSignalProtocolAddress(author);
737
759
  const failKey = isGroupMsg ? `sk:${msg.key.remoteJid}:${addr}` : addr;
738
760
  const fails = (decryptFailCounts.get(failKey) || 0) + 1;
739
- if (fails >= DECRYPT_FAIL_SESSION_RESET_LIMIT) {
761
+ // AunuBaileys (1.0.46): kegagalan lapis sesi β€” record
762
+ // sesi tersimpan tidak dapat mendekripsi pesan ini
763
+ // sama sekali (termasuk jurang "Over 2000 messages
764
+ // into the future" akibat state sesi yang mundur).
765
+ // Jurang itu tidak bisa diisi ulang, jadi menunggu
766
+ // 3x hanya membanjiri log; dan di grup, menghapus
767
+ // sender key saja tidak menyentuh sesi E2E yang
768
+ // rusak. Kelas ini direset SEKETIKA.
769
+ const sessionLayerFailure = isUnrecoverableSessionError(msg?.messageStubParameters?.[0]);
770
+ let healed = false;
771
+ const resetE2ESession = async () => {
772
+ await signalRepository.deleteSession(author);
773
+ // sesi yang sama bisa tersimpan di bawah bentuk
774
+ // alamat alternatifnya (PN <-> LID)
775
+ const alt = sock.aunuLid?.getAltJid?.(author);
776
+ if (alt && alt !== author) {
777
+ await signalRepository.deleteSession(alt);
778
+ }
779
+ };
780
+ if (sessionLayerFailure) {
781
+ await resetE2ESession();
782
+ if (isGroupMsg) {
783
+ await signalRepository.deleteSenderKey(msg.key.remoteJid, author);
784
+ }
785
+ decryptFailCounts.delete(failKey);
786
+ forceIncludeKeys = true;
787
+ healed = true;
788
+ logger.warn({ author, addr, group: isGroupMsg ? msg.key.remoteJid : undefined }, 'aunu: reset E2E session after an unrecoverable session-layer decrypt failure (stored session cannot decrypt, e.g. regressed state); retry receipt carries fresh keys');
789
+ }
790
+ else if (fails >= DECRYPT_FAIL_SESSION_RESET_LIMIT) {
740
791
  if (isGroupMsg) {
741
792
  await signalRepository.deleteSenderKey(msg.key.remoteJid, author);
742
793
  logger.warn({ author, group: msg.key.remoteJid }, 'aunu: deleted stale sender-key record after repeated group decrypt failures; retry request asks the author to resend with a fresh distribution message');
743
794
  }
744
795
  else {
745
- await signalRepository.deleteSession(author);
796
+ await resetE2ESession();
746
797
  logger.warn({ author, addr }, 'aunu: deleted stale E2E session after repeated decrypt failures (Bad MAC self-heal); retry receipt carries fresh keys');
747
798
  }
748
799
  decryptFailCounts.delete(failKey);
749
800
  forceIncludeKeys = true;
801
+ healed = true;
750
802
  }
751
803
  else {
752
804
  decryptFailCounts.set(failKey, fails);
@@ -760,7 +812,8 @@ export const makeMessagesRecvSocket = (config) => {
760
812
  address: addr,
761
813
  isGroup: isGroupMsg,
762
814
  failures: fails,
763
- healed: fails >= DECRYPT_FAIL_SESSION_RESET_LIMIT,
815
+ healed,
816
+ errorClass: sessionLayerFailure ? 'session' : 'other',
764
817
  messageId: node.attrs.id,
765
818
  timestamp: Date.now()
766
819
  });
@@ -1201,6 +1254,8 @@ export const makeMessagesRecvSocket = (config) => {
1201
1254
  sendMessageAck,
1202
1255
  sendRetryRequest,
1203
1256
  rejectCall,
1257
+ acceptCall,
1258
+ terminateCall,
1204
1259
  fetchMessageHistory,
1205
1260
  requestPlaceholderResend
1206
1261
  };
@@ -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
+ });
@@ -2,6 +2,7 @@ import type { SignalRepository, WAMessage } from '../Types/index.js';
2
2
  import { type BinaryNode } from '../WABinary/index.js';
3
3
  import type { ILogger } from './logger.js';
4
4
  export declare const NO_MESSAGE_FOUND_ERROR_TEXT = "Message absent from node";
5
+ export declare const isUnrecoverableSessionError: (text: unknown) => boolean;
5
6
  export declare const MISSING_KEYS_ERROR_TEXT = "Key used already or never filled";
6
7
  export declare const NACK_REASONS: {
7
8
  ParsingError: number;
@@ -216,4 +216,15 @@ export const decryptMessageNode = (stanza, meId, meLid, repository, logger) => {
216
216
  }
217
217
  };
218
218
  };
219
+ /**
220
+ * AunuBaileys: true when a decrypt failure text is libsignal's session-layer
221
+ * aggregate β€” the stored session record(s) could not decrypt the message at
222
+ * all ("No matching sessions found for message", "No session record",
223
+ * "No sessions available"). This class includes the unfillable counter gap
224
+ * ("Over 2000 messages into the future!") thrown when the stored session
225
+ * state has regressed behind the sender's ratchet, e.g. after a backup
226
+ * restore; such a session can never decrypt again and must be reset.
227
+ */
228
+ export const isUnrecoverableSessionError = (text) => typeof text === 'string' &&
229
+ /no matching sessions found|no session record|no sessions available/i.test(text);
219
230
  //# sourceMappingURL=decode-wa-message.js.map
@@ -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>>;
@@ -683,4 +683,49 @@ const MEDIA_RETRY_STATUS_MAP = {
683
683
  [proto.MediaRetryNotification.ResultType.NOT_FOUND]: 404,
684
684
  [proto.MediaRetryNotification.ResultType.GENERAL_ERROR]: 418
685
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
+ };
686
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.44",
4
+ "version": "1.0.46",
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",