davexbaileys 2.5.21 → 2.5.23

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.
@@ -17,11 +17,17 @@ const WAUSync_1 = require("../WAUSync");
17
17
  const groups_1 = require("./groups");
18
18
  const newsletter_1 = require("./newsletter");
19
19
  const GiftedStatus = require("./gcstatus");
20
+ const tc_token_utils_1 = require("../Utils/tc-token-utils");
21
+ const lid_mapping_1 = require("../Utils/lid-mapping");
20
22
  // Permanently blacklist device JIDs that WhatsApp rejects with not-acceptable.
21
23
  // These are typically stale companion devices. Avoids repeated failed IQ queries.
22
24
  const _deadDeviceJids = new Set();
23
25
 
24
26
  const makeMessagesSocket = (config) => {
27
+ // Per-socket set of tctoken storage JIDs with a fire-and-forget `issuePrivacyTokens` IQ in flight.
28
+ // Prevents duplicate IQs from rapid back-to-back sends before `senderTimestamp` persists.
29
+ // Scoped per-socket so multi-account processes don't have one socket suppress another's issuance.
30
+ const _inFlightTcTokenIssuance = new Set();
25
31
  const {
26
32
  logger,
27
33
  linkPreviewImageThumbnailWidth,
@@ -45,6 +51,23 @@ const makeMessagesSocket = (config) => {
45
51
  groupMetadata,
46
52
  groupToggleEphemeral,
47
53
  } = sock;
54
+ // Adapter: globalLidMapping uses sync getLidFromPn; tc-token utils expect async (jid) => Promise<string|null>
55
+ const _getLIDForPN = async (pn) => {
56
+ try {
57
+ const lid = lid_mapping_1.globalLidMapping.getLidFromPn(pn);
58
+ return lid || null;
59
+ } catch (_) {
60
+ return null;
61
+ }
62
+ };
63
+ const _getPNForLID = async (lid) => {
64
+ try {
65
+ const pn = lid_mapping_1.globalLidMapping.getPnFromLid(lid);
66
+ return pn || null;
67
+ } catch (_) {
68
+ return null;
69
+ }
70
+ };
48
71
  const userDevicesCache =
49
72
  config.userDevicesCache ||
50
73
  new node_cache_1.default({
@@ -755,6 +778,44 @@ const makeMessagesSocket = (config) => {
755
778
  });
756
779
  logger.debug({ jid }, "adding device identity");
757
780
  }
781
+ // tc-token attachment: WA Web attaches a stored trusted-contact token to outgoing 1:1 messages
782
+ // WA Web never attaches tctoken to peer (AppStateSync) messages — server rejects with 479
783
+ const _isPeerMessage = (additionalAttributes && additionalAttributes["category"] === "peer");
784
+ const _isRetryResend = !!participant;
785
+ const _is1on1Send = !isGroup && !_isRetryResend && !isStatus && !isNewsletter && !_isPeerMessage;
786
+ let _tcTokenJid = destinationJid;
787
+ let _existingTokenEntry;
788
+ try {
789
+ if (_is1on1Send) {
790
+ _tcTokenJid = await (0, tc_token_utils_1.resolveTcTokenJid)(destinationJid, _getLIDForPN);
791
+ const _contactTcTokenData = await authState.keys.get("tctoken", [_tcTokenJid]);
792
+ _existingTokenEntry = _contactTcTokenData && _contactTcTokenData[_tcTokenJid];
793
+ let _tcTokenBuffer = _existingTokenEntry && _existingTokenEntry.token;
794
+ // Treat expired tokens the same as missing — clear from cache
795
+ if (_tcTokenBuffer && _tcTokenBuffer.length && (0, tc_token_utils_1.isTcTokenExpired)(_existingTokenEntry && _existingTokenEntry.timestamp)) {
796
+ logger.debug({ jid: destinationJid, timestamp: _existingTokenEntry && _existingTokenEntry.timestamp }, "tctoken expired, clearing");
797
+ _tcTokenBuffer = undefined;
798
+ // Preserve senderTimestamp so the fire-and-forget issuance dedupe survives cleanup.
799
+ const _cleared = _existingTokenEntry && _existingTokenEntry.senderTimestamp !== undefined
800
+ ? { token: Buffer.alloc(0), senderTimestamp: _existingTokenEntry.senderTimestamp }
801
+ : null;
802
+ try {
803
+ await authState.keys.set({ tctoken: { [_tcTokenJid]: _cleared } });
804
+ } catch (err) {
805
+ logger.debug({ jid: destinationJid, err: err && err.message }, "failed to persist tctoken expiry cleanup");
806
+ }
807
+ }
808
+ if (_tcTokenBuffer && _tcTokenBuffer.length) {
809
+ stanza.content.push({
810
+ tag: "tctoken",
811
+ attrs: {},
812
+ content: _tcTokenBuffer
813
+ });
814
+ }
815
+ }
816
+ } catch (err) {
817
+ logger.debug({ jid: destinationJid, err: err && err.message }, "tctoken attachment failed");
818
+ }
758
819
  if (additionalNodes && additionalNodes.length > 0) {
759
820
  stanza.content.push(...additionalNodes);
760
821
  }
@@ -763,6 +824,52 @@ const makeMessagesSocket = (config) => {
763
824
  `sending message to ${participants.length} devices`,
764
825
  );
765
826
  await sendNode(stanza);
827
+ // Fire-and-forget: issue our token to the contact AFTER message send.
828
+ // WA Web skips protocol messages and PSA/bot contacts (TcTokenChatAction: isRegularUser)
829
+ try {
830
+ const _normalized = (0, Utils_1.normalizeMessageContent)(message);
831
+ const _isProtocolMsg = !!(_normalized && _normalized.protocolMessage);
832
+ const _isBotOrPSA = destinationJid === WABinary_1.PSA_WID
833
+ || (0, WABinary_1.isJidBot)(destinationJid)
834
+ || (0, WABinary_1.isJidMetaIa)(destinationJid);
835
+ if (
836
+ _is1on1Send &&
837
+ !_isProtocolMsg &&
838
+ !_isBotOrPSA &&
839
+ (0, tc_token_utils_1.shouldSendNewTcToken)(_existingTokenEntry && _existingTokenEntry.senderTimestamp) &&
840
+ !_inFlightTcTokenIssuance.has(_tcTokenJid)
841
+ ) {
842
+ _inFlightTcTokenIssuance.add(_tcTokenJid);
843
+ const _issueTimestamp = (0, Utils_1.unixTimestampSeconds)();
844
+ (0, tc_token_utils_1.resolveIssuanceJid)(destinationJid, false, _getLIDForPN, _getPNForLID)
845
+ .then((issueJid) => issuePrivacyTokens([issueJid], _issueTimestamp))
846
+ .then(async (result) => {
847
+ await (0, tc_token_utils_1.storeTcTokensFromIqResult)({
848
+ result,
849
+ fallbackJid: _tcTokenJid,
850
+ keys: authState.keys,
851
+ getLIDForPN: _getLIDForPN
852
+ });
853
+ const _currentData = await authState.keys.get("tctoken", [_tcTokenJid]);
854
+ const _currentEntry = _currentData && _currentData[_tcTokenJid];
855
+ const _indexWrite = await (0, tc_token_utils_1.buildMergedTcTokenIndexWrite)(authState.keys, [_tcTokenJid]);
856
+ await authState.keys.set({
857
+ tctoken: Object.assign(
858
+ { [_tcTokenJid]: Object.assign({ token: Buffer.alloc(0) }, _currentEntry, { senderTimestamp: _issueTimestamp }) },
859
+ _indexWrite
860
+ )
861
+ });
862
+ })
863
+ .catch((err) => {
864
+ logger.debug({ jid: destinationJid, err: err && err.message }, "fire-and-forget tctoken issuance failed");
865
+ })
866
+ .finally(() => {
867
+ _inFlightTcTokenIssuance.delete(_tcTokenJid);
868
+ });
869
+ }
870
+ } catch (err) {
871
+ logger.debug({ jid: destinationJid, err: err && err.message }, "tctoken issuance gate failed");
872
+ }
766
873
  });
767
874
  return msgId;
768
875
  };
@@ -816,8 +923,10 @@ const makeMessagesSocket = (config) => {
816
923
  return "text";
817
924
  }
818
925
  };
819
- const getPrivacyTokens = async (jids) => {
820
- const t = (0, Utils_1.unixTimestampSeconds)().toString();
926
+ const issuePrivacyTokens = async (jids, timestamp) => {
927
+ const t = (timestamp !== undefined && timestamp !== null
928
+ ? timestamp
929
+ : (0, Utils_1.unixTimestampSeconds)()).toString();
821
930
  const result = await query({
822
931
  tag: "iq",
823
932
  attrs: {
@@ -875,7 +984,8 @@ const makeMessagesSocket = (config) => {
875
984
  return {
876
985
  ...sock,
877
986
  pinMessage,
878
- getPrivacyTokens,
987
+ issuePrivacyTokens,
988
+ getPrivacyTokens: issuePrivacyTokens,
879
989
  assertSessions,
880
990
  relayMessage,
881
991
  sendReceipt,
@@ -1002,6 +1112,7 @@ const makeMessagesSocket = (config) => {
1002
1112
  }),
1003
1113
  //TODO: CACHE
1004
1114
  getProfilePicUrl: sock.profilePictureUrl,
1115
+ getCallLink: sock.createCallLink,
1005
1116
  upload: waUploadToServer,
1006
1117
  mediaCache: config.mediaCache,
1007
1118
  options: config.options,
@@ -0,0 +1,33 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.getPlatformId = exports.Browsers = void 0;
4
+ const os_1 = require("os");
5
+ const WAProto_1 = require("../../WAProto");
6
+
7
+ const PLATFORM_MAP = {
8
+ aix: 'AIX',
9
+ darwin: 'Mac OS',
10
+ win32: 'Windows',
11
+ android: 'Android',
12
+ freebsd: 'FreeBSD',
13
+ openbsd: 'OpenBSD',
14
+ sunos: 'Solaris',
15
+ linux: undefined,
16
+ haiku: undefined,
17
+ cygwin: undefined,
18
+ netbsd: undefined
19
+ };
20
+
21
+ exports.Browsers = {
22
+ ubuntu: browser => ['Ubuntu', browser, '22.04.4'],
23
+ macOS: browser => ['Mac OS', browser, '14.4.1'],
24
+ baileys: browser => ['Baileys', browser, '6.5.0'],
25
+ windows: browser => ['Windows', browser, '10.0.22631'],
26
+ appropriate: browser => [PLATFORM_MAP[(0, os_1.platform)()] || 'Ubuntu', browser, (0, os_1.release)()]
27
+ };
28
+
29
+ const getPlatformId = (browser) => {
30
+ const platformType = WAProto_1.proto.DeviceProps.PlatformType[browser.toUpperCase()];
31
+ return platformType ? platformType.toString() : '1';
32
+ };
33
+ exports.getPlatformId = getPlatformId;
@@ -55,7 +55,10 @@ const makeLtHashGenerator = ({ indexValueMap, hash }) => {
55
55
  const prevOp = indexValueMap[indexMacBase64];
56
56
  if (operation === WAProto_1.proto.SyncdMutation.SyncdOperation.REMOVE) {
57
57
  if (!prevOp) {
58
- throw new boom_1.Boom('tried remove, but no previous op', { data: { indexMac, valueMac } });
58
+ // WA Web does not throw here it logs a warning and skips the subtract.
59
+ // The missing REMOVE will cause an LTHash mismatch, which is handled
60
+ // by the MAC validation layer (snapshot recovery or retry).
61
+ return;
59
62
  }
60
63
  // remove from index value mac, since this mutation is erased
61
64
  delete indexValueMap[indexMacBase64];
@@ -90,10 +93,18 @@ const generatePatchMac = (snapshotMac, valueMacs, version, type, key) => {
90
93
  };
91
94
  const newLTHashState = () => ({ version: 0, hash: Buffer.alloc(128), indexValueMap: {} });
92
95
  exports.newLTHashState = newLTHashState;
96
+ const ensureLTHashStateVersion = (state) => {
97
+ if (typeof state.version !== 'number' || isNaN(state.version)) {
98
+ state.version = 0;
99
+ }
100
+ return state;
101
+ };
102
+ exports.ensureLTHashStateVersion = ensureLTHashStateVersion;
103
+ exports.MAX_SYNC_ATTEMPTS = 2;
93
104
  const encodeSyncdPatch = async ({ type, index, syncAction, apiVersion, operation }, myAppStateKeyId, state, getAppStateSyncKey) => {
94
105
  const key = !!myAppStateKeyId ? await getAppStateSyncKey(myAppStateKeyId) : undefined;
95
106
  if (!key) {
96
- throw new boom_1.Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { statusCode: 404 });
107
+ throw new boom_1.Boom(`myAppStateKey ("${myAppStateKeyId}") not present`, { data: { isMissingKey: true } });
97
108
  }
98
109
  const encKeyId = Buffer.from(myAppStateKeyId, 'base64');
99
110
  state = { ...state, indexValueMap: { ...state.indexValueMap } };
@@ -181,8 +192,7 @@ const decodeSyncdMutations = async (msgMutations, initialState, getAppStateSyncK
181
192
  const keyEnc = await getAppStateSyncKey(base64Key);
182
193
  if (!keyEnc) {
183
194
  throw new boom_1.Boom(`failed to find key "${base64Key}" to decode mutation`, {
184
- statusCode: 404,
185
- data: { msgMutations }
195
+ data: { isMissingKey: true, msgMutations }
186
196
  });
187
197
  }
188
198
  return mutationKeys(keyEnc.keyData);
@@ -194,7 +204,7 @@ const decodeSyncdPatch = async (msg, name, initialState, getAppStateSyncKey, onM
194
204
  const base64Key = Buffer.from(msg.keyId.id).toString('base64');
195
205
  const mainKeyObj = await getAppStateSyncKey(base64Key);
196
206
  if (!mainKeyObj) {
197
- throw new boom_1.Boom(`failed to find key "${base64Key}" to decode patch`, { statusCode: 404, data: { msg } });
207
+ throw new boom_1.Boom(`failed to find key "${base64Key}" to decode patch`, { data: { isMissingKey: true, msg } });
198
208
  }
199
209
  const mainKey = await mutationKeys(mainKeyObj.keyData);
200
210
  const mutationmacs = msg.mutations.map(mutation => mutation.record.value.blob.slice(-32));
@@ -277,7 +287,7 @@ const decodeSyncdSnapshot = async (name, snapshot, getAppStateSyncKey, minimumVe
277
287
  const base64Key = Buffer.from(snapshot.keyId.id).toString('base64');
278
288
  const keyEnc = await getAppStateSyncKey(base64Key);
279
289
  if (!keyEnc) {
280
- throw new boom_1.Boom(`failed to find key "${base64Key}" to decode mutation`);
290
+ throw new boom_1.Boom(`failed to find key "${base64Key}" to decode mutation`, { data: { isMissingKey: true } });
281
291
  }
282
292
  const result = await mutationKeys(keyEnc.keyData);
283
293
  const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey);
@@ -322,7 +332,7 @@ const decodePatches = async (name, syncds, initial, getAppStateSyncKey, options,
322
332
  const base64Key = Buffer.from(keyId.id).toString('base64');
323
333
  const keyEnc = await getAppStateSyncKey(base64Key);
324
334
  if (!keyEnc) {
325
- throw new boom_1.Boom(`failed to find key "${base64Key}" to decode mutation`);
335
+ throw new boom_1.Boom(`failed to find key "${base64Key}" to decode mutation`, { data: { isMissingKey: true } });
326
336
  }
327
337
  const result = await mutationKeys(keyEnc.keyData);
328
338
  const computedSnapshotMac = generateSnapshotMac(newState.hash, newState.version, name, result.snapshotMacKey);
@@ -595,16 +605,16 @@ const processSyncAction = (syncAction, ev, me, initialSyncOpts, logger) => {
595
605
  // 1. if the account unarchiveChats setting is true
596
606
  // a. if the chat is archived, and no further messages have been received -- simple, keep archived
597
607
  // b. if the chat was archived, and the user received messages from the other person afterwards
598
- // then the chat should be marked unarchved --
599
- // we compare the timestamp of latest message from the other person to determine this
608
+ // then the chat should be marked unarchved --
609
+ // we compare the timestamp of latest message from the other person to determine this
600
610
  // 2. if the account unarchiveChats setting is false -- then it doesn't matter,
601
- // it'll always take an app state action to mark in unarchived -- which we'll get anyway
611
+ // it'll always take an app state action to mark in unarchived -- which we'll get anyway
602
612
  const archiveAction = action === null || action === void 0 ? void 0 : action.archiveChatAction;
603
613
  const isArchived = archiveAction ? archiveAction.archived : type === 'archive';
604
614
  // // basically we don't need to fire an "archive" update if the chat is being marked unarchvied
605
615
  // // this only applies for the initial sync
606
616
  // if(isInitialSync && !isArchived) {
607
- // isArchived = false
617
+ // isArchived = false
608
618
  // }
609
619
  const msgRange = !(accountSettings === null || accountSettings === void 0 ? void 0 : accountSettings.unarchiveChats) ? undefined : archiveAction === null || archiveAction === void 0 ? void 0 : archiveAction.messageRange;
610
620
  // logger?.debug({ chat: id, syncAction }, 'message range archive')
@@ -642,11 +652,11 @@ const processSyncAction = (syncAction, ev, me, initialSyncOpts, logger) => {
642
652
  });
643
653
  }
644
654
  else if (action === null || action === void 0 ? void 0 : action.contactAction) {
645
- ev.emit('contacts.upsert', [{ id, name: action.contactAction.fullName, lid: action.contactAction.lidJid }]);
646
655
  ev.emit('contacts.upsert', [
647
656
  {
648
657
  id: id,
649
658
  name: action.contactAction.fullName,
659
+ username: action.contactAction.username || undefined,
650
660
  lid: action.contactAction.lidJid || undefined,
651
661
  jid: (0, WABinary_1.isJidUser)(id) ? id : undefined
652
662
  }
@@ -170,15 +170,22 @@ function decodeMessageNode(stanza, meId, meLid) {
170
170
  lid_mapping_1.globalLidMapping.set(participantLidValue, participantPnValue);
171
171
  }
172
172
  }
173
+ const isGroupChat = (0, WABinary_1.isJidGroup)(chatId);
174
+ const remoteJidUsername = !isGroupChat
175
+ ? (stanza.attrs.peer_recipient_username || stanza.attrs.recipient_username)
176
+ : undefined;
177
+ const participantUsername = stanza.attrs.participant ? stanza.attrs.participant_username : undefined;
173
178
  const key = {
174
179
  remoteJid: chatId,
175
180
  fromMe,
176
181
  id: msgId,
182
+ ...(remoteJidUsername ? { remoteJidUsername } : {}),
177
183
  ...(senderLidValue ? { senderLid: senderLidValue } : {}),
178
184
  ...(senderPnValue ? { senderPn: senderPnValue } : {}),
179
185
  ...(participant ? { participant } : {}),
180
186
  ...(participantPnValue ? { participantPn: participantPnValue } : {}),
181
187
  ...(participantLidValue ? { participantLid: participantLidValue } : {}),
188
+ ...(participantUsername ? { participantUsername } : {}),
182
189
  ...(msgType === 'newsletter' && stanza.attrs.server_id ? { server_id: stanza.attrs.server_id } : {})
183
190
  };
184
191
  const fullMessage = {
@@ -182,6 +182,7 @@ eventData, logger) {
182
182
  data.historySets.progress = eventData.progress;
183
183
  data.historySets.peerDataRequestSessionId = eventData.peerDataRequestSessionId;
184
184
  data.historySets.isLatest = eventData.isLatest || data.historySets.isLatest;
185
+ data.historySets.chunkOrder = eventData.chunkOrder !== undefined ? eventData.chunkOrder : data.historySets.chunkOrder;
185
186
  break;
186
187
  case 'chats.upsert':
187
188
  for (const chat of eventData) {
@@ -446,7 +447,8 @@ function consolidateEvents(data) {
446
447
  syncType: data.historySets.syncType,
447
448
  progress: data.historySets.progress,
448
449
  isLatest: data.historySets.isLatest,
449
- peerDataRequestSessionId: data.historySets.peerDataRequestSessionId
450
+ peerDataRequestSessionId: data.historySets.peerDataRequestSessionId,
451
+ chunkOrder: data.historySets.chunkOrder
450
452
  };
451
453
  }
452
454
  const chatUpsertList = Object.values(data.chatUpserts);
@@ -324,6 +324,9 @@ const getCallStatusFromNode = ({ tag, attrs }) => {
324
324
  case 'offer_notice':
325
325
  status = 'offer';
326
326
  break;
327
+ case 'preaccept':
328
+ status = 'preaccept';
329
+ break;
327
330
  case 'terminate':
328
331
  if (attrs.reason === 'timeout') {
329
332
  status = 'timeout';
@@ -339,6 +342,12 @@ const getCallStatusFromNode = ({ tag, attrs }) => {
339
342
  case 'accept':
340
343
  status = 'accept';
341
344
  break;
345
+ case 'transport':
346
+ status = 'transport';
347
+ break;
348
+ case 'relaylatency':
349
+ status = 'relaylatency';
350
+ break;
342
351
  default:
343
352
  status = 'ringing';
344
353
  break;
@@ -12,11 +12,18 @@ const messages_media_1 = require("./messages-media");
12
12
  const inflatePromise = (0, util_1.promisify)(zlib_1.inflate);
13
13
  const downloadHistory = async (msg, options) => {
14
14
  const stream = await (0, messages_media_1.downloadContentFromMessage)(msg, 'md-msg-hist', { options });
15
- const bufferArray = [];
15
+ const chunks = [];
16
+ let totalLength = 0;
16
17
  for await (const chunk of stream) {
17
- bufferArray.push(chunk);
18
+ chunks.push(chunk);
19
+ totalLength += chunk.length;
20
+ }
21
+ let buffer = Buffer.allocUnsafe(totalLength);
22
+ let offset = 0;
23
+ for (const chunk of chunks) {
24
+ chunk.copy(buffer, offset);
25
+ offset += chunk.length;
18
26
  }
19
- let buffer = Buffer.concat(bufferArray);
20
27
  // decompress buffer
21
28
  buffer = await inflatePromise(buffer);
22
29
  const syncData = WAProto_1.proto.HistorySync.decode(buffer);
@@ -34,11 +41,11 @@ const processHistoryMessage = (item) => {
34
41
  case WAProto_1.proto.HistorySync.HistorySyncType.FULL:
35
42
  case WAProto_1.proto.HistorySync.HistorySyncType.ON_DEMAND:
36
43
  for (const chat of item.conversations) {
37
- contacts.push({ id: chat.id, name: chat.name || undefined, lid: chat.lidJid });
38
44
  contacts.push({
39
45
  id: chat.id,
40
46
  name: chat.name || undefined,
41
47
  lid: chat.lidJid || undefined,
48
+ username: chat.username || undefined,
42
49
  jid: (0, WABinary_1.isJidUser)(chat.id) ? chat.id : undefined
43
50
  });
44
51
  const msgs = chat.messages || [];
@@ -33,3 +33,10 @@ __exportStar(require("./event-buffer"), exports);
33
33
  __exportStar(require("./process-message"), exports);
34
34
  __exportStar(require("./lid-mapping"), exports);
35
35
  __exportStar(require("./message-type-utils"), exports);
36
+ __exportStar(require("./stanza-ack"), exports);
37
+ __exportStar(require("./offline-node-processor"), exports);
38
+ __exportStar(require("./tc-token-utils"), exports);
39
+ __exportStar(require("./sync-action-utils"), exports);
40
+ __exportStar(require("./message-retry-manager"), exports);
41
+ __exportStar(require("./browser-utils"), exports);
42
+ __exportStar(require("./pre-key-manager"), exports);
@@ -0,0 +1,151 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.MessageRetryManager = void 0;
4
+ const lru_cache_1 = require("lru-cache");
5
+
6
+ const RECENT_MESSAGES_SIZE = 512;
7
+ const MESSAGE_KEY_SEPARATOR = '\u0000';
8
+ const RECREATE_SESSION_TIMEOUT = 60 * 60 * 1000;
9
+ const PHONE_REQUEST_DELAY = 3000;
10
+
11
+ class MessageRetryManager {
12
+ constructor(logger, maxMsgRetryCount) {
13
+ this.logger = logger;
14
+ this.recentMessagesMap = new lru_cache_1.LRUCache({
15
+ max: RECENT_MESSAGES_SIZE,
16
+ ttl: 5 * 60 * 1000,
17
+ ttlAutopurge: true,
18
+ dispose: (_value, key) => {
19
+ const separatorIndex = key.lastIndexOf(MESSAGE_KEY_SEPARATOR);
20
+ if (separatorIndex > -1) {
21
+ const messageId = key.slice(separatorIndex + MESSAGE_KEY_SEPARATOR.length);
22
+ this.messageKeyIndex.delete(messageId);
23
+ }
24
+ }
25
+ });
26
+ this.messageKeyIndex = new Map();
27
+ this.sessionRecreateHistory = new lru_cache_1.LRUCache({
28
+ ttl: RECREATE_SESSION_TIMEOUT * 2,
29
+ ttlAutopurge: true
30
+ });
31
+ this.retryCounters = new lru_cache_1.LRUCache({
32
+ ttl: 15 * 60 * 1000,
33
+ ttlAutopurge: true,
34
+ updateAgeOnGet: true
35
+ });
36
+ this.pendingPhoneRequests = {};
37
+ this.maxMsgRetryCount = 5;
38
+ this.statistics = {
39
+ totalRetries: 0,
40
+ successfulRetries: 0,
41
+ failedRetries: 0,
42
+ mediaRetries: 0,
43
+ sessionRecreations: 0,
44
+ phoneRequests: 0
45
+ };
46
+ this.maxMsgRetryCount = maxMsgRetryCount;
47
+ }
48
+
49
+ addRecentMessage(to, id, message) {
50
+ const key = { to, id };
51
+ const keyStr = this.keyToString(key);
52
+ this.recentMessagesMap.set(keyStr, {
53
+ message,
54
+ timestamp: Date.now()
55
+ });
56
+ this.messageKeyIndex.set(id, keyStr);
57
+ this.logger.debug(`Added message to retry cache: ${to}/${id}`);
58
+ }
59
+
60
+ getRecentMessage(to, id) {
61
+ const key = { to, id };
62
+ const keyStr = this.keyToString(key);
63
+ return this.recentMessagesMap.get(keyStr);
64
+ }
65
+
66
+ shouldRecreateSession(jid, retryCount, hasSession) {
67
+ if (!hasSession) {
68
+ this.sessionRecreateHistory.set(jid, Date.now());
69
+ this.statistics.sessionRecreations++;
70
+ return {
71
+ reason: "we don't have a Signal session with them",
72
+ recreate: true
73
+ };
74
+ }
75
+ if (retryCount < 2) {
76
+ return { reason: '', recreate: false };
77
+ }
78
+ const now = Date.now();
79
+ const prevTime = this.sessionRecreateHistory.get(jid);
80
+ if (!prevTime || now - prevTime > RECREATE_SESSION_TIMEOUT) {
81
+ this.sessionRecreateHistory.set(jid, now);
82
+ this.statistics.sessionRecreations++;
83
+ return {
84
+ reason: 'retry count > 1 and over an hour since last recreation',
85
+ recreate: true
86
+ };
87
+ }
88
+ return { reason: '', recreate: false };
89
+ }
90
+
91
+ incrementRetryCount(messageId) {
92
+ this.retryCounters.set(messageId, (this.retryCounters.get(messageId) || 0) + 1);
93
+ this.statistics.totalRetries++;
94
+ return this.retryCounters.get(messageId);
95
+ }
96
+
97
+ getRetryCount(messageId) {
98
+ return this.retryCounters.get(messageId) || 0;
99
+ }
100
+
101
+ hasExceededMaxRetries(messageId) {
102
+ return this.getRetryCount(messageId) >= this.maxMsgRetryCount;
103
+ }
104
+
105
+ markRetrySuccess(messageId) {
106
+ this.statistics.successfulRetries++;
107
+ this.retryCounters.delete(messageId);
108
+ this.cancelPendingPhoneRequest(messageId);
109
+ this.removeRecentMessage(messageId);
110
+ }
111
+
112
+ markRetryFailed(messageId) {
113
+ this.statistics.failedRetries++;
114
+ this.retryCounters.delete(messageId);
115
+ this.cancelPendingPhoneRequest(messageId);
116
+ this.removeRecentMessage(messageId);
117
+ }
118
+
119
+ schedulePhoneRequest(messageId, callback, delay = PHONE_REQUEST_DELAY) {
120
+ this.cancelPendingPhoneRequest(messageId);
121
+ this.pendingPhoneRequests[messageId] = setTimeout(() => {
122
+ delete this.pendingPhoneRequests[messageId];
123
+ this.statistics.phoneRequests++;
124
+ callback();
125
+ }, delay);
126
+ this.logger.debug(`Scheduled phone request for message ${messageId} with ${delay}ms delay`);
127
+ }
128
+
129
+ cancelPendingPhoneRequest(messageId) {
130
+ const timeout = this.pendingPhoneRequests[messageId];
131
+ if (timeout) {
132
+ clearTimeout(timeout);
133
+ delete this.pendingPhoneRequests[messageId];
134
+ this.logger.debug(`Cancelled pending phone request for message ${messageId}`);
135
+ }
136
+ }
137
+
138
+ keyToString(key) {
139
+ return `${key.to}${MESSAGE_KEY_SEPARATOR}${key.id}`;
140
+ }
141
+
142
+ removeRecentMessage(messageId) {
143
+ const keyStr = this.messageKeyIndex.get(messageId);
144
+ if (!keyStr) {
145
+ return;
146
+ }
147
+ this.recentMessagesMap.delete(keyStr);
148
+ this.messageKeyIndex.delete(messageId);
149
+ }
150
+ }
151
+ exports.MessageRetryManager = MessageRetryManager;
@@ -136,7 +136,7 @@ const prepareWAMessageMedia = async (message, options) => {
136
136
  }
137
137
  const requiresDurationComputation = mediaType === 'audio' && typeof uploadData.seconds === 'undefined';
138
138
  const requiresThumbnailComputation = (mediaType === 'image' || mediaType === 'video') && typeof uploadData['jpegThumbnail'] === 'undefined';
139
- const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true;
139
+ const requiresWaveformProcessing = mediaType === 'audio' && uploadData.ptt === true && typeof uploadData.waveform === 'undefined';
140
140
  const requiresAudioBackground = options.backgroundColor && mediaType === 'audio' && uploadData.ptt === true;
141
141
  const requiresOriginalForSomeProcessing = requiresDurationComputation || requiresThumbnailComputation;
142
142
  const { mediaKey, encFilePath, originalFilePath, fileEncSha256, fileSha256, fileLength } = await (0, messages_media_1.encryptedStream)(uploadData.media, options.mediaTypeOverride || mediaType, {
@@ -444,6 +444,18 @@ const generateWAMessageContent = async (message, options) => {
444
444
  else if ('requestPhoneNumber' in message) {
445
445
  m.requestPhoneNumberMessage = {};
446
446
  }
447
+ else if ('album' in message && message.album) {
448
+ m.albumMessage = {
449
+ expectedImageCount: message.album.expectedImageCount,
450
+ expectedVideoCount: message.album.expectedVideoCount
451
+ };
452
+ if (message.albumParentKey) {
453
+ m.messageContextInfo = Object.assign(Object.assign({}, m.messageContextInfo), { messageAssociation: {
454
+ associationType: Types_1.WAProto.MessageAssociation ? Types_1.WAProto.MessageAssociation.AssociationType.MEDIA_ALBUM : 3,
455
+ parentMessageKey: message.albumParentKey
456
+ } });
457
+ }
458
+ }
447
459
  else {
448
460
  m = await (0, exports.prepareWAMessageMedia)(message, options);
449
461
  }
@@ -452,8 +464,24 @@ const generateWAMessageContent = async (message, options) => {
452
464
  }
453
465
  if ('mentions' in message && ((_a = message.mentions) === null || _a === void 0 ? void 0 : _a.length)) {
454
466
  const [messageType] = Object.keys(m);
455
- m[messageType].contextInfo = m[messageType] || {};
456
- m[messageType].contextInfo.mentionedJid = message.mentions;
467
+ const key = m[messageType];
468
+ if (key) {
469
+ if (!key.contextInfo) {
470
+ key.contextInfo = {};
471
+ }
472
+ key.contextInfo.mentionedJid = message.mentions;
473
+ if (message.mentionAll) {
474
+ key.contextInfo.nonJidMentions = 1;
475
+ }
476
+ }
477
+ else if (key !== undefined) {
478
+ m[messageType] = {
479
+ contextInfo: {
480
+ mentionedJid: message.mentions,
481
+ nonJidMentions: message.mentionAll ? 1 : 0
482
+ }
483
+ };
484
+ }
457
485
  }
458
486
  if ('edit' in message) {
459
487
  m = {
@@ -0,0 +1,34 @@
1
+ "use strict";
2
+ Object.defineProperty(exports, "__esModule", { value: true });
3
+ exports.makeOfflineNodeProcessor = void 0;
4
+ /**
5
+ * Processes offline nodes sequentially.
6
+ * Nodes arriving while offline are queued and processed in order
7
+ * once a handler invokes enqueue().
8
+ */
9
+ const makeOfflineNodeProcessor = (nodeProcessorMap, onUnexpectedError, ws) => {
10
+ const nodes = [];
11
+ let isProcessing = false;
12
+ const enqueue = (type, node) => {
13
+ nodes.push({ type, node });
14
+ if (isProcessing) {
15
+ return;
16
+ }
17
+ isProcessing = true;
18
+ const promise = async () => {
19
+ while (nodes.length && ws.isOpen) {
20
+ const { type, node } = nodes.shift();
21
+ const nodeProcessor = nodeProcessorMap.get(type);
22
+ if (!nodeProcessor) {
23
+ onUnexpectedError(new Error(`unknown offline node type: ${type}`), 'processing offline node');
24
+ continue;
25
+ }
26
+ await nodeProcessor(node);
27
+ }
28
+ isProcessing = false;
29
+ };
30
+ promise().catch(error => onUnexpectedError(error, 'processing offline nodes'));
31
+ };
32
+ return { enqueue };
33
+ };
34
+ exports.makeOfflineNodeProcessor = makeOfflineNodeProcessor;