@aunucore/baileys 1.0.85 → 1.0.87

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
  };
@@ -82,8 +82,30 @@ export const makeChatsSocket = (config) => {
82
82
  // AunuBaileys (1.0.71): a privacy change is a settings change —
83
83
  // refetch the full set (the cached copy is now stale) and publish
84
84
  // it (donor event: violetix 'privacy.update' on account_sync).
85
- privacySettings = await fetchPrivacySettings(true);
86
- ev.emit('privacy.update', privacySettings);
85
+ // AunuBaileys (1.0.87, HB-163): the SET above has already
86
+ // been applied by the server when this refetch runs. A
87
+ // refetch failure used to reject the whole privacyQuery —
88
+ // the caller saw a failed update that had in fact taken
89
+ // effect, the cache kept the OLD value, and readMessages
90
+ // derived its receipt type from that stale cache (sending
91
+ // 'read' receipts against a receipts setting the user had
92
+ // just switched off). Make the refetch best-effort: on
93
+ // failure, fold the applied change into the cached copy so
94
+ // cache and event reflect what the server now holds for
95
+ // this key. Never fabricate a partial cache when none
96
+ // exists — that would suppress the next full fetch.
97
+ try {
98
+ privacySettings = await fetchPrivacySettings(true);
99
+ }
100
+ catch (err) {
101
+ logger.warn({ err, name, value }, 'failed to refetch privacy settings after a successful set; folding the change into the cached copy');
102
+ if (privacySettings) {
103
+ privacySettings = { ...privacySettings, [name]: value };
104
+ }
105
+ }
106
+ if (privacySettings) {
107
+ ev.emit('privacy.update', privacySettings);
108
+ }
87
109
  };
88
110
  const updateMessagesPrivacy = async (value) => {
89
111
  await privacyQuery('messages', value);
@@ -906,16 +928,33 @@ export const makeChatsSocket = (config) => {
906
928
  }
907
929
  }
908
930
  const historyMsg = getHistoryMsg(msg.message);
909
- const shouldProcessHistoryMsg = historyMsg
931
+ // AunuBaileys (1.0.86, HB-113 short series): the
932
+ // shouldSyncHistoryMessage preference governs PASSIVE sync
933
+ // only. An ON_DEMAND payload is the answer to an explicit
934
+ // host request (fetchMessageHistory /
935
+ // requestPlaceholderResend); the single shared gate used
936
+ // to discard it silently whenever the preference was off
937
+ // — including the syncFullHistory:false default — so the
938
+ // host's request vanished without an error and without
939
+ // the messaging-history.set event. The initial-sync state
940
+ // machine below keeps the preference-based predicate
941
+ // (syncPreferenceAllows): on-demand responses must not
942
+ // drive it. For every non-ON_DEMAND input, and for
943
+ // ON_DEMAND with the preference on, syncPreferenceAllows
944
+ // equals the old gate exactly.
945
+ const syncPreferenceAllows = historyMsg
910
946
  ? shouldSyncHistoryMessage(historyMsg) && PROCESSABLE_HISTORY_TYPES.includes(historyMsg.syncType)
911
947
  : false;
948
+ const shouldProcessHistoryMsg = historyMsg?.syncType === proto.HistorySync.HistorySyncType.ON_DEMAND
949
+ ? true
950
+ : syncPreferenceAllows;
912
951
  // State machine: decide on sync and flush
913
952
  if (historyMsg && syncState === SyncState.AwaitingInitialSync) {
914
953
  if (awaitingSyncTimeout) {
915
954
  clearTimeout(awaitingSyncTimeout);
916
955
  awaitingSyncTimeout = undefined;
917
956
  }
918
- if (shouldProcessHistoryMsg) {
957
+ if (syncPreferenceAllows) {
919
958
  syncState = SyncState.Syncing;
920
959
  logger.info('Transitioned to Syncing state');
921
960
  // Let doAppStateSync handle the final flush after it's done
@@ -929,11 +968,35 @@ export const makeChatsSocket = (config) => {
929
968
  const doAppStateSync = async () => {
930
969
  if (syncState === SyncState.Syncing) {
931
970
  logger.info('Doing app state sync');
932
- await resyncAppState(ALL_WA_PATCH_NAMES, true);
933
- // Sync is complete, go online and flush everything
971
+ // AunuBaileys (1.0.87, HB-241): resyncAppState can
972
+ // throw OUTSIDE its per-collection handling — the
973
+ // sync IQ itself, a malformed blob in
974
+ // extractSyncdPatches, the resync-round cap. That
975
+ // throw used to reject upsertMessage and leave
976
+ // syncState at Syncing for the rest of the session:
977
+ // the full initial sync was never retried (nothing
978
+ // re-triggers it) and the state machine never
979
+ // reached Online. A failed sync must END the
980
+ // Syncing state, not freeze it — go Online and
981
+ // flush either way, and only count the sync as
982
+ // complete (the accountSyncCounter advance below)
983
+ // when it actually succeeded.
984
+ let syncFailed = false;
985
+ try {
986
+ await resyncAppState(ALL_WA_PATCH_NAMES, true);
987
+ }
988
+ catch (err) {
989
+ syncFailed = true;
990
+ logger.error({ err }, 'app state sync failed; transitioning to Online state anyway');
991
+ }
992
+ // Sync ran its course, go online and flush everything
934
993
  syncState = SyncState.Online;
935
994
  logger.info('App state sync complete, transitioning to Online state and flushing buffer');
936
995
  ev.flush();
996
+ // (HB-241) a failed sync is not a completed one:
997
+ // do not advance the counter that signals the
998
+ // initial sync finished.
999
+ if (!syncFailed) {
937
1000
  const accountSyncCounter = (authState.creds.accountSyncCounter || 0) + 1;
938
1001
  // AunuBaileys (1.0.81, HB-257): same guard as the
939
1002
  // pushName emit above — a throwing 'creds.update'
@@ -946,6 +1009,7 @@ export const makeChatsSocket = (config) => {
946
1009
  catch (err) {
947
1010
  onUnexpectedError(err, 'creds.update (accountSyncCounter)');
948
1011
  }
1012
+ }
949
1013
  }
950
1014
  };
951
1015
  await Promise.all([
@@ -1771,7 +1771,14 @@ export const makeMessagesRecvSocket = (config) => {
1771
1771
  }
1772
1772
  const operation = data?.operation;
1773
1773
  const updates = data?.updates;
1774
- if (!updates || !operation) {
1774
+ // AunuBaileys (1.0.87, HB-121): this guard used to be a
1775
+ // truthiness check only — a MEX payload whose `updates`
1776
+ // is a non-array (object/number/boolean) passed it, and
1777
+ // the for...of loops in every switch branch below threw a
1778
+ // TypeError, silently dropping the whole notification
1779
+ // (handleNotification acks the stanza in its finally
1780
+ // either way, so nothing ever retried it).
1781
+ if (!Array.isArray(updates) || !operation) {
1775
1782
  logger.warn({ data }, 'Invalid mex newsletter notification content');
1776
1783
  return;
1777
1784
  }
@@ -1986,9 +1993,22 @@ export const makeMessagesRecvSocket = (config) => {
1986
1993
  msg.messageStubType = call.isVideo ? WAMessageStubType.CALL_MISSED_VIDEO : WAMessageStubType.CALL_MISSED_VOICE;
1987
1994
  }
1988
1995
  }
1989
- else {
1996
+ else if (call.id) {
1990
1997
  msg.message = { call: { callKey: Buffer.from(call.id) } };
1991
1998
  }
1999
+ else {
2000
+ // AunuBaileys (1.0.86, HB-216): a group call offer
2001
+ // without a call-id (an optional stanza attr the
2002
+ // decoder does not validate) made Buffer.from throw
2003
+ // a raw TypeError inside this listener; the throw
2004
+ // unwound the whole 'call' emit, so host listeners
2005
+ // registered after this one never saw the offer at
2006
+ // all. There is no call key to reference and the
2007
+ // message key would carry no id either — skip the
2008
+ // notification message; the 'call' event itself
2009
+ // still reaches every listener.
2010
+ return;
2011
+ }
1992
2012
  const protoMsg = proto.WebMessageInfo.fromObject(msg);
1993
2013
  // AunuBaileys: this floated unawaited and uncaught — a
1994
2014
  // 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;
package/lib/Socket/mex.js CHANGED
@@ -22,11 +22,29 @@ export const executeWMexQuery = async (variables, queryId, dataPath, query, gene
22
22
  const result = await wMexQuery(variables, queryId, query, generateMessageTag);
23
23
  const child = getBinaryNodeChild(result, 'result');
24
24
  if (child?.content) {
25
- const data = JSON.parse(child.content.toString());
25
+ // AunuBaileys (1.0.87, HB-211): a <result> that is not JSON
26
+ // (including a child-node array body, which stringifies to
27
+ // "[object Object]") used to reject with a raw SyntaxError
28
+ // the host could not classify.
29
+ let data;
30
+ try {
31
+ data = JSON.parse(child.content.toString());
32
+ }
33
+ catch (err) {
34
+ throw new Boom('GraphQL server error: unparseable response', { statusCode: 400, data: result });
35
+ }
26
36
  if (data.errors && data.errors.length > 0) {
27
37
  const errorMessages = data.errors.map((err) => err.message || 'Unknown error').join(', ');
28
38
  const firstError = data.errors[0];
29
- const errorCode = firstError.extensions?.error_code || 400;
39
+ // AunuBaileys (1.0.87, HB-211): the server's error_code
40
+ // went into the Boom constructor unsanitised — a string
41
+ // code ('RATE_LIMITED') or an out-of-range one (200)
42
+ // made `new Boom` itself throw a non-Boom TypeError,
43
+ // and the server's actual error message was lost with
44
+ // it. Same sanitation as HB-42: only a numeric HTTP
45
+ // error code survives, everything else becomes 400.
46
+ const rawErrorCode = firstError.extensions?.error_code;
47
+ const errorCode = typeof rawErrorCode === 'number' && rawErrorCode >= 400 && rawErrorCode <= 599 ? rawErrorCode : 400;
30
48
  throw new Boom(`GraphQL server error: ${errorMessages}`, { statusCode: errorCode, data: firstError });
31
49
  }
32
50
  const response = dataPath ? data?.data?.[dataPath] : data?.data;
@@ -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) {
@@ -37,31 +37,37 @@ import { jidDecode } from '../WABinary/index.js';
37
37
  import { getKeyAuthor } from './generics.js';
38
38
  import { getAggregateVotesInPollMessage, normalizeMessageContent } from './messages.js';
39
39
 
40
+ // AunuBaileys (1.0.87, HB-218): every operand is optional-chained,
41
+ // exactly like the base getFutureProofMessage in messages.js. A
42
+ // wrapper whose `message` field is absent decodes with message ===
43
+ // null; the bare derefs used to throw a raw TypeError on the next
44
+ // loop iteration of normalizeAunuMessageContentExtended (and in
45
+ // getAggregateVotesInPollMessageExtended downstream of it).
40
46
  const getFutureProofMessageExtended = (message) => (
41
- message.ephemeralMessage ||
42
- message.viewOnceMessage ||
43
- message.documentWithCaptionMessage ||
44
- message.viewOnceMessageV2 ||
45
- message.viewOnceMessageV2Extension ||
46
- message.editedMessage ||
47
- message.groupMentionedMessage ||
48
- message.botInvokeMessage ||
49
- message.lottieStickerMessage ||
50
- message.eventCoverImage ||
51
- message.statusMentionMessage ||
52
- message.pollCreationOptionImageMessage ||
53
- message.associatedChildMessage ||
54
- message.groupStatusMentionMessage ||
55
- message.pollCreationMessageV4 ||
56
- message.pollCreationMessageV5 ||
57
- message.statusAddYours ||
58
- message.groupStatusMessage ||
59
- message.limitSharingMessage ||
60
- message.botTaskMessage ||
61
- message.questionMessage ||
62
- message.groupStatusMessageV2 ||
63
- message.botForwardedMessage ||
64
- message.spoilerMessage
47
+ message?.ephemeralMessage ||
48
+ message?.viewOnceMessage ||
49
+ message?.documentWithCaptionMessage ||
50
+ message?.viewOnceMessageV2 ||
51
+ message?.viewOnceMessageV2Extension ||
52
+ message?.editedMessage ||
53
+ message?.groupMentionedMessage ||
54
+ message?.botInvokeMessage ||
55
+ message?.lottieStickerMessage ||
56
+ message?.eventCoverImage ||
57
+ message?.statusMentionMessage ||
58
+ message?.pollCreationOptionImageMessage ||
59
+ message?.associatedChildMessage ||
60
+ message?.groupStatusMentionMessage ||
61
+ message?.pollCreationMessageV4 ||
62
+ message?.pollCreationMessageV5 ||
63
+ message?.statusAddYours ||
64
+ message?.groupStatusMessage ||
65
+ message?.limitSharingMessage ||
66
+ message?.botTaskMessage ||
67
+ message?.questionMessage ||
68
+ message?.groupStatusMessageV2 ||
69
+ message?.botForwardedMessage ||
70
+ message?.spoilerMessage
65
71
  );
66
72
 
67
73
  /**
@@ -286,6 +286,22 @@ export const decodePatches = async (name, syncds, initial, getAppStateSyncKey, o
286
286
  };
287
287
  const mutationMap = {};
288
288
  for (const syncd of syncds) {
289
+ // AunuBaileys (1.0.87, HB-141 + HB-149): snapshot the state
290
+ // before this patch. The version assignment inside the try
291
+ // runs BEFORE the patch is decoded, so a patch that failed
292
+ // (bad content HMAC, missing key, snapshot-MAC mismatch)
293
+ // still advanced the persisted version past itself — its
294
+ // mutations were never re-sent by the server, and the LT
295
+ // hash diverged from the true state permanently: every
296
+ // later snapshot-MAC check failed while the log claimed the
297
+ // patch had merely been "skipped". decodeSyncdPatch never
298
+ // mutates newState in place (the LT-hash generator copies
299
+ // the index map and returns fresh state), so restoring
300
+ // these three references in the catch is an exact rollback.
301
+ const prevVersion = newState.version;
302
+ const prevHash = newState.hash;
303
+ const prevIndexValueMap = newState.indexValueMap;
304
+ const appliedMutations = [];
289
305
  try {
290
306
  const { version, keyId, snapshotMac } = syncd;
291
307
  if (syncd.externalMutations) {
@@ -300,6 +316,11 @@ export const decodePatches = async (name, syncds, initial, getAppStateSyncKey, o
300
316
  const decodeResult = await decodeSyncdPatch(syncd, name, newState, getAppStateSyncKey, shouldMutate
301
317
  ? mutation => {
302
318
  const index = mutation.syncAction.index?.toString();
319
+ // (HB-141 + HB-149) record for rollback: a patch
320
+ // whose snapshot-MAC check fails below has already
321
+ // delivered its mutations here — they must not
322
+ // survive being "skipped" either.
323
+ appliedMutations.push({ index, had: index in mutationMap, prev: mutationMap[index] });
303
324
  mutationMap[index] = mutation;
304
325
  }
305
326
  : () => { }, true);
@@ -324,6 +345,28 @@ export const decodePatches = async (name, syncds, initial, getAppStateSyncKey, o
324
345
  // AunuBaileys: satu patch rusak tidak membunuh seluruh sinkronisasi
325
346
  logger?.warn({ err, name, version: syncd.version }, 'skipping undecodable app state patch');
326
347
  syncd.mutations = [];
348
+ // AunuBaileys (1.0.87, HB-141 + HB-149): roll the failed
349
+ // patch back completely, then stop this batch — later
350
+ // patches build on this one's state, and letting one of
351
+ // them advance the persisted version past the hole would
352
+ // cement the loss. The version stays at the last good
353
+ // patch, so the next resync re-requests the failed patch
354
+ // instead of reporting a version that silently skips it;
355
+ // a transient cause (failed external download, a sync key
356
+ // that had not arrived yet) now self-heals.
357
+ newState.version = prevVersion;
358
+ newState.hash = prevHash;
359
+ newState.indexValueMap = prevIndexValueMap;
360
+ for (let i = appliedMutations.length - 1; i >= 0; i--) {
361
+ const { index, had, prev } = appliedMutations[i];
362
+ if (had) {
363
+ mutationMap[index] = prev;
364
+ }
365
+ else {
366
+ delete mutationMap[index];
367
+ }
368
+ }
369
+ break;
327
370
  }
328
371
  }
329
372
  return { state: newState, mutationMap };
@@ -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;
@@ -110,7 +110,22 @@ export class USyncQuery {
110
110
  if (errCode === 401 || errCode === 403 || errCode === 405) {
111
111
  return ['isBlockedByContact', true];
112
112
  }
113
- throw err;
113
+ // AunuBaileys (1.0.87, HB-175): any
114
+ // OTHER per-user parser error — the
115
+ // protocol node carried an <error>
116
+ // child with e.g. a 404 — used to be
117
+ // rethrown, aborting the parse of
118
+ // the ENTIRE batch: one user's error
119
+ // cost every other user's devices /
120
+ // contacts, and downstream sends that
121
+ // depend on the device list failed
122
+ // outright. Treat it the way the
123
+ // business parser treats an error
124
+ // child: this user simply has no data
125
+ // for this protocol (the null is
126
+ // dropped by the filter below); the
127
+ // rest of the batch continues.
128
+ return [protocol, null];
114
129
  }
115
130
  }
116
131
  else {
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.87",
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",