@aunucore/baileys 1.0.82 → 1.0.84
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/lib/Socket/chats.js +11 -5
- package/lib/Socket/messages-send.js +232 -56
- package/lib/Store/make-in-memory-store.js +87 -11
- package/lib/Utils/auth-utils.js +27 -3
- package/lib/Utils/baileys-event-stream.js +39 -4
- package/lib/Utils/event-buffer.js +38 -6
- package/lib/Utils/generics.js +20 -1
- package/lib/Utils/lid-store.js +4 -1
- package/lib/Utils/messages.js +49 -7
- package/lib/Utils/signal.d.ts +1 -1
- package/lib/Utils/signal.js +13 -2
- package/package.json +1 -1
package/lib/Socket/chats.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import NodeCache from '@cacheable/node-cache';
|
|
2
1
|
import { Boom } from '@hapi/boom';
|
|
3
2
|
import { proto } from '../../WAProto/index.js';
|
|
4
3
|
import { DEFAULT_CACHE_TTLS, PROCESSABLE_HISTORY_TYPES } from '../Defaults/index.js';
|
|
@@ -6,6 +5,7 @@ import { ALL_WA_PATCH_NAMES } from '../Types/index.js';
|
|
|
6
5
|
import { SyncState } from '../Types/State.js';
|
|
7
6
|
import { chatModificationToAppPatch, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch, extractSyncdPatches, generateProfilePicture, getHistoryMsg, newLTHashState, processSyncAction } from '../Utils/index.js';
|
|
8
7
|
import { makeMutex } from '../Utils/make-mutex.js';
|
|
8
|
+
import { makeBoundedTTLCache } from '../Utils/bounded-cache.js';
|
|
9
9
|
import processMessage from '../Utils/process-message.js';
|
|
10
10
|
import { getBinaryNodeChild, getBinaryNodeChildren, isJidUser, isLidUser, jidDecode, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
11
11
|
import { USyncQuery, USyncUser } from '../WAUSync/index.js';
|
|
@@ -24,10 +24,16 @@ export const makeChatsSocket = (config) => {
|
|
|
24
24
|
// Timeout for AwaitingInitialSync state
|
|
25
25
|
let awaitingSyncTimeout;
|
|
26
26
|
const placeholderResendCache = config.placeholderResendCache ||
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
// AunuBaileys (1.0.84, HB-231): in the standard
|
|
28
|
+
// makeWASocket composition this factory runs BEFORE
|
|
29
|
+
// messages-recv creates its own default, and the
|
|
30
|
+
// write-back below means the instance created HERE is
|
|
31
|
+
// the one the whole receive path actually uses. It used
|
|
32
|
+
// to be a NodeCache with no size cap, silently shadowing
|
|
33
|
+
// the bounded default messages-recv intended ("bounded
|
|
34
|
+
// by default — a TTL alone lets these grow without
|
|
35
|
+
// limit"). Build the same bounded cache here.
|
|
36
|
+
makeBoundedTTLCache({ ttlSeconds: DEFAULT_CACHE_TTLS.MSG_RETRY, maxEntries: 1000 });
|
|
31
37
|
if (!config.placeholderResendCache) {
|
|
32
38
|
config.placeholderResendCache = placeholderResendCache;
|
|
33
39
|
}
|
|
@@ -188,7 +188,11 @@ export const makeMessagesSocket = (config) => {
|
|
|
188
188
|
}
|
|
189
189
|
const result = await sock.executeUSyncQuery(query);
|
|
190
190
|
if (result) {
|
|
191
|
-
|
|
191
|
+
// AunuBaileys (1.0.83, HB-258): pass our LID as well —
|
|
192
|
+
// comparing only the PN user left our own LID devices in
|
|
193
|
+
// the extracted list, so group sends encrypted a copy to
|
|
194
|
+
// our own LID device (upstream v7 made the same fix).
|
|
195
|
+
const extracted = extractDeviceJids(result?.list, authState.creds.me?.id, authState.creds.me?.lid, ignoreZeroDevices);
|
|
192
196
|
const deviceMap = {};
|
|
193
197
|
for (const item of extracted) {
|
|
194
198
|
deviceMap[item.user] = deviceMap[item.user] || [];
|
|
@@ -327,6 +331,16 @@ export const makeMessagesSocket = (config) => {
|
|
|
327
331
|
return { nodes: filteredNodes, shouldIncludeDeviceIdentity };
|
|
328
332
|
};
|
|
329
333
|
const relayMessage = async (jid, message, { messageId: msgId, participant, additionalAttributes, additionalNodes, useUserDevicesCache, useCachedGroupMetadata, addBizAttributes, statusJidList, statusPrivacy }) => {
|
|
334
|
+
// AunuBaileys (1.0.83, HB-253): a non-object message — or an
|
|
335
|
+
// empty object — proto-encodes to ZERO content bytes without
|
|
336
|
+
// throwing, so the relay used to report success while the
|
|
337
|
+
// peer received random padding only: the 1.0.71 silent-empty
|
|
338
|
+
// class at the one public entry point that never got a guard
|
|
339
|
+
// (sendMessage fails loudly for the same class of input).
|
|
340
|
+
// Fail fast, before any wait or network work.
|
|
341
|
+
if (!message || typeof message !== 'object' || Array.isArray(message) || !Object.keys(message).length) {
|
|
342
|
+
throw new Boom('relayMessage requires a non-empty message object', { statusCode: 400 });
|
|
343
|
+
}
|
|
330
344
|
if (!ws.isOpen) {
|
|
331
345
|
// AunuBaileys: a send attempted during a reconnect window used to die
|
|
332
346
|
// instantly with Boom 428 from the first internal query — the message
|
|
@@ -412,6 +426,10 @@ export const makeMessagesSocket = (config) => {
|
|
|
412
426
|
const { user, device } = decodedParticipant;
|
|
413
427
|
devices.push({ user, device });
|
|
414
428
|
}
|
|
429
|
+
// AunuBaileys (1.0.83, HB-178): the stanza built inside the
|
|
430
|
+
// transaction is captured here and sent only AFTER the
|
|
431
|
+
// transaction commits — see the tail of this function.
|
|
432
|
+
let stanzaToSend;
|
|
415
433
|
await authState.keys.transaction(async () => {
|
|
416
434
|
const mediaType = getMediaType(message);
|
|
417
435
|
if (mediaType) {
|
|
@@ -420,6 +438,15 @@ export const makeMessagesSocket = (config) => {
|
|
|
420
438
|
if (isNewsletter) {
|
|
421
439
|
// Patch message if needed, then encode as plaintext
|
|
422
440
|
const patched = patchMessageBeforeSending ? await patchMessageBeforeSending(message, []) : message;
|
|
441
|
+
// AunuBaileys (1.0.83, HB-212): a per-jid patch ARRAY
|
|
442
|
+
// is in-contract for patchMessageBeforeSending, but
|
|
443
|
+
// encodeNewsletterMessage is a bare proto encode —
|
|
444
|
+
// an array encodes to 0 bytes, so the channel got an
|
|
445
|
+
// empty post while sendMessage resolved as success.
|
|
446
|
+
// Mirror the group branch and refuse it loudly.
|
|
447
|
+
if (Array.isArray(patched)) {
|
|
448
|
+
throw new Boom('Per-jid patching is not supported in newsletters', { statusCode: 400 });
|
|
449
|
+
}
|
|
423
450
|
const bytes = encodeNewsletterMessage(patched);
|
|
424
451
|
binaryNodeContent.push({
|
|
425
452
|
tag: 'plaintext',
|
|
@@ -447,8 +474,14 @@ export const makeMessagesSocket = (config) => {
|
|
|
447
474
|
content: binaryNodeContent
|
|
448
475
|
};
|
|
449
476
|
logger.debug({ msgId }, `sending newsletter message to ${jid}`);
|
|
450
|
-
|
|
451
|
-
|
|
477
|
+
// AunuBaileys (1.0.83, HB-178): captured, not sent —
|
|
478
|
+
// the send happens after the commit at the tail of
|
|
479
|
+
// relayMessage, and the sent-message cache add lives
|
|
480
|
+
// there too (it used to sit here, inside the
|
|
481
|
+
// transaction; with send-after-commit it would
|
|
482
|
+
// register a message the commit failure prevented
|
|
483
|
+
// from ever being sent).
|
|
484
|
+
stanzaToSend = stanza;
|
|
452
485
|
return;
|
|
453
486
|
}
|
|
454
487
|
if (normalizeMessageContent(message)?.pinInChatMessage) {
|
|
@@ -586,10 +619,43 @@ export const makeMessagesSocket = (config) => {
|
|
|
586
619
|
allJids.push(jid);
|
|
587
620
|
}
|
|
588
621
|
await assertSessions(allJids, false);
|
|
589
|
-
|
|
622
|
+
// AunuBaileys (1.0.83, HB-161): the echo to our own
|
|
623
|
+
// devices and the send to the peer used to share one
|
|
624
|
+
// Promise.all — when encrypting for ALL of our own
|
|
625
|
+
// devices failed (a rotten self-session the one-shot
|
|
626
|
+
// heal inside createParticipantNodes could not save),
|
|
627
|
+
// the peer's already-encrypted nodes were thrown away
|
|
628
|
+
// with it and the DM never left, even though the
|
|
629
|
+
// peer's sessions were healthy; symmetrically, a
|
|
630
|
+
// total peer failure took the echo down too. The echo
|
|
631
|
+
// is best-effort: settle both sides independently and
|
|
632
|
+
// fail only when the side that must deliver produced
|
|
633
|
+
// nothing.
|
|
634
|
+
const [meSettled, otherSettled] = await Promise.allSettled([
|
|
590
635
|
createParticipantNodes(meJids, meMsg, extraAttrs),
|
|
591
636
|
createParticipantNodes(otherJids, message, extraAttrs)
|
|
592
637
|
]);
|
|
638
|
+
if (otherSettled.status === 'rejected') {
|
|
639
|
+
throw otherSettled.reason;
|
|
640
|
+
}
|
|
641
|
+
let meNodes = [];
|
|
642
|
+
let s1 = false;
|
|
643
|
+
if (meSettled.status === 'fulfilled') {
|
|
644
|
+
meNodes = meSettled.value.nodes;
|
|
645
|
+
s1 = meSettled.value.shouldIncludeDeviceIdentity;
|
|
646
|
+
}
|
|
647
|
+
else if (!otherSettled.value.nodes.length && meJids.length) {
|
|
648
|
+
// Our own devices were the ONLY recipients (a
|
|
649
|
+
// retry resend aimed at our own device, a
|
|
650
|
+
// self-chat): with the echo gone there is nothing
|
|
651
|
+
// to send — keep the original failure instead of
|
|
652
|
+
// emitting an empty stanza.
|
|
653
|
+
throw meSettled.reason;
|
|
654
|
+
}
|
|
655
|
+
else {
|
|
656
|
+
logger.warn({ err: meSettled.reason }, 'aunu: encrypting the echo to our own devices failed; sending to the peer without the device-sent echo');
|
|
657
|
+
}
|
|
658
|
+
const { nodes: otherNodes, shouldIncludeDeviceIdentity: s2 } = otherSettled.value;
|
|
593
659
|
participants.push(...meNodes);
|
|
594
660
|
participants.push(...otherNodes);
|
|
595
661
|
shouldIncludeDeviceIdentity = shouldIncludeDeviceIdentity || s1 || s2;
|
|
@@ -658,8 +724,24 @@ export const makeMessagesSocket = (config) => {
|
|
|
658
724
|
stanza.content.push(getBizBinaryNode(message));
|
|
659
725
|
}
|
|
660
726
|
logger.debug({ msgId }, `sending message to ${participants.length} devices`);
|
|
661
|
-
|
|
727
|
+
stanzaToSend = stanza;
|
|
662
728
|
});
|
|
729
|
+
// AunuBaileys (1.0.83, HB-178): the stanza goes on the wire
|
|
730
|
+
// only now — AFTER the transaction above has committed the
|
|
731
|
+
// ratchet state it was encrypted under. The send used to be
|
|
732
|
+
// the last await INSIDE the transaction, so a commit that
|
|
733
|
+
// failed through every retry threw after the message was
|
|
734
|
+
// already delivered: the caller saw an error for a delivered
|
|
735
|
+
// message (and a host retry then produced a duplicate the
|
|
736
|
+
// peer could not decrypt, re-using the un-persisted chain
|
|
737
|
+
// key), while the cache add and the message-secret
|
|
738
|
+
// registration below were skipped for that delivered
|
|
739
|
+
// message. Persist first, then send: a commit failure now
|
|
740
|
+
// fails a message that was never sent, and a sent message
|
|
741
|
+
// is always registered.
|
|
742
|
+
if (stanzaToSend) {
|
|
743
|
+
await sendNode(stanzaToSend);
|
|
744
|
+
}
|
|
663
745
|
sentMessageCacheFor(config).add(jid, msgId, message);
|
|
664
746
|
// AunuBaileys (1.0.71): register the messageSecret at send time
|
|
665
747
|
// so an msmsg reply can be decrypted even if it arrives before
|
|
@@ -759,7 +841,19 @@ export const makeMessagesSocket = (config) => {
|
|
|
759
841
|
}
|
|
760
842
|
const callId = newCallId();
|
|
761
843
|
const offerContent = buildCallOfferContent(isVideo);
|
|
762
|
-
|
|
844
|
+
// AunuBaileys (1.0.83, HB-184): the callee's devices take
|
|
845
|
+
// the server of the DESTINATION jid — a LID callee's devices
|
|
846
|
+
// live on 'lid'; encoding them on 's.whatsapp.net' addressed
|
|
847
|
+
// a phone number that does not exist (LID digits are not a
|
|
848
|
+
// number), so the offer could never ring.
|
|
849
|
+
const callServer = jidDecode(toJid)?.server === 'lid' ? 'lid' : 's.whatsapp.net';
|
|
850
|
+
const devices = (await getUSyncDevices([toJid], true, false)).map(({ user, device }) => jidEncode(user, callServer, device));
|
|
851
|
+
// AunuBaileys (1.0.83, HB-147): an empty device list built
|
|
852
|
+
// an offer with ZERO <destination> nodes — a call that could
|
|
853
|
+
// ring nobody, with no client-side complaint. Fail fast.
|
|
854
|
+
if (!devices.length) {
|
|
855
|
+
throw new Boom(`offerCall: no devices found for ${toJid}; the call would ring nobody`, { statusCode: 400 });
|
|
856
|
+
}
|
|
763
857
|
await assertSessions(devices, true);
|
|
764
858
|
const { nodes: destinations, shouldIncludeDeviceIdentity } = await createParticipantNodes(devices, {
|
|
765
859
|
call: { callKey: createCallKey() }
|
|
@@ -800,7 +894,25 @@ export const makeMessagesSocket = (config) => {
|
|
|
800
894
|
}
|
|
801
895
|
const callId = newCallId();
|
|
802
896
|
const offerContent = buildCallOfferContent(isVideo);
|
|
803
|
-
|
|
897
|
+
// AunuBaileys (1.0.83, HB-184): each participant's devices
|
|
898
|
+
// take the server of that participant's OWN jid — in a
|
|
899
|
+
// LID-addressed group the participants are LID jids and
|
|
900
|
+
// their devices live on 'lid', not 's.whatsapp.net'.
|
|
901
|
+
const serverByUser = new Map();
|
|
902
|
+
for (const participantJid of participants) {
|
|
903
|
+
const decodedParticipant = jidDecode(jidNormalizedUser(participantJid) || '');
|
|
904
|
+
if (decodedParticipant?.user) {
|
|
905
|
+
serverByUser.set(decodedParticipant.user, decodedParticipant.server === 'lid' ? 'lid' : 's.whatsapp.net');
|
|
906
|
+
}
|
|
907
|
+
}
|
|
908
|
+
const devices = (await getUSyncDevices(participants, true, false)).map(({ user, device }) => jidEncode(user, serverByUser.get(user) ?? 's.whatsapp.net', device));
|
|
909
|
+
// AunuBaileys (1.0.83, HB-147): the default participants=[]
|
|
910
|
+
// (or a usync answer with no devices) built an offer with
|
|
911
|
+
// ZERO <destination> nodes — a call that could ring nobody,
|
|
912
|
+
// with no client-side complaint. Fail fast.
|
|
913
|
+
if (!devices.length) {
|
|
914
|
+
throw new Boom('groupCall: no participant devices to call; the call would ring nobody', { statusCode: 400 });
|
|
915
|
+
}
|
|
804
916
|
await assertSessions(devices, true);
|
|
805
917
|
const { nodes: destinations, shouldIncludeDeviceIdentity } = await createParticipantNodes(devices, {
|
|
806
918
|
call: { callKey: createCallKey() }
|
|
@@ -869,19 +981,44 @@ export const makeMessagesSocket = (config) => {
|
|
|
869
981
|
throw new Boom(`exclusive target is not a valid JID: ${target}`, { statusCode: 400 });
|
|
870
982
|
}
|
|
871
983
|
const devices = await getUSyncDevices([targetUser], relayOpts.useUserDevicesCache !== false, false);
|
|
872
|
-
|
|
984
|
+
// AunuBaileys (1.0.83, HB-188): the target's devices take
|
|
985
|
+
// the server of the TARGET jid — for a LID member they live
|
|
986
|
+
// on 'lid'; encoding them on 's.whatsapp.net' addressed a
|
|
987
|
+
// phone number that does not exist (the LID digits are not
|
|
988
|
+
// a number), so the exclusive message could never arrive.
|
|
989
|
+
const targetServer = jidDecode(targetUser)?.server === 'lid' ? 'lid' : 's.whatsapp.net';
|
|
990
|
+
const deviceJids = devices.map(({ user, device }) => jidEncode(user, targetServer, device)).filter(Boolean);
|
|
873
991
|
// No device list means the member is unknown to usync; addressing
|
|
874
992
|
// the bare jid still reaches their primary device rather than
|
|
875
993
|
// failing the send outright.
|
|
876
994
|
const targets = deviceJids.length ? deviceJids : [targetUser];
|
|
877
995
|
logger.debug({ jid, target: targetUser, devices: targets.length }, 'sending exclusive group message');
|
|
996
|
+
// AunuBaileys (1.0.83, HB-108): per-device isolation — one
|
|
997
|
+
// device's failure (a rotten session, a rejected stanza)
|
|
998
|
+
// used to abort the loop, so the remaining devices never
|
|
999
|
+
// received the message and the error did not say the
|
|
1000
|
+
// delivery was partial. Deliver to every device that works
|
|
1001
|
+
// and fail only when NONE did; the return value now lists
|
|
1002
|
+
// the devices actually delivered to.
|
|
1003
|
+
const delivered = [];
|
|
1004
|
+
let firstDeviceError;
|
|
878
1005
|
for (const deviceJid of targets) {
|
|
879
|
-
|
|
880
|
-
|
|
881
|
-
|
|
882
|
-
|
|
1006
|
+
try {
|
|
1007
|
+
await relayMessage(jid, message, {
|
|
1008
|
+
...relayOpts,
|
|
1009
|
+
participant: { jid: deviceJid, count: 0 }
|
|
1010
|
+
});
|
|
1011
|
+
delivered.push(deviceJid);
|
|
1012
|
+
}
|
|
1013
|
+
catch (err) {
|
|
1014
|
+
firstDeviceError = firstDeviceError ?? err;
|
|
1015
|
+
logger.warn({ err, jid, deviceJid }, 'aunu: exclusive send failed for one device; continuing with the remaining devices');
|
|
1016
|
+
}
|
|
883
1017
|
}
|
|
884
|
-
|
|
1018
|
+
if (!delivered.length) {
|
|
1019
|
+
throw firstDeviceError ?? new Boom('exclusive send failed for every target device', { statusCode: 500 });
|
|
1020
|
+
}
|
|
1021
|
+
return delivered;
|
|
885
1022
|
};
|
|
886
1023
|
// AunuBaileys (1.0.71): named so the wrappers below can call the
|
|
887
1024
|
// socket's own sendMessage — the `sock` closure variable is the
|
|
@@ -1094,60 +1231,99 @@ export const makeMessagesSocket = (config) => {
|
|
|
1094
1231
|
// AunuBaileys (1.0.71): the option images follow the parent,
|
|
1095
1232
|
// each pointing back at it via messageAssociation.
|
|
1096
1233
|
if (imagePollOptionMedia?.length) {
|
|
1234
|
+
// AunuBaileys (1.0.83, HB-173): per-child
|
|
1235
|
+
// isolation — one failing option image used to
|
|
1236
|
+
// abort the loop, leaving the already-sent poll
|
|
1237
|
+
// parent with only some (or none) of its images
|
|
1238
|
+
// while the caller saw a total failure. Deliver
|
|
1239
|
+
// every child that works; fail only when NONE
|
|
1240
|
+
// did.
|
|
1241
|
+
let deliveredChildren = 0;
|
|
1242
|
+
let firstChildError;
|
|
1097
1243
|
for (const media of imagePollOptionMedia) {
|
|
1098
|
-
|
|
1099
|
-
|
|
1100
|
-
|
|
1101
|
-
|
|
1102
|
-
|
|
1103
|
-
|
|
1244
|
+
try {
|
|
1245
|
+
const optionMsg = generateWAMessageFromContent(jid, {
|
|
1246
|
+
pollCreationOptionImageMessage: { message: media },
|
|
1247
|
+
messageContextInfo: {
|
|
1248
|
+
messageAssociation: {
|
|
1249
|
+
parentMessageKey: fullMsg.key,
|
|
1250
|
+
associationType: proto.MessageAssociation.AssociationType.MEDIA_POLL
|
|
1251
|
+
}
|
|
1104
1252
|
}
|
|
1105
|
-
}
|
|
1106
|
-
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
|
|
1112
|
-
|
|
1113
|
-
|
|
1114
|
-
|
|
1115
|
-
|
|
1253
|
+
}, {
|
|
1254
|
+
userJid,
|
|
1255
|
+
messageId: generateMessageIDV2(sock.user?.id)
|
|
1256
|
+
});
|
|
1257
|
+
await relay(jid, optionMsg.message, {
|
|
1258
|
+
messageId: optionMsg.key.id,
|
|
1259
|
+
useCachedGroupMetadata: options.useCachedGroupMetadata,
|
|
1260
|
+
additionalAttributes,
|
|
1261
|
+
additionalNodes: []
|
|
1262
|
+
});
|
|
1263
|
+
deliveredChildren += 1;
|
|
1264
|
+
}
|
|
1265
|
+
catch (err) {
|
|
1266
|
+
firstChildError = firstChildError ?? err;
|
|
1267
|
+
logger.warn({ err, jid }, 'aunu: one image-poll option image failed to send; continuing with the rest');
|
|
1268
|
+
}
|
|
1269
|
+
}
|
|
1270
|
+
if (!deliveredChildren) {
|
|
1271
|
+
throw firstChildError ?? new Boom('all image-poll option images failed to send', { statusCode: 500 });
|
|
1116
1272
|
}
|
|
1117
1273
|
}
|
|
1118
1274
|
// AunuBaileys: kirim isi album sebagai pesan terasosiasi (dari @itsliaaa/baileys)
|
|
1119
1275
|
if ('album' in content) {
|
|
1120
1276
|
const { delayMs = 1500 } = options;
|
|
1277
|
+
// AunuBaileys (1.0.83, HB-173): per-item
|
|
1278
|
+
// isolation — one failing item (upload timeout,
|
|
1279
|
+
// total encryption failure) used to abort the
|
|
1280
|
+
// loop, leaving the already-sent album parent
|
|
1281
|
+
// with only some (or none) of its items while
|
|
1282
|
+
// the caller saw a total failure (and a retry
|
|
1283
|
+
// duplicated the parent). Deliver every item
|
|
1284
|
+
// that works; fail only when NONE did.
|
|
1285
|
+
let deliveredItems = 0;
|
|
1286
|
+
let firstItemError;
|
|
1121
1287
|
for (const albumMedia of content.album) {
|
|
1122
|
-
|
|
1123
|
-
|
|
1124
|
-
|
|
1125
|
-
|
|
1126
|
-
|
|
1127
|
-
|
|
1128
|
-
|
|
1129
|
-
|
|
1130
|
-
|
|
1131
|
-
|
|
1132
|
-
|
|
1133
|
-
|
|
1288
|
+
try {
|
|
1289
|
+
const albumMsg = await generateWAMessage(jid, albumMedia, {
|
|
1290
|
+
logger,
|
|
1291
|
+
userJid,
|
|
1292
|
+
upload: (fp, opts) => waUploadToServer(fp, { ...opts, newsletter: isJidNewsletter(jid) }),
|
|
1293
|
+
mediaCache: config.mediaCache,
|
|
1294
|
+
options: config.options,
|
|
1295
|
+
...options,
|
|
1296
|
+
messageId: generateMessageIDV2(sock.user?.id)
|
|
1297
|
+
});
|
|
1298
|
+
const normalized = normalizeMessageContent(albumMsg.message);
|
|
1299
|
+
if (!normalized || !('imageMessage' in normalized || 'videoMessage' in normalized)) {
|
|
1300
|
+
throw new Boom('Invalid message type for album', { statusCode: 400 });
|
|
1301
|
+
}
|
|
1302
|
+
albumMsg.message.messageContextInfo = albumMsg.message.messageContextInfo || {};
|
|
1303
|
+
albumMsg.message.messageContextInfo.messageAssociation = {
|
|
1304
|
+
parentMessageKey: fullMsg.key,
|
|
1305
|
+
associationType: proto.MessageAssociation.AssociationType.MEDIA_ALBUM
|
|
1306
|
+
};
|
|
1307
|
+
await relay(jid, albumMsg.message, {
|
|
1308
|
+
messageId: albumMsg.key.id,
|
|
1309
|
+
useCachedGroupMetadata: options.useCachedGroupMetadata,
|
|
1310
|
+
statusJidList: options.statusJidList,
|
|
1311
|
+
statusPrivacy: options.statusPrivacy,
|
|
1312
|
+
statusPrivacy: options.statusPrivacy,
|
|
1313
|
+
additionalAttributes,
|
|
1314
|
+
additionalNodes
|
|
1315
|
+
});
|
|
1316
|
+
deliveredItems += 1;
|
|
1317
|
+
}
|
|
1318
|
+
catch (err) {
|
|
1319
|
+
firstItemError = firstItemError ?? err;
|
|
1320
|
+
logger.warn({ err, jid }, 'aunu: one album item failed to send; continuing with the rest of the album');
|
|
1134
1321
|
}
|
|
1135
|
-
albumMsg.message.messageContextInfo = albumMsg.message.messageContextInfo || {};
|
|
1136
|
-
albumMsg.message.messageContextInfo.messageAssociation = {
|
|
1137
|
-
parentMessageKey: fullMsg.key,
|
|
1138
|
-
associationType: proto.MessageAssociation.AssociationType.MEDIA_ALBUM
|
|
1139
|
-
};
|
|
1140
|
-
await relay(jid, albumMsg.message, {
|
|
1141
|
-
messageId: albumMsg.key.id,
|
|
1142
|
-
useCachedGroupMetadata: options.useCachedGroupMetadata,
|
|
1143
|
-
statusJidList: options.statusJidList,
|
|
1144
|
-
statusPrivacy: options.statusPrivacy,
|
|
1145
|
-
statusPrivacy: options.statusPrivacy,
|
|
1146
|
-
additionalAttributes,
|
|
1147
|
-
additionalNodes
|
|
1148
|
-
});
|
|
1149
1322
|
await delay(delayMs);
|
|
1150
1323
|
}
|
|
1324
|
+
if (content.album.length && !deliveredItems) {
|
|
1325
|
+
throw firstItemError ?? new Boom('all album items failed to send', { statusCode: 500 });
|
|
1326
|
+
}
|
|
1151
1327
|
}
|
|
1152
1328
|
if (config.emitOwnEvents) {
|
|
1153
1329
|
process.nextTick(() => {
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// AunuCore fork backport: in-memory store dari Baileys 6.6.0, dikonversi ke ESM untuk basis 6.7.x
|
|
2
2
|
import KeyedDBModule from '@adiwajshing/keyed-db';
|
|
3
3
|
const KeyedDB = KeyedDBModule.default || KeyedDBModule;
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, renameSync } from 'fs';
|
|
5
5
|
import { proto } from '../../WAProto/index.js';
|
|
6
6
|
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults/index.js';
|
|
7
7
|
import { LabelAssociationType } from '../Types/LabelAssociation.js';
|
|
@@ -131,6 +131,16 @@ const makeInMemoryStore = ({ logger: _logger, chatKey, labelAssociationKey }) =>
|
|
|
131
131
|
for (const update of updates) {
|
|
132
132
|
if (contacts[update.id]) {
|
|
133
133
|
Object.assign(contacts[update.id], update);
|
|
134
|
+
// AunuBaileys (1.0.84, HB-245): a picture
|
|
135
|
+
// notification arrives as imgUrl 'changed' /
|
|
136
|
+
// 'removed' — a sentinel, not a URL. Drop the
|
|
137
|
+
// cached URL instead of storing the sentinel:
|
|
138
|
+
// fetchImageUrl used to hand the literal string
|
|
139
|
+
// 'changed' to the host as the avatar URL, and
|
|
140
|
+
// writeToFile persisted the poison to disk.
|
|
141
|
+
if (update.imgUrl === 'changed' || update.imgUrl === 'removed') {
|
|
142
|
+
delete contacts[update.id].imgUrl;
|
|
143
|
+
}
|
|
134
144
|
}
|
|
135
145
|
else {
|
|
136
146
|
logger.debug({ update }, 'got update for non-existant contact');
|
|
@@ -258,7 +268,18 @@ const makeInMemoryStore = ({ logger: _logger, chatKey, labelAssociationKey }) =>
|
|
|
258
268
|
if (metadata) {
|
|
259
269
|
switch (action) {
|
|
260
270
|
case 'add':
|
|
261
|
-
|
|
271
|
+
// AunuBaileys (1.0.84, HB-237): skip ids
|
|
272
|
+
// that are already listed. The metadata is
|
|
273
|
+
// usually populated by a fetch that already
|
|
274
|
+
// contains the member, and a redelivery
|
|
275
|
+
// replays the notification — both used to
|
|
276
|
+
// push a duplicate entry every single time,
|
|
277
|
+
// without bound.
|
|
278
|
+
for (const participantId of participants) {
|
|
279
|
+
if (!metadata.participants.some(p => p.id === participantId)) {
|
|
280
|
+
metadata.participants.push({ id: participantId, isAdmin: false, isSuperAdmin: false });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
262
283
|
break;
|
|
263
284
|
case 'demote':
|
|
264
285
|
case 'promote':
|
|
@@ -301,14 +322,43 @@ const makeInMemoryStore = ({ logger: _logger, chatKey, labelAssociationKey }) =>
|
|
|
301
322
|
labelAssociations
|
|
302
323
|
});
|
|
303
324
|
const fromJSON = (json) => {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
325
|
+
// AunuBaileys (1.0.84, HB-134 + HB-142 + addenda): every
|
|
326
|
+
// stage — and every entry — is isolated. One poisoned
|
|
327
|
+
// message entry used to throw out of fromJSON mid-loop:
|
|
328
|
+
// that jid's remaining messages and EVERY later jid were
|
|
329
|
+
// silently lost on every boot, while the stages before
|
|
330
|
+
// the throw stayed applied and readFromFile logged
|
|
331
|
+
// "starting with an empty store" — a claim the state did
|
|
332
|
+
// not match. A missing/null contacts stage threw the
|
|
333
|
+
// same way (Object.values of undefined), and a poisoned
|
|
334
|
+
// contacts/labels entry aborted every stage after it.
|
|
335
|
+
// Wrong-typed stages now read as empty, unusable entries
|
|
336
|
+
// are skipped, and a bad message entry is caught per
|
|
337
|
+
// entry with a warning — the healthy remainder of the
|
|
338
|
+
// file always loads.
|
|
339
|
+
if (!json || typeof json !== 'object') {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const entriesOf = (value) => (Array.isArray(value) ? value : []).filter(e => !!e && typeof e === 'object');
|
|
343
|
+
const valuesOf = (value) => (value && typeof value === 'object' ? Object.values(value) : []);
|
|
344
|
+
chats.upsert(...entriesOf(json.chats));
|
|
345
|
+
labelAssociations.upsert(...entriesOf(json.labelAssociations));
|
|
346
|
+
contactsUpsert(valuesOf(json.contacts).filter(c => !!c && typeof c === 'object' && typeof c.id === 'string'));
|
|
347
|
+
labelsUpsert(valuesOf(json.labels).filter(l => !!l && typeof l === 'object' && typeof l.id === 'string'));
|
|
348
|
+
const messageSets = json.messages && typeof json.messages === 'object' ? json.messages : {};
|
|
349
|
+
for (const jid in messageSets) {
|
|
350
|
+
const entries = messageSets[jid];
|
|
351
|
+
if (!Array.isArray(entries)) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
309
354
|
const list = assertMessageList(jid);
|
|
310
|
-
for (const msg of
|
|
311
|
-
|
|
355
|
+
for (const msg of entries) {
|
|
356
|
+
try {
|
|
357
|
+
list.upsert(proto.WebMessageInfo.fromObject(msg), 'append');
|
|
358
|
+
}
|
|
359
|
+
catch (err) {
|
|
360
|
+
logger.warn({ jid, err }, 'aunu: skipped an unreadable message entry while loading the store');
|
|
361
|
+
}
|
|
312
362
|
}
|
|
313
363
|
}
|
|
314
364
|
};
|
|
@@ -329,6 +379,16 @@ const makeInMemoryStore = ({ logger: _logger, chatKey, labelAssociationKey }) =>
|
|
|
329
379
|
const cursorKey = !!cursor ? ('before' in cursor ? cursor.before : cursor.after) : undefined;
|
|
330
380
|
const cursorValue = cursorKey ? list.get(cursorKey.id) : undefined;
|
|
331
381
|
let messages;
|
|
382
|
+
// AunuBaileys (1.0.84, HB-177): a count of 0 means
|
|
383
|
+
// "no messages" — the slicing below used
|
|
384
|
+
// slice(-count), and slice(-0) is slice(0), so
|
|
385
|
+
// loadMessages(jid, 0) returned the chat's ENTIRE
|
|
386
|
+
// history (and negative counts mis-sliced as well).
|
|
387
|
+
// Every other path in this function already resolves
|
|
388
|
+
// to []. An absent count keeps the legacy behaviour.
|
|
389
|
+
if (typeof count === 'number' && count <= 0) {
|
|
390
|
+
return [];
|
|
391
|
+
}
|
|
332
392
|
if (list && mode === 'before' && (!cursorKey || cursorValue)) {
|
|
333
393
|
if (cursorValue) {
|
|
334
394
|
const msgIdx = list.array.findIndex(m => m.key.id === (cursorKey === null || cursorKey === void 0 ? void 0 : cursorKey.id));
|
|
@@ -386,7 +446,13 @@ const makeInMemoryStore = ({ logger: _logger, chatKey, labelAssociationKey }) =>
|
|
|
386
446
|
if (!contact) {
|
|
387
447
|
return sock === null || sock === void 0 ? void 0 : sock.profilePictureUrl(jid);
|
|
388
448
|
}
|
|
389
|
-
|
|
449
|
+
// AunuBaileys (1.0.84, HB-245): a store file written
|
|
450
|
+
// before this fix can still carry the 'changed' /
|
|
451
|
+
// 'removed' sentinel as imgUrl — treat it as no
|
|
452
|
+
// cached URL and fetch fresh; never hand a sentinel
|
|
453
|
+
// back as an avatar URL. A real cached URL is still
|
|
454
|
+
// returned as-is (and a fresh fetch re-caches it).
|
|
455
|
+
if (typeof contact.imgUrl === 'undefined' || contact.imgUrl === 'changed' || contact.imgUrl === 'removed') {
|
|
390
456
|
contact.imgUrl = await (sock === null || sock === void 0 ? void 0 : sock.profilePictureUrl(jid));
|
|
391
457
|
}
|
|
392
458
|
return contact.imgUrl;
|
|
@@ -419,7 +485,17 @@ const makeInMemoryStore = ({ logger: _logger, chatKey, labelAssociationKey }) =>
|
|
|
419
485
|
writeToFile: (path) => {
|
|
420
486
|
// require fs here so that in case "fs" is not available -- the app does not crash
|
|
421
487
|
|
|
422
|
-
|
|
488
|
+
// AunuBaileys (1.0.84, HB-120 + addendum): every
|
|
489
|
+
// other persistence writer in the lib is atomic and
|
|
490
|
+
// 0600 (auth files HB-116, the lid store); this one
|
|
491
|
+
// wrote the full chat/contact/message history
|
|
492
|
+
// world-readable AND non-atomically — a crash
|
|
493
|
+
// mid-write left a truncated file the next boot read
|
|
494
|
+
// as corrupt and discarded wholesale. Write a
|
|
495
|
+
// per-pid temp file, then rename it over the target.
|
|
496
|
+
const tmpPath = `${path}.tmp-${process.pid}`;
|
|
497
|
+
writeFileSync(tmpPath, JSON.stringify(toJSON()), { mode: 0o600 });
|
|
498
|
+
renameSync(tmpPath, path);
|
|
423
499
|
},
|
|
424
500
|
readFromFile: (path) => {
|
|
425
501
|
// require fs here so that in case "fs" is not available -- the app does not crash
|
package/lib/Utils/auth-utils.js
CHANGED
|
@@ -16,11 +16,35 @@ export function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
|
16
16
|
new NodeCache({
|
|
17
17
|
stdTTL: DEFAULT_CACHE_TTLS.SIGNAL_STORE, // 5 minutes
|
|
18
18
|
useClones: false,
|
|
19
|
-
deleteOnExpire: true
|
|
19
|
+
deleteOnExpire: true,
|
|
20
|
+
// AunuBaileys (1.0.84, HB-199): a TTL bounds an
|
|
21
|
+
// entry's AGE, not the entry COUNT — a burst of
|
|
22
|
+
// distinct sender addresses inside the 5-minute
|
|
23
|
+
// window grew this cache without limit (the exact
|
|
24
|
+
// warning written in bounded-cache.js). Cap the
|
|
25
|
+
// number of keys as well. NOTE: NodeCache's maxKeys
|
|
26
|
+
// does not evict — set() THROWS once the cap is hit —
|
|
27
|
+
// so every cache write below goes through cacheSet,
|
|
28
|
+
// which swallows that one error: the underlying store
|
|
29
|
+
// is authoritative, a skipped cache entry only costs
|
|
30
|
+
// a re-read, and the cap can never break a session
|
|
31
|
+
// operation.
|
|
32
|
+
maxKeys: 1000
|
|
20
33
|
});
|
|
21
34
|
function getUniqueId(type, id) {
|
|
22
35
|
return `${type}.${id}`;
|
|
23
36
|
}
|
|
37
|
+
// AunuBaileys (1.0.84, HB-199): see the maxKeys note above —
|
|
38
|
+
// a full cache must degrade to "not cached", never to a throw
|
|
39
|
+
// escaping into get()/set().
|
|
40
|
+
function cacheSet(key, value) {
|
|
41
|
+
try {
|
|
42
|
+
cache.set(key, value);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// cache is at its key cap; the store stays authoritative
|
|
46
|
+
}
|
|
47
|
+
}
|
|
24
48
|
return {
|
|
25
49
|
async get(type, ids) {
|
|
26
50
|
const data = {};
|
|
@@ -41,7 +65,7 @@ export function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
|
41
65
|
const item = fetched[id];
|
|
42
66
|
if (item) {
|
|
43
67
|
data[id] = item;
|
|
44
|
-
|
|
68
|
+
cacheSet(getUniqueId(type, id), item);
|
|
45
69
|
}
|
|
46
70
|
}
|
|
47
71
|
}
|
|
@@ -67,7 +91,7 @@ export function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
|
67
91
|
cache.del(getUniqueId(type, id));
|
|
68
92
|
}
|
|
69
93
|
else {
|
|
70
|
-
|
|
94
|
+
cacheSet(getUniqueId(type, id), data[type][id]);
|
|
71
95
|
}
|
|
72
96
|
keys += 1;
|
|
73
97
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import EventEmitter from 'events';
|
|
2
2
|
import { createReadStream } from 'fs';
|
|
3
|
-
import { writeFile } from 'fs/promises';
|
|
3
|
+
import { rename, writeFile } from 'fs/promises';
|
|
4
4
|
import { createInterface } from 'readline';
|
|
5
5
|
import { delay } from './generics.js';
|
|
6
6
|
import { makeMutex } from './make-mutex.js';
|
|
@@ -13,13 +13,48 @@ export const captureEventStream = (ev, filename) => {
|
|
|
13
13
|
const oldEmit = ev.emit;
|
|
14
14
|
// write mutex so data is appended in order
|
|
15
15
|
const writeMutex = makeMutex();
|
|
16
|
+
// AunuBaileys (1.0.84, HB-247): a creds.update payload is the
|
|
17
|
+
// full credential material (private noise / identity /
|
|
18
|
+
// signed-pre-key bytes, the adv secret). The capture below
|
|
19
|
+
// redacts it to the NAMES of the fields that changed, creates
|
|
20
|
+
// the file 0600, and rotates it to `<filename>.1` once per
|
|
21
|
+
// 10 MiB so a long-running debug capture cannot grow (or
|
|
22
|
+
// leak) without bound.
|
|
23
|
+
let capturedBytes = 0;
|
|
24
|
+
const MAX_CAPTURE_BYTES = 10 * 1024 * 1024;
|
|
16
25
|
// monkey patch eventemitter to capture all events
|
|
17
26
|
ev.emit = function (...args) {
|
|
18
|
-
const content = JSON.stringify({ timestamp: Date.now(), event: args[0], data: args[1] }) + '\n';
|
|
19
27
|
const result = oldEmit.apply(ev, args);
|
|
28
|
+
let content;
|
|
29
|
+
try {
|
|
30
|
+
const data = args[0] === 'creds.update' && args[1] && typeof args[1] === 'object'
|
|
31
|
+
? { redacted: 'creds.update payload withheld (credential material)', updatedFields: Object.keys(args[1]) }
|
|
32
|
+
: args[1];
|
|
33
|
+
content = JSON.stringify({ timestamp: Date.now(), event: args[0], data }) + '\n';
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// AunuBaileys (1.0.84, HB-129 addendum): an
|
|
37
|
+
// unserialisable (circular) payload used to throw
|
|
38
|
+
// synchronously out of the patched emit — into
|
|
39
|
+
// whatever lib code had emitted the event — because
|
|
40
|
+
// the stringify ran before oldEmit. Serialisation now
|
|
41
|
+
// happens after the real emit, and a payload that
|
|
42
|
+
// cannot be serialised is simply not captured.
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
// AunuBaileys (1.0.84, HB-129): the mutex promise used to
|
|
46
|
+
// be discarded — a failed write (deleted directory, full
|
|
47
|
+
// disk, permissions) rejected with no handler attached
|
|
48
|
+
// and crashed the host process (Node >= 15 default). A
|
|
49
|
+
// debug capture must never take the host down with it.
|
|
20
50
|
writeMutex.mutex(async () => {
|
|
21
|
-
|
|
22
|
-
|
|
51
|
+
capturedBytes += Buffer.byteLength(content);
|
|
52
|
+
if (capturedBytes > MAX_CAPTURE_BYTES) {
|
|
53
|
+
capturedBytes = Buffer.byteLength(content);
|
|
54
|
+
await rename(filename, `${filename}.1`).catch(() => { });
|
|
55
|
+
}
|
|
56
|
+
await writeFile(filename, content, { flag: 'a', mode: 0o600 });
|
|
57
|
+
}).catch(() => { });
|
|
23
58
|
return result;
|
|
24
59
|
};
|
|
25
60
|
};
|
|
@@ -45,11 +45,25 @@ export const makeEventBuffer = (logger) => {
|
|
|
45
45
|
// had to run. Log the error and keep dispatching the
|
|
46
46
|
// remaining events of the batch; the event itself
|
|
47
47
|
// still fired, with its payload unchanged.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
//
|
|
49
|
+
// AunuBaileys (1.0.84, HB-264): dispatch PER LISTENER.
|
|
50
|
+
// A single ev.emit stops at the first listener that
|
|
51
|
+
// throws, so every listener registered AFTER the
|
|
52
|
+
// thrower silently missed that emit — a throwing
|
|
53
|
+
// logging listener registered before the host's
|
|
54
|
+
// saveCreds made that credential update vanish.
|
|
55
|
+
// rawListeners returns the once-wrappers themselves,
|
|
56
|
+
// so once semantics are preserved, and each listener
|
|
57
|
+
// gets its own guard: a throw now costs only the
|
|
58
|
+
// listener that threw.
|
|
59
|
+
const listeners = ev.rawListeners(event);
|
|
60
|
+
for (const listener of listeners) {
|
|
61
|
+
try {
|
|
62
|
+
listener.call(ev, map[event]);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
logger.error({ err, event }, 'error in event listener — dispatch continues');
|
|
66
|
+
}
|
|
53
67
|
}
|
|
54
68
|
}
|
|
55
69
|
});
|
|
@@ -65,6 +79,17 @@ export const makeEventBuffer = (logger) => {
|
|
|
65
79
|
}
|
|
66
80
|
logger.debug('Flushing event buffer');
|
|
67
81
|
isBuffering = false;
|
|
82
|
+
// AunuBaileys (1.0.84, HB-130 + addendum): the history
|
|
83
|
+
// dedup set only has to live for ONE buffering session.
|
|
84
|
+
// It used to survive every flush (cleared only in
|
|
85
|
+
// destroy()), so a second history sync inside the same
|
|
86
|
+
// socket lifetime silently dropped every already-known
|
|
87
|
+
// chat — and every already-known message key, even when
|
|
88
|
+
// that message's content had changed: fresh unread
|
|
89
|
+
// counts and edits never reached the store. Dedup for
|
|
90
|
+
// THIS session's appends already happened above; the
|
|
91
|
+
// next session starts empty.
|
|
92
|
+
historyCache.clear();
|
|
68
93
|
const newData = makeBufferData();
|
|
69
94
|
const chatUpdates = Object.values(data.chatUpdates);
|
|
70
95
|
let conditionalChatUpdatesLeft = 0;
|
|
@@ -305,7 +330,14 @@ eventData, logger) {
|
|
|
305
330
|
data.contactUpserts[contact.id] = upsert;
|
|
306
331
|
}
|
|
307
332
|
if (data.contactUpdates[contact.id]) {
|
|
308
|
-
|
|
333
|
+
// AunuBaileys (1.0.84, HB-182): the pending
|
|
334
|
+
// update merges INTO the upsert. The old
|
|
335
|
+
// direction merged the contact into the pending
|
|
336
|
+
// update object and then deleted that object —
|
|
337
|
+
// the merged result went nowhere, and fields
|
|
338
|
+
// only the update carried (notify, verifiedName)
|
|
339
|
+
// silently vanished from the flushed upsert.
|
|
340
|
+
Object.assign(upsert, data.contactUpdates[contact.id]);
|
|
309
341
|
delete data.contactUpdates[contact.id];
|
|
310
342
|
}
|
|
311
343
|
}
|
package/lib/Utils/generics.js
CHANGED
|
@@ -46,7 +46,26 @@ export const BufferJSON = {
|
|
|
46
46
|
reviver: (_, value) => {
|
|
47
47
|
if (typeof value === 'object' && !!value && (value.buffer === true || value.type === 'Buffer')) {
|
|
48
48
|
const val = value.data || value.value;
|
|
49
|
-
|
|
49
|
+
if (typeof val === 'string') {
|
|
50
|
+
// AunuBaileys (1.0.84, HB-143): Node's base64
|
|
51
|
+
// decoder is lenient — it silently DROPS
|
|
52
|
+
// characters outside the alphabet, so a garbage
|
|
53
|
+
// or hand-mangled string in a creds/key file
|
|
54
|
+
// decoded to wrong-length, wrong-value key bytes
|
|
55
|
+
// with no error anywhere, and the corruption only
|
|
56
|
+
// surfaced later as baffling Curve / handshake
|
|
57
|
+
// failures. The replacer above only ever produces
|
|
58
|
+
// canonical base64; reject anything else loudly.
|
|
59
|
+
// Both in-tree readers contain the throw: the
|
|
60
|
+
// auth-state reader logs a corrupt file and starts
|
|
61
|
+
// clean, and the sender-key reader falls back to
|
|
62
|
+
// an empty (self-healing) record.
|
|
63
|
+
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(val)) {
|
|
64
|
+
throw new Error('BufferJSON: invalid base64 in serialized Buffer data');
|
|
65
|
+
}
|
|
66
|
+
return Buffer.from(val, 'base64');
|
|
67
|
+
}
|
|
68
|
+
return Buffer.from(val || []);
|
|
50
69
|
}
|
|
51
70
|
return value;
|
|
52
71
|
}
|
package/lib/Utils/lid-store.js
CHANGED
|
@@ -169,7 +169,10 @@ export const makeLIDStore = (options = {}) => {
|
|
|
169
169
|
// AunuBaileys (1.0.70): atomic tmp+rename — a crash mid-write
|
|
170
170
|
// used to leave a truncated store file behind.
|
|
171
171
|
const tmpPath = `${path}.tmp-${process.pid}`;
|
|
172
|
-
|
|
172
|
+
// AunuBaileys (1.0.84, HB-120): 0600, like the auth
|
|
173
|
+
// files (HB-116) — this file maps phone numbers to
|
|
174
|
+
// LIDs and must not land world-readable.
|
|
175
|
+
writeFileSync(tmpPath, JSON.stringify(Object.fromEntries(pnToLid)), { mode: 0o600 });
|
|
173
176
|
renameSync(tmpPath, path);
|
|
174
177
|
},
|
|
175
178
|
loadFromFile: (path) => {
|
package/lib/Utils/messages.js
CHANGED
|
@@ -603,6 +603,18 @@ const withAunuButtonIcon = (button) => {
|
|
|
603
603
|
buttonParamsJson: typeof params === 'string' ? params : JSON.stringify(params ?? {})
|
|
604
604
|
};
|
|
605
605
|
};
|
|
606
|
+
// AunuBaileys (1.0.83, HB-254): delete / pin / edit each carry a
|
|
607
|
+
// message KEY, and WAProto.Message.fromObject silently DROPS a key
|
|
608
|
+
// whose value is undefined — so { delete: undefined } used to relay
|
|
609
|
+
// a key-less REVOKE (pin and edit likewise) that "succeeded" but
|
|
610
|
+
// could never take effect, because the server had no key to act
|
|
611
|
+
// on. Mirror the pollUpdate branch's discipline: the argument
|
|
612
|
+
// must be a real key object carrying a message id.
|
|
613
|
+
const assertMessageKeyArg = (value, field) => {
|
|
614
|
+
if (!value || typeof value !== 'object' || Array.isArray(value) || typeof value.id !== 'string' || !value.id) {
|
|
615
|
+
throw new Boom(`"${field}" requires a valid message key (an object with a message id)`, { statusCode: 400 });
|
|
616
|
+
}
|
|
617
|
+
};
|
|
606
618
|
export const generateWAMessageContent = async (message, options) => {
|
|
607
619
|
var _a, _b;
|
|
608
620
|
let m = {};
|
|
@@ -610,8 +622,13 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
610
622
|
// straight through to generateWAMessage() unprocessed (donor:
|
|
611
623
|
// @violetix/baileys). The caller owns the proto shape entirely.
|
|
612
624
|
if (message && typeof message === 'object' && 'raw' in message && !!message.raw) {
|
|
613
|
-
|
|
614
|
-
|
|
625
|
+
// AunuBaileys (1.0.83, HB-165): work on a copy — the object
|
|
626
|
+
// belongs to the caller, and deleting `raw` from it made a
|
|
627
|
+
// second send of the same object silently lose the raw
|
|
628
|
+
// passthrough entirely.
|
|
629
|
+
const rawContent = { ...message };
|
|
630
|
+
delete rawContent.raw;
|
|
631
|
+
return rawContent;
|
|
615
632
|
}
|
|
616
633
|
// AunuBaileys: rich response — blok kode / tabel / tautan (dari @itsliaaa/baileys)
|
|
617
634
|
if ('code' in message || 'links' in message || 'table' in message || 'richResponse' in message || 'html' in message || 'richHtml' in message) {
|
|
@@ -664,12 +681,19 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
664
681
|
m.locationMessage = WAProto.Message.LocationMessage.fromObject(message.location);
|
|
665
682
|
}
|
|
666
683
|
else if ('react' in message) {
|
|
667
|
-
|
|
668
|
-
|
|
684
|
+
// AunuBaileys (1.0.83, HB-165): stamp the timestamp on a
|
|
685
|
+
// copy — the caller's react object used to keep it, so a
|
|
686
|
+
// re-send of the same object carried the first send's
|
|
687
|
+
// (stale) timestamp. A non-object react keeps the original
|
|
688
|
+
// behaviour (the deref below throws, caller-visible).
|
|
689
|
+
const react = message.react && typeof message.react === 'object' ? { ...message.react } : message.react;
|
|
690
|
+
if (!react.senderTimestampMs) {
|
|
691
|
+
react.senderTimestampMs = Date.now();
|
|
669
692
|
}
|
|
670
|
-
m.reactionMessage = WAProto.Message.ReactionMessage.fromObject(
|
|
693
|
+
m.reactionMessage = WAProto.Message.ReactionMessage.fromObject(react);
|
|
671
694
|
}
|
|
672
695
|
else if ('delete' in message) {
|
|
696
|
+
assertMessageKeyArg(message.delete, 'delete');
|
|
673
697
|
m.protocolMessage = {
|
|
674
698
|
key: message.delete,
|
|
675
699
|
type: WAProto.Message.ProtocolMessage.Type.REVOKE
|
|
@@ -706,6 +730,7 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
706
730
|
}
|
|
707
731
|
}
|
|
708
732
|
else if ('pin' in message) {
|
|
733
|
+
assertMessageKeyArg(message.pin, 'pin');
|
|
709
734
|
m.pinInChatMessage = {};
|
|
710
735
|
m.messageContextInfo = {};
|
|
711
736
|
m.pinInChatMessage.key = message.pin;
|
|
@@ -729,6 +754,13 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
729
754
|
type: proto.Message.ButtonsResponseMessage.Type.DISPLAY_TEXT
|
|
730
755
|
};
|
|
731
756
|
break;
|
|
757
|
+
default:
|
|
758
|
+
// AunuBaileys (1.0.83, HB-253 family): an unknown
|
|
759
|
+
// reply type used to fall through the switch,
|
|
760
|
+
// leaving m empty — a content-less message was
|
|
761
|
+
// relayed as if the reply had been sent. Fail
|
|
762
|
+
// loudly instead.
|
|
763
|
+
throw new Boom(`Unknown buttonReply type: ${message.type} (expected 'template' or 'plain')`, { statusCode: 400 });
|
|
732
764
|
}
|
|
733
765
|
}
|
|
734
766
|
else if ('ptv' in message && message.ptv) {
|
|
@@ -814,6 +846,10 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
814
846
|
throw new Boom('Event responses (RSVPs) are sent as encEventResponseMessage (Message field 76), encrypted with the event creation key — not yet implemented in AunuBaileys', { statusCode: 501 });
|
|
815
847
|
}
|
|
816
848
|
else if ('poll' in message) {
|
|
849
|
+
// AunuBaileys (1.0.83, HB-165): the default stamps below
|
|
850
|
+
// (selectableCount / toAnnouncementGroup) land on a copy of
|
|
851
|
+
// the caller's content, not on the caller's poll object.
|
|
852
|
+
message = { ...message, poll: { ...message.poll } };
|
|
817
853
|
(_a = message.poll).selectableCount || (_a.selectableCount = 0);
|
|
818
854
|
(_b = message.poll).toAnnouncementGroup || (_b.toAnnouncementGroup = false);
|
|
819
855
|
if (!Array.isArray(message.poll.values)) {
|
|
@@ -1190,7 +1226,10 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
1190
1226
|
};
|
|
1191
1227
|
}
|
|
1192
1228
|
m = { groupStatusMessageV2: { message: m } };
|
|
1193
|
-
|
|
1229
|
+
// AunuBaileys (1.0.83, HB-165): the flag used to be deleted
|
|
1230
|
+
// from the CALLER's object here — a re-send of the same
|
|
1231
|
+
// content object silently lost its group-status wrapping.
|
|
1232
|
+
// The flag is already consumed into m; leave it in place.
|
|
1194
1233
|
}
|
|
1195
1234
|
// AunuBaileys (1.0.71): spoiler — set contextInfo.isSpoiler and wrap
|
|
1196
1235
|
// the message into spoilerMessage (Message field 118).
|
|
@@ -1206,7 +1245,9 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
1206
1245
|
};
|
|
1207
1246
|
}
|
|
1208
1247
|
m = { spoilerMessage: { message: m } };
|
|
1209
|
-
|
|
1248
|
+
// AunuBaileys (1.0.83, HB-165): same caller-object mutation
|
|
1249
|
+
// as the groupStatus block above — the flag stays on the
|
|
1250
|
+
// caller's object, so a re-send wraps identically.
|
|
1210
1251
|
}
|
|
1211
1252
|
// AunuBaileys: interactiveAsTemplate (dari @itsliaaa/baileys) — bungkus interactiveMessage
|
|
1212
1253
|
// ke templateMessage; messageContextInfo (deviceListMetadata) dipertahankan di sisinya
|
|
@@ -1223,6 +1264,7 @@ export const generateWAMessageContent = async (message, options) => {
|
|
|
1223
1264
|
};
|
|
1224
1265
|
}
|
|
1225
1266
|
if ('edit' in message) {
|
|
1267
|
+
assertMessageKeyArg(message.edit, 'edit');
|
|
1226
1268
|
m = {
|
|
1227
1269
|
protocolMessage: {
|
|
1228
1270
|
key: message.edit,
|
package/lib/Utils/signal.d.ts
CHANGED
|
@@ -18,7 +18,7 @@ export declare const xmppPreKey: (pair: KeyPair, id: number) => BinaryNode;
|
|
|
18
18
|
export declare const parseAndInjectE2ESessions: (node: BinaryNode, repository: SignalRepository) => Promise<void>;
|
|
19
19
|
/** AunuBaileys: session parsed from a retry receipt's embedded key bundle, or undefined when absent. */
|
|
20
20
|
export declare const parseRetryReceiptSession: (node: BinaryNode) => { registrationId: number; identityKey: Buffer; signedPreKey?: { keyId: number; publicKey: Buffer; signature?: Buffer }; preKey?: { keyId: number; publicKey: Buffer } } | undefined;
|
|
21
|
-
export declare const extractDeviceJids: (result: USyncQueryResultList[], myJid: string, excludeZeroDevices: boolean) => JidWithDevice[];
|
|
21
|
+
export declare const extractDeviceJids: (result: USyncQueryResultList[], myJid: string, myLid: string | undefined, excludeZeroDevices: boolean) => JidWithDevice[];
|
|
22
22
|
/**
|
|
23
23
|
* get the next N keys for upload or processing
|
|
24
24
|
* @param count number of pre-keys to get or generate
|
package/lib/Utils/signal.js
CHANGED
|
@@ -184,8 +184,19 @@ export const parseAndInjectE2ESessions = async (node, repository, logger) => {
|
|
|
184
184
|
}));
|
|
185
185
|
}
|
|
186
186
|
};
|
|
187
|
-
export const extractDeviceJids = (result, myJid, excludeZeroDevices) => {
|
|
187
|
+
export const extractDeviceJids = (result, myJid, myLid, excludeZeroDevices) => {
|
|
188
188
|
const { user: myUser, device: myDevice } = jidDecode(myJid) || {};
|
|
189
|
+
// AunuBaileys (1.0.83, HB-258): our own devices exist under TWO
|
|
190
|
+
// identities — the PN user and the LID user. Comparing only the
|
|
191
|
+
// PN user left our own LID devices in the extracted list, so a
|
|
192
|
+
// group send encrypted and delivered a copy to our own LID
|
|
193
|
+
// device (self-delivery plus wasted encryption). Upstream v7
|
|
194
|
+
// added the same myLid parameter; the comparison below decodes
|
|
195
|
+
// it to its user first, so the current device is excluded
|
|
196
|
+
// under BOTH identities while our other devices stay listed
|
|
197
|
+
// (group sender-key distribution and the DM echo still need
|
|
198
|
+
// them, exactly like the PN side).
|
|
199
|
+
const myLidUser = myLid ? jidDecode(myLid)?.user : undefined;
|
|
189
200
|
const extracted = [];
|
|
190
201
|
for (const userResult of result) {
|
|
191
202
|
const { devices, id } = userResult;
|
|
@@ -197,7 +208,7 @@ export const extractDeviceJids = (result, myJid, excludeZeroDevices) => {
|
|
|
197
208
|
if (Array.isArray(deviceList)) {
|
|
198
209
|
for (const { id: device, keyIndex } of deviceList) {
|
|
199
210
|
if ((!excludeZeroDevices || device !== 0) && // if zero devices are not-excluded, or device is non zero
|
|
200
|
-
(myUser !== user || myDevice !== device) && // either different user or if me user, not this device
|
|
211
|
+
((myUser !== user && myLidUser !== user) || myDevice !== device) && // either a different user (neither our PN nor our LID) or if me user, not this device
|
|
201
212
|
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
|
|
202
213
|
(device === 0 || !!keyIndex) // ensure that "key-index" is specified for "non-zero" devices, produces a bad req otherwise
|
|
203
214
|
) {
|
package/package.json
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@aunucore/baileys",
|
|
3
3
|
"type": "module",
|
|
4
|
-
"version": "1.0.
|
|
4
|
+
"version": "1.0.84",
|
|
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",
|