aunubaileys 1.0.50 β†’ 1.0.52

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,14 @@ 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.52 / Baru di 1.0.52 β€” EXPERIMENTAL
68
+
69
+ - πŸ‘ **`aunuCard` gains like/dislike buttons.** The new `likeDislike: true` option appends two quick-reply buttons under the card β€” **πŸ‘ Suka** (id `aunu_like`) and **πŸ‘Ž Tidak Suka** (id `aunu_dislike`) β€” after any custom buttons. The ids are overridable via `likeId`/`dislikeId`; taps return to the bot as button replies carrying those ids, so votes can be counted. Without the option, card behaviour is exactly as in 1.0.50.
70
+
71
+ ### 🩹 What's new in 1.0.51 / Baru di 1.0.51
72
+
73
+ - πŸ” **Three audited gaps, closed.** (1) **Key-store commit failures are no longer swallowed** (`auth-utils.js`): transaction commits still retry as before, but when every retry fails the error is now **thrown** β€” previously the mutations were silently discarded while the in-memory ratchet had already advanced, so every later message failed against the stale on-disk record. (2) **Hosted device :99 guard** (`signal.js`): `extractDeviceJids` now skips device 99 β€” hosted / Cloud-API companion devices are not E2E targets and libsignal rejects them when the device list is built. (3) **PN β†’ LID session migration** (`libsignal.js`, `Socket/index.js`): the new repository method `migrateSession(fromJid, toJid)` moves the raw session record onto the LID address when the target has none (never overwrites). It fires automatically and best-effort when live traffic teaches a new PN↔LID pair, and for the account's own session when the connection opens with both `me.id` and `me.lid` known. The 1.0.14–1.0.48 decrypt heals remain the safety net.
74
+
67
75
  ### πŸ§ͺ What's new in 1.0.50 / Baru di 1.0.50 β€” EXPERIMENTAL
68
76
 
69
77
  - πŸƒ **Six more experimental features.** (1) `sock.aunuCard(jid, options)` β€” an interactive photo card: the header photo **does not open as a normal photo** when tapped, a title/name and text sit fused below it in the same bubble, and divider-separated buttons (`reply`/`url`/`copy`/`call`) sit underneath; the photo is uploaded from a Buffer/URL and embedded as the header, or supplied prebuilt as `imageMessage`. (2) `sock.aunuFakeQuote(jid, content, quote)` β€” send a message carrying a fabricated quote, as if replying to someone. (3) `sock.aunuTypewriter(jid, text, options)` β€” a typing effect: the message is sent in growing pieces by editing it on an interval until the full text stands. (4) `sock.aunuReactSequence(jid, key, emojis, options)` β€” cycle reactions on one message. (5) `sock.aunuPostStatus(text, options)` β€” a styled text status (background colour + font) in one call. (6) `sock.aunuPersonalCard(jid, targetJid, options)` β€” the one-person form of 1.0.49's personalised tag-all: an own-profile-picture + name card for a single target, into a chat or as a DM. All experimental, like the features of 1.0.47/1.0.49.
@@ -74,6 +74,27 @@ export function makeLibSignalRepository(auth) {
74
74
  const addr = jidToSignalProtocolAddress(jid).toString();
75
75
  await auth.keys.set({ session: { [addr]: null } });
76
76
  },
77
+ async migrateSession(fromJid, toJid) {
78
+ // AunuBaileys: move a stored session record to another address
79
+ // form of the same identity (PN -> LID). The raw record is
80
+ // moved untouched; an existing target record is never
81
+ // overwritten. Returns true when a record was moved.
82
+ const fromAddr = jidToSignalProtocolAddress(fromJid).toString();
83
+ const toAddr = jidToSignalProtocolAddress(toJid).toString();
84
+ if (fromAddr === toAddr) {
85
+ return false;
86
+ }
87
+ const { [fromAddr]: record } = await auth.keys.get('session', [fromAddr]);
88
+ if (!record) {
89
+ return false;
90
+ }
91
+ const { [toAddr]: existing } = await auth.keys.get('session', [toAddr]);
92
+ if (existing) {
93
+ return false;
94
+ }
95
+ await auth.keys.set({ session: { [toAddr]: record, [fromAddr]: null } });
96
+ return true;
97
+ },
77
98
  async injectE2ESession({ jid, session }) {
78
99
  const cipher = new libsignal.SessionBuilder(storage, jidToSignalProtocolAddress(jid));
79
100
  await cipher.initOutgoing(session);
@@ -61,6 +61,10 @@ export interface AunuCardOptions {
61
61
  /** Prebuilt imageMessage object, used as-is when provided. */
62
62
  imageMessage?: Record<string, unknown>;
63
63
  buttons?: AunuCardButton[];
64
+ /** Append πŸ‘ Suka / πŸ‘Ž Tidak Suka quick-reply buttons under the card. Taps return as button replies with the like/dislike ids. */
65
+ likeDislike?: boolean;
66
+ likeId?: string;
67
+ dislikeId?: string;
64
68
  messageOptions?: MiscMessageGenerationOptions;
65
69
  }
66
70
  export interface AunuFakeQuoteOptions {
@@ -187,13 +187,18 @@ export const attachAunuExperimental = (sock) => {
187
187
  const prepared = await prepareWAMessageMedia({ image: options.image }, { upload: sock.waUploadToServer });
188
188
  imageMessage = prepared.imageMessage;
189
189
  }
190
+ const cardButtons = (options.buttons || []).map(mapCardButton).filter(Boolean);
191
+ if (options.likeDislike) {
192
+ cardButtons.push({ name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: 'πŸ‘ Suka', id: options.likeId || 'aunu_like' }) });
193
+ cardButtons.push({ name: 'quick_reply', buttonParamsJson: JSON.stringify({ display_text: 'πŸ‘Ž Tidak Suka', id: options.dislikeId || 'aunu_dislike' }) });
194
+ }
190
195
  const interactive = {
191
196
  header: (options.title || options.subtitle || imageMessage)
192
197
  ? { title: options.title || '', subtitle: options.subtitle, hasMediaAttachment: !!imageMessage, imageMessage }
193
198
  : undefined,
194
199
  body: { text: options.text || '' },
195
200
  footer: options.footer ? { text: options.footer } : undefined,
196
- buttons: (options.buttons || []).map(mapCardButton).filter(Boolean)
201
+ buttons: cardButtons
197
202
  };
198
203
  return sock.sendMessage(jid, { interactive }, options.messageOptions || {});
199
204
  };
@@ -27,6 +27,45 @@ const makeWASocket = (config) => {
27
27
  catch {
28
28
  // identity learning must never break message handling
29
29
  }
30
+ // AunuBaileys: when live traffic teaches us a PN <-> LID pair, move
31
+ // any stored PN session onto the LID address (best-effort; the
32
+ // decrypt heals remain the fallback).
33
+ try {
34
+ for (const msg of upsert?.messages ?? []) {
35
+ const key = msg?.key ?? {};
36
+ const pairs = [];
37
+ if (key.senderPn && key.senderLid) {
38
+ pairs.push([key.senderPn, key.senderLid]);
39
+ }
40
+ if (key.participantPn && key.participantLid) {
41
+ pairs.push([key.participantPn, key.participantLid]);
42
+ }
43
+ if (key.remoteJid && key.senderLid && !key.fromMe) {
44
+ pairs.push([key.remoteJid, key.senderLid]);
45
+ }
46
+ for (const [pn, lid] of pairs) {
47
+ sock.signalRepository?.migrateSession?.(pn, lid)?.catch?.(() => { });
48
+ }
49
+ }
50
+ }
51
+ catch {
52
+ // session migration must never break message handling
53
+ }
54
+ });
55
+ sock.ev.on('connection.update', (update) => {
56
+ // AunuBaileys: migrate the account's OWN session to its LID
57
+ // address once the connection opens with both identities known.
58
+ try {
59
+ if (update.connection === 'open') {
60
+ const me = sock.authState?.creds?.me;
61
+ if (me?.id && me?.lid) {
62
+ sock.signalRepository?.migrateSession?.(me.id, me.lid)?.catch?.(() => { });
63
+ }
64
+ }
65
+ }
66
+ catch {
67
+ // session migration must never break connection handling
68
+ }
30
69
  });
31
70
  return sock;
32
71
  };
@@ -54,6 +54,7 @@ export type SignalRepository = {
54
54
  injectE2ESession(opts: E2ESessionOpts): Promise<void>;
55
55
  /** AunuBaileys: delete the stored E2E session for a JID (used by the Bad MAC self-heal). */
56
56
  deleteSession(jid: string): Promise<void>;
57
+ migrateSession(fromJid: string, toJid: string): Promise<boolean>;
57
58
  /** AunuBaileys: delete the stored sender-key record for a group author (stale group session heal). */
58
59
  deleteSenderKey(group: string, authorJid: string): Promise<void>;
59
60
  jidToSignalProtocolAddress(jid: string): string;
@@ -143,19 +143,32 @@ export const addTransactionCapability = (state, logger, { maxCommitRetries, dela
143
143
  // retry mechanism to ensure we've some recovery
144
144
  // in case a transaction fails in the first attempt
145
145
  let tries = maxCommitRetries;
146
+ let committed = false;
147
+ let lastCommitError;
146
148
  while (tries) {
147
149
  tries -= 1;
148
150
  //eslint-disable-next-line max-depth
149
151
  try {
150
152
  await state.set(mutations);
151
153
  logger.trace({ dbQueriesInTransaction }, 'committed transaction');
154
+ committed = true;
152
155
  break;
153
156
  }
154
157
  catch (error) {
158
+ lastCommitError = error;
155
159
  logger.warn(`failed to commit ${Object.keys(mutations).length} mutations, tries left=${tries}`);
156
160
  await delay(delayBetweenTriesMs);
157
161
  }
158
162
  }
163
+ if (!committed) {
164
+ // AunuBaileys: a commit that fails through every retry
165
+ // must NOT be swallowed β€” the in-memory ratchet has
166
+ // advanced while the persisted state has not, and
167
+ // every later message would fail against the stale
168
+ // record. Throw so the caller learns its state was
169
+ // never persisted.
170
+ throw lastCommitError ?? new Error('failed to commit transaction mutations');
171
+ }
159
172
  }
160
173
  else {
161
174
  logger.trace('no mutations in transaction');
@@ -135,6 +135,7 @@ export const extractDeviceJids = (result, myJid, excludeZeroDevices) => {
135
135
  for (const { id: device, keyIndex } of deviceList) {
136
136
  if ((!excludeZeroDevices || device !== 0) && // if zero devices are not-excluded, or device is non zero
137
137
  (myUser !== user || myDevice !== device) && // either different user or if me user, not this device
138
+ device !== 99 && // AunuBaileys: hosted / Cloud-API companion devices (:99) are not E2E targets β€” libsignal rejects them ("Unexpected non-hosted device JID with device 99"); skip them when building the device list
138
139
  (device === 0 || !!keyIndex) // ensure that "key-index" is specified for "non-zero" devices, produces a bad req otherwise
139
140
  ) {
140
141
  extracted.push({ user, device });
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "aunubaileys",
3
3
  "type": "module",
4
- "version": "1.0.50",
4
+ "version": "1.0.52",
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",