@aunucore/baileys 1.0.85 → 1.0.86

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.
@@ -5,6 +5,10 @@ import { generateWAMessageFromContent, prepareWAMessageMedia } from '../Utils/me
5
5
  import { proto } from '../../WAProto/index.js';
6
6
  import { randomBytes } from 'crypto';
7
7
  const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
8
+ // AunuBaileys (1.0.86, HB-112 short series): Node clamps any
9
+ // setTimeout delay above 2^31-1 ms (~24.8 days) down to 1 ms, so
10
+ // a longer delay must be armed in slices of at most this size.
11
+ const MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
8
12
  /**
9
13
  * AunuBaileys EXPERIMENTAL socket features — these exist only in
10
14
  * AunuBaileys and may change or disappear in any release:
@@ -125,7 +129,8 @@ export const attachAunuExperimental = (sock) => {
125
129
  sock.aunuSchedule = (jid, content, when, options = {}) => {
126
130
  const at = when instanceof Date ? when.getTime() : Date.now() + Math.max(0, Number(when) || 0);
127
131
  const id = `aunu-sched-${++seq}`;
128
- const timer = setTimeout(() => {
132
+ const entry = { jid, at, timer: undefined };
133
+ const fire = () => {
129
134
  scheduled.delete(id);
130
135
  // AunuBaileys (1.0.72): failures used to be swallowed by
131
136
  // an empty catch — a scheduled message that failed (socket
@@ -133,9 +138,23 @@ export const attachAunuExperimental = (sock) => {
133
138
  Promise.resolve(sock.sendMessage(jid, content, options)).catch((err) => {
134
139
  logger?.warn?.({ err, jid, id }, 'aunu: scheduled message failed to send');
135
140
  });
136
- }, Math.max(0, at - Date.now()));
137
- timer.unref?.();
138
- scheduled.set(id, { jid, at, timer });
141
+ };
142
+ // AunuBaileys (1.0.86, HB-112 short series): arm in slices.
143
+ // A single setTimeout for a send scheduled more than ~24.8
144
+ // days out used to be clamped by Node to 1 ms, so the
145
+ // message fired IMMEDIATELY and the entry vanished as if
146
+ // sent on schedule. entry.timer always holds the CURRENT
147
+ // slice's handle, so cancel and socket-end cleanup (which
148
+ // read entry.timer) keep working unchanged.
149
+ const arm = () => {
150
+ const remaining = at - Date.now();
151
+ entry.timer = remaining > MAX_TIMER_DELAY_MS
152
+ ? setTimeout(arm, MAX_TIMER_DELAY_MS)
153
+ : setTimeout(fire, Math.max(0, remaining));
154
+ entry.timer.unref?.();
155
+ };
156
+ arm();
157
+ scheduled.set(id, entry);
139
158
  return { id, at, cancel: () => sock.aunuCancelScheduled(id) };
140
159
  };
141
160
  sock.aunuCancelScheduled = (id) => {
@@ -152,14 +171,29 @@ export const attachAunuExperimental = (sock) => {
152
171
  const sent = await sock.sendMessage(jid, content, options);
153
172
  const key = sent?.key;
154
173
  if (key?.id) {
155
- const timer = setTimeout(() => {
174
+ // AunuBaileys (1.0.86, HB-112 short series): same
175
+ // slice-arming as aunuSchedule — a self-destruct set
176
+ // more than ~24.8 days out used to be clamped by Node
177
+ // to 1 ms and deleted the message almost immediately
178
+ // after it was sent. The map always holds the current
179
+ // slice's handle, so cancel and socket-end cleanup are
180
+ // unchanged.
181
+ const fireAt = Date.now() + Math.max(0.05, Number(seconds) || 0) * 1000;
182
+ const fire = () => {
156
183
  destructs.delete(key.id);
157
184
  Promise.resolve(sock.sendMessage(jid, { delete: key })).catch((err) => {
158
185
  logger?.warn?.({ err, jid, id: key.id }, 'aunu: self-destruct delete failed');
159
186
  });
160
- }, Math.max(0.05, Number(seconds) || 0) * 1000);
161
- timer.unref?.();
162
- destructs.set(key.id, timer);
187
+ };
188
+ const arm = () => {
189
+ const remaining = fireAt - Date.now();
190
+ const timer = remaining > MAX_TIMER_DELAY_MS
191
+ ? setTimeout(arm, MAX_TIMER_DELAY_MS)
192
+ : setTimeout(fire, Math.max(0, remaining));
193
+ timer.unref?.();
194
+ destructs.set(key.id, timer);
195
+ };
196
+ arm();
163
197
  }
164
198
  return sent;
165
199
  };
@@ -906,16 +906,33 @@ export const makeChatsSocket = (config) => {
906
906
  }
907
907
  }
908
908
  const historyMsg = getHistoryMsg(msg.message);
909
- const shouldProcessHistoryMsg = historyMsg
909
+ // AunuBaileys (1.0.86, HB-113 short series): the
910
+ // shouldSyncHistoryMessage preference governs PASSIVE sync
911
+ // only. An ON_DEMAND payload is the answer to an explicit
912
+ // host request (fetchMessageHistory /
913
+ // requestPlaceholderResend); the single shared gate used
914
+ // to discard it silently whenever the preference was off
915
+ // — including the syncFullHistory:false default — so the
916
+ // host's request vanished without an error and without
917
+ // the messaging-history.set event. The initial-sync state
918
+ // machine below keeps the preference-based predicate
919
+ // (syncPreferenceAllows): on-demand responses must not
920
+ // drive it. For every non-ON_DEMAND input, and for
921
+ // ON_DEMAND with the preference on, syncPreferenceAllows
922
+ // equals the old gate exactly.
923
+ const syncPreferenceAllows = historyMsg
910
924
  ? shouldSyncHistoryMessage(historyMsg) && PROCESSABLE_HISTORY_TYPES.includes(historyMsg.syncType)
911
925
  : false;
926
+ const shouldProcessHistoryMsg = historyMsg?.syncType === proto.HistorySync.HistorySyncType.ON_DEMAND
927
+ ? true
928
+ : syncPreferenceAllows;
912
929
  // State machine: decide on sync and flush
913
930
  if (historyMsg && syncState === SyncState.AwaitingInitialSync) {
914
931
  if (awaitingSyncTimeout) {
915
932
  clearTimeout(awaitingSyncTimeout);
916
933
  awaitingSyncTimeout = undefined;
917
934
  }
918
- if (shouldProcessHistoryMsg) {
935
+ if (syncPreferenceAllows) {
919
936
  syncState = SyncState.Syncing;
920
937
  logger.info('Transitioned to Syncing state');
921
938
  // Let doAppStateSync handle the final flush after it's done
@@ -1986,9 +1986,22 @@ export const makeMessagesRecvSocket = (config) => {
1986
1986
  msg.messageStubType = call.isVideo ? WAMessageStubType.CALL_MISSED_VIDEO : WAMessageStubType.CALL_MISSED_VOICE;
1987
1987
  }
1988
1988
  }
1989
- else {
1989
+ else if (call.id) {
1990
1990
  msg.message = { call: { callKey: Buffer.from(call.id) } };
1991
1991
  }
1992
+ else {
1993
+ // AunuBaileys (1.0.86, HB-216): a group call offer
1994
+ // without a call-id (an optional stanza attr the
1995
+ // decoder does not validate) made Buffer.from throw
1996
+ // a raw TypeError inside this listener; the throw
1997
+ // unwound the whole 'call' emit, so host listeners
1998
+ // registered after this one never saw the offer at
1999
+ // all. There is no call key to reference and the
2000
+ // message key would carry no id either — skip the
2001
+ // notification message; the 'call' event itself
2002
+ // still reaches every listener.
2003
+ return;
2004
+ }
1992
2005
  const protoMsg = proto.WebMessageInfo.fromObject(msg);
1993
2006
  // AunuBaileys: this floated unawaited and uncaught — a
1994
2007
  // rejection here was an unhandledRejection that could kill
@@ -279,7 +279,17 @@ export const makeMessagesSocket = (config) => {
279
279
  try {
280
280
  const { recipientJid: jid, ...patchedMessage } = patchedMessageWithJid;
281
281
  if (!jid) {
282
- return {};
282
+ // AunuBaileys (1.0.86, HB-136): this used to return
283
+ // {} — truthy, so it passed the filter below and
284
+ // counted as a SUCCESSFUL node. A patch whose every
285
+ // entry lacked a recipientJid therefore "sent" a
286
+ // stanza with an empty <participants> list and
287
+ // relayMessage resolved, instead of failing with
288
+ // 'All encryptions failed' (the encoder silently
289
+ // drops tag-less children, so the stanza really
290
+ // went out). Upstream returns null here — the same
291
+ // value the per-recipient catch below already uses.
292
+ return null;
283
293
  }
284
294
  const bytes = encodeWAMessage(patchedMessage);
285
295
  let encResult;
@@ -22,7 +22,7 @@ export declare const makeNewsletterSocket: (sock: GroupsSocket) => {
22
22
  subscribeNewsletterUpdates: (jid: string) => Promise<{
23
23
  duration: string;
24
24
  } | null>;
25
- newsletterAdminCount: (jid: string) => Promise<number>;
25
+ newsletterAdminCount: (jid: string) => Promise<number | undefined>;
26
26
  newsletterChangeOwner: (jid: string, newOwnerJid: string) => Promise<void>;
27
27
  newsletterDemote: (jid: string, userJid: string) => Promise<void>;
28
28
  newsletterDelete: (jid: string) => Promise<void>;
@@ -181,7 +181,13 @@ export const makeNewsletterSocket = (sock) => {
181
181
  },
182
182
  newsletterAdminCount: async (jid) => {
183
183
  const response = await executeWMexQuery({ newsletter_id: jid }, QueryIds.ADMIN_COUNT, XWAPaths.xwa2_newsletter_admin_count);
184
- return response.admin_count;
184
+ // AunuBaileys (1.0.86, HB-111 short series): the guard in
185
+ // executeWMexQuery only rejects an UNDEFINED response —
186
+ // a GraphQL null for the admin path passes through
187
+ // verbatim, and this bare deref threw a raw TypeError at
188
+ // the host. A channel without admin info simply has no
189
+ // count to report.
190
+ return response?.admin_count;
185
191
  },
186
192
  newsletterChangeOwner: async (jid, newOwnerJid) => {
187
193
  await executeWMexQuery({ newsletter_id: jid, user_id: newOwnerJid }, QueryIds.CHANGE_OWNER, XWAPaths.xwa2_newsletter_change_owner);
@@ -306,6 +306,13 @@ const makeInMemoryStore = ({ logger: _logger, chatKey, labelAssociationKey }) =>
306
306
  });
307
307
  ev.on('messages.reaction', (reactions) => {
308
308
  for (const { key, reaction } of reactions) {
309
+ // AunuBaileys (1.0.86, HB-187 addendum): a key-less
310
+ // reaction (host-emitted, or delivered unbuffered)
311
+ // dereferenced key.remoteJid bare here and threw
312
+ // inside the listener. It has no target; skip it.
313
+ if (!key?.remoteJid) {
314
+ continue;
315
+ }
309
316
  const obj = messages[key.remoteJid];
310
317
  const msg = obj === null || obj === void 0 ? void 0 : obj.get(key.id);
311
318
  if (msg) {
@@ -100,7 +100,7 @@ export const makeEventBuffer = (logger) => {
100
100
  delete data.chatUpdates[update.id];
101
101
  }
102
102
  }
103
- const consolidatedData = consolidateEvents(data);
103
+ const consolidatedMaps = consolidateEvents(data);
104
104
  // AunuBaileys (1.0.81, HB-166 + HB-224): swap the fresh
105
105
  // buffer in BEFORE emitting, and guard the emit. The old
106
106
  // order assigned `data = newData` after the emit, so
@@ -116,12 +116,14 @@ export const makeEventBuffer = (logger) => {
116
116
  // paths (processNodeWithBuffer), where a throw became an
117
117
  // uncaught exception / unhandled rejection.
118
118
  data = newData;
119
- if (Object.keys(consolidatedData).length) {
120
- try {
121
- ev.emit('event', consolidatedData);
122
- }
123
- catch (err) {
124
- logger.error({ err }, 'error emitting flushed events');
119
+ for (const consolidatedData of consolidatedMaps) {
120
+ if (Object.keys(consolidatedData).length) {
121
+ try {
122
+ ev.emit('event', consolidatedData);
123
+ }
124
+ catch (err) {
125
+ logger.error({ err }, 'error emitting flushed events');
126
+ }
125
127
  }
126
128
  }
127
129
  logger.trace({ conditionalChatUpdatesLeft }, 'released buffered events');
@@ -433,6 +435,17 @@ eventData, logger) {
433
435
  case 'messages.reaction':
434
436
  const reactions = eventData;
435
437
  for (const { key, reaction } of reactions) {
438
+ // AunuBaileys (1.0.86, HB-187): a reaction whose
439
+ // target key is missing (an optional proto field)
440
+ // used to throw a raw TypeError inside
441
+ // stringifyMessageKey — inside upsertMessage, after
442
+ // the delivery receipt had already been sent, so
443
+ // the stanza ALSO earned a NACK 500 and the
444
+ // reaction was lost. A key-less reaction has no
445
+ // target to attach to; skip it.
446
+ if (!key) {
447
+ continue;
448
+ }
436
449
  const keyStr = stringifyMessageKey(key);
437
450
  const existing = data.messageUpserts[keyStr];
438
451
  if (existing) {
@@ -462,10 +475,15 @@ eventData, logger) {
462
475
  const groupUpdates = eventData;
463
476
  for (const update of groupUpdates) {
464
477
  const id = update.id;
465
- const groupUpdate = data.groupUpdates[id] || {};
466
- if (!data.groupUpdates[id]) {
467
- data.groupUpdates[id] = Object.assign(groupUpdate, update);
468
- }
478
+ // AunuBaileys (1.0.86, HB-176): a second update for
479
+ // the same group inside one buffer window used to
480
+ // be discarded whole — the merge only ran when no
481
+ // update was stored yet, so a subject change
482
+ // followed by a description change delivered only
483
+ // the subject and the store kept stale metadata.
484
+ // Merge into the stored update, like chats.update
485
+ // and contacts.update already do.
486
+ data.groupUpdates[id] = Object.assign(data.groupUpdates[id] || {}, update);
469
487
  }
470
488
  break;
471
489
  default:
@@ -507,6 +525,11 @@ eventData, logger) {
507
525
  }
508
526
  function consolidateEvents(data) {
509
527
  const map = {};
528
+ // AunuBaileys (1.0.86, HB-159): extra maps emitted right after
529
+ // the main one (see the messages.upsert grouping below). The
530
+ // common single-type case leaves this empty and behaviour is
531
+ // exactly what it was before.
532
+ const extraMaps = [];
510
533
  if (!data.historySets.empty) {
511
534
  map['messaging-history.set'] = {
512
535
  chats: Object.values(data.historySets.chats),
@@ -532,11 +555,40 @@ function consolidateEvents(data) {
532
555
  }
533
556
  const messageUpsertList = Object.values(data.messageUpserts);
534
557
  if (messageUpsertList.length) {
535
- const type = messageUpsertList[0].type;
558
+ // AunuBaileys (1.0.86, HB-159): the whole batch used to
559
+ // take the FIRST message's type. A live 'notify' message
560
+ // buffered behind offline 'append' messages (the initial
561
+ // offline drain, or a history resync racing live traffic)
562
+ // was delivered as 'append' — the store never created its
563
+ // chat, the unread count vanished, and hosts that only act
564
+ // on 'notify' never saw a live message. Group by type
565
+ // instead: same-type batches keep the single consolidated
566
+ // event; a mixed batch emits one messages.upsert per type
567
+ // (the extra maps flush right after the main one), every
568
+ // message keeping its own type, in arrival order.
569
+ const messagesByType = new Map();
570
+ for (const entry of messageUpsertList) {
571
+ const list = messagesByType.get(entry.type);
572
+ if (list) {
573
+ list.push(entry.message);
574
+ }
575
+ else {
576
+ messagesByType.set(entry.type, [entry.message]);
577
+ }
578
+ }
579
+ const [firstType, ...otherTypes] = [...messagesByType.keys()];
536
580
  map['messages.upsert'] = {
537
- messages: messageUpsertList.map(m => m.message),
538
- type
581
+ messages: messagesByType.get(firstType),
582
+ type: firstType
539
583
  };
584
+ for (const type of otherTypes) {
585
+ extraMaps.push({
586
+ 'messages.upsert': {
587
+ messages: messagesByType.get(type),
588
+ type
589
+ }
590
+ });
591
+ }
540
592
  }
541
593
  const messageUpdateList = Object.values(data.messageUpdates);
542
594
  if (messageUpdateList.length) {
@@ -566,7 +618,7 @@ function consolidateEvents(data) {
566
618
  if (groupUpdateList.length) {
567
619
  map['groups.update'] = groupUpdateList;
568
620
  }
569
- return map;
621
+ return [map, ...extraMaps];
570
622
  }
571
623
  function concatChats(a, b) {
572
624
  if (b.unreadCount === null && // neutralize unread counter
@@ -39,6 +39,17 @@ export const processHistoryMessage = (item) => {
39
39
  delete chat.messages;
40
40
  for (const item of msgs) {
41
41
  const message = item.message;
42
+ // AunuBaileys (1.0.86, HB-181): HistorySyncMsg.
43
+ // message is an OPTIONAL proto field — a wrapper
44
+ // carrying only msgOrderId decodes with message
45
+ // === null, and the bare derefs below (starting
46
+ // at message.key) threw a TypeError that cost
47
+ // the WHOLE history chunk (every conversation
48
+ // in the blob was NACKed and lost), not just the
49
+ // empty entry. Skip the entry.
50
+ if (!message) {
51
+ continue;
52
+ }
42
53
  messages.push(message);
43
54
  if (!chat.messages?.length) {
44
55
  // keep only the most recent message in the chat array
@@ -1334,6 +1334,15 @@ export const generateWAMessageFromContent = (jid, message, options) => {
1334
1334
  }
1335
1335
  const innerMessage = normalizeMessageContent(message);
1336
1336
  const key = getContentType(innerMessage);
1337
+ // AunuBaileys (1.0.86, HB-122): on the raw path the content
1338
+ // value can be a bare string ('conversation') or absent
1339
+ // entirely (key undefined). Quote and ephemeral contextInfo
1340
+ // can only be attached to an object-shaped value — the `in`
1341
+ // check and the property assignment below both threw raw
1342
+ // TypeErrors otherwise (Cannot use 'in' operator... /
1343
+ // Cannot create property 'contextInfo' on string...).
1344
+ const contextTarget = key ? innerMessage[key] : undefined;
1345
+ const canAttachContext = !!contextTarget && typeof contextTarget === 'object';
1337
1346
  const timestamp = unixTimestampSeconds(options.timestamp);
1338
1347
  const { quoted, userJid } = options;
1339
1348
  if (quoted) {
@@ -1348,7 +1357,7 @@ export const generateWAMessageFromContent = (jid, message, options) => {
1348
1357
  if (typeof quotedContent === 'object' && quotedContent && 'contextInfo' in quotedContent) {
1349
1358
  delete quotedContent.contextInfo;
1350
1359
  }
1351
- const contextInfo = ('contextInfo' in innerMessage[key] && innerMessage[key]?.contextInfo) || {};
1360
+ const contextInfo = (canAttachContext && 'contextInfo' in contextTarget && contextTarget.contextInfo) || {};
1352
1361
  contextInfo.participant = jidNormalizedUser(participant);
1353
1362
  contextInfo.stanzaId = quoted.key.id;
1354
1363
  contextInfo.quotedMessage = quotedMsg;
@@ -1360,9 +1369,9 @@ export const generateWAMessageFromContent = (jid, message, options) => {
1360
1369
  if (!isJidNewsletter(jid) && jid !== quoted.key.remoteJid) {
1361
1370
  contextInfo.remoteJid = quoted.key.remoteJid;
1362
1371
  }
1363
- if (contextInfo && innerMessage[key]) {
1372
+ if (contextInfo && canAttachContext) {
1364
1373
  /* @ts-ignore */
1365
- innerMessage[key].contextInfo = contextInfo;
1374
+ contextTarget.contextInfo = contextInfo;
1366
1375
  }
1367
1376
  }
1368
1377
  if (
@@ -1373,7 +1382,9 @@ export const generateWAMessageFromContent = (jid, message, options) => {
1373
1382
  // already not converted to disappearing message
1374
1383
  key !== 'ephemeralMessage' &&
1375
1384
  // newsletters don't support ephemeral messages
1376
- !isJidNewsletter(jid)) {
1385
+ !isJidNewsletter(jid) &&
1386
+ // and a string/absent content value cannot carry one (HB-122)
1387
+ canAttachContext) {
1377
1388
  /* @ts-ignore */
1378
1389
  innerMessage[key].contextInfo = {
1379
1390
  ...(innerMessage[key].contextInfo || {}),
@@ -284,12 +284,24 @@ const processMessage = async (message, { shouldProcessHistoryMsg, placeholderRes
284
284
  ...content.reactionMessage,
285
285
  key: message.key
286
286
  };
287
- ev.emit('messages.reaction', [
288
- {
289
- reaction,
290
- key: content.reactionMessage?.key
291
- }
292
- ]);
287
+ // AunuBaileys (1.0.86, HB-187): reactionMessage.key is an
288
+ // optional proto field — a reaction without it used to be
289
+ // emitted with key === undefined and detonated downstream
290
+ // (the event-buffer append and the in-memory store both
291
+ // deref key.remoteJid) with a raw TypeError, AFTER the
292
+ // delivery receipt had already been sent: the stanza
293
+ // earned a NACK 500 on top of its receipt and the reaction
294
+ // was lost on every redelivery. The reaction has no
295
+ // target message, so there is nothing to attach it to —
296
+ // do not emit it at all.
297
+ if (content.reactionMessage.key) {
298
+ ev.emit('messages.reaction', [
299
+ {
300
+ reaction,
301
+ key: content.reactionMessage.key
302
+ }
303
+ ]);
304
+ }
293
305
  }
294
306
  else if (message.messageStubType) {
295
307
  const jid = message.key?.remoteJid;
package/package.json CHANGED
@@ -1,7 +1,7 @@
1
1
  {
2
2
  "name": "@aunucore/baileys",
3
3
  "type": "module",
4
- "version": "1.0.85",
4
+ "version": "1.0.86",
5
5
  "description": "AunuBaileys — the self-healing WhatsApp Web API for Node.js. Bad MAC and \"2000 messages into the future\" errors repair themselves, LID and phone-number identities stay in sync, anti-ban is built in, pairing codes are custom — plus 29 experimental features no other Baileys fork ships: invisible watermarks, per-person tag-all cards, side-profile text, ghost mentions and more. ESM, CJS and full TypeScript types.",
6
6
  "keywords": [
7
7
  "whatsapp",