@itsliaaa/baileys 0.1.5 → 0.1.6

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.
@@ -7,6 +7,8 @@ import { DEFAULT_CACHE_TTLS, KEY_BUNDLE_TYPE, MIN_PREKEY_COUNT, PLACEHOLDER_MAX_
7
7
  import { WAMessageStatus, WAMessageStubType } from '../Types/index.js';
8
8
  import { aesDecryptCTR, aesEncryptGCM, cleanMessage, Curve, decodeMediaRetryNode, decodeMessageNode, decryptMessageNode, delay, derivePairingCodeKey, encodeBigEndian, encodeSignedDeviceIdentity, extractAddressingContext, getCallStatusFromNode, getHistoryMsg, getNextPreKeys, getStatusFromReceiptType, handleIdentityChange, hkdf, MISSING_KEYS_ERROR_TEXT, NACK_REASONS, NO_MESSAGE_FOUND_ERROR_TEXT, toNumber, unixTimestampSeconds, xmppPreKey, xmppSignedPreKey } from '../Utils/index.js';
9
9
  import { makeMutex } from '../Utils/make-mutex.js';
10
+ import { makeOfflineNodeProcessor } from '../Utils/offline-node-processor.js';
11
+ import { buildAckStanza } from '../Utils/stanza-ack.js';
10
12
  import { areJidsSameUser, binaryNodeToString, getAllBinaryNodeChildren, getBinaryNodeChild, getBinaryNodeChildBuffer, getBinaryNodeChildren, getBinaryNodeChildString, isJidGroup, isJidNewsletter, isJidStatusBroadcast, isLidUser, isPnUser, jidDecode, jidNormalizedUser, S_WHATSAPP_NET } from '../WABinary/index.js';
11
13
  import { extractGroupMetadata } from './groups.js';
12
14
  import { makeMessagesSocket } from './messages-send.js';
@@ -217,32 +219,9 @@ export const makeMessagesRecvSocket = (config) => {
217
219
  break;
218
220
  }
219
221
  };
220
- const sendMessageAck = async ({ tag, attrs, content }, errorCode) => {
221
- const stanza = {
222
- tag: 'ack',
223
- attrs: {
224
- id: attrs.id,
225
- to: attrs.from,
226
- class: tag
227
- }
228
- };
229
- if (!!errorCode) {
230
- stanza.attrs.error = errorCode.toString();
231
- }
232
- if (!!attrs.participant) {
233
- stanza.attrs.participant = attrs.participant;
234
- }
235
- if (!!attrs.recipient) {
236
- stanza.attrs.recipient = attrs.recipient;
237
- }
238
- if (!!attrs.type &&
239
- (tag !== 'message' || getBinaryNodeChild({ tag, attrs, content }, 'unavailable') || errorCode !== 0)) {
240
- stanza.attrs.type = attrs.type;
241
- }
242
- if (tag === 'message' && getBinaryNodeChild({ tag, attrs, content }, 'unavailable')) {
243
- stanza.attrs.from = authState.creds.me.id;
244
- }
245
- logger.debug({ recv: { tag, attrs }, sent: stanza.attrs }, 'sent ack');
222
+ const sendMessageAck = async (node, errorCode) => {
223
+ const stanza = buildAckStanza(node, errorCode, authState.creds.me.id);
224
+ logger.debug({ recv: { tag: node.tag, attrs: node.attrs }, sent: stanza.attrs }, 'sent ack');
246
225
  await sendNode(stanza);
247
226
  };
248
227
  const rejectCall = async (callId, callFrom) => {
@@ -995,7 +974,7 @@ export const makeMessagesRecvSocket = (config) => {
995
974
  ]);
996
975
  }
997
976
  finally {
998
- await sendMessageAck(node);
977
+ await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack receipt'));
999
978
  }
1000
979
  };
1001
980
  const handleNotification = async (node) => {
@@ -1030,7 +1009,7 @@ export const makeMessagesRecvSocket = (config) => {
1030
1009
  ]);
1031
1010
  }
1032
1011
  finally {
1033
- await sendMessageAck(node);
1012
+ await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack notification'));
1034
1013
  }
1035
1014
  };
1036
1015
  const handleMessage = async (node) => {
@@ -1046,36 +1025,34 @@ export const makeMessagesRecvSocket = (config) => {
1046
1025
  await sendMessageAck(node, NACK_REASONS.MissingMessageSecret);
1047
1026
  return;
1048
1027
  }
1049
- const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '', signalRepository, logger);
1050
- const alt = msg.key.participantAlt || msg.key.remoteJidAlt;
1051
- // store new mappings we didn't have before
1052
- if (!!alt) {
1053
- const altServer = jidDecode(alt)?.server;
1054
- const primaryJid = msg.key.participant || msg.key.remoteJid;
1055
- if (altServer === 'lid') {
1056
- if (!(await signalRepository.lidMapping.getPNForLID(alt))) {
1057
- await signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }]);
1058
- await signalRepository.migrateSession(primaryJid, alt);
1028
+ let acked = false;
1029
+ try {
1030
+ const { fullMessage: msg, category, author, decrypt } = decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '', signalRepository, logger);
1031
+ const alt = msg.key.participantAlt || msg.key.remoteJidAlt;
1032
+ // store new mappings we didn't have before
1033
+ if (!!alt) {
1034
+ const altServer = jidDecode(alt)?.server;
1035
+ const primaryJid = msg.key.participant || msg.key.remoteJid;
1036
+ if (altServer === 'lid') {
1037
+ if (!(await signalRepository.lidMapping.getPNForLID(alt))) {
1038
+ await signalRepository.lidMapping.storeLIDPNMappings([{ lid: alt, pn: primaryJid }]);
1039
+ await signalRepository.migrateSession(primaryJid, alt);
1040
+ }
1041
+ }
1042
+ else {
1043
+ await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }]);
1044
+ await signalRepository.migrateSession(alt, primaryJid);
1059
1045
  }
1060
1046
  }
1061
- else {
1062
- await signalRepository.lidMapping.storeLIDPNMappings([{ lid: primaryJid, pn: alt }]);
1063
- await signalRepository.migrateSession(alt, primaryJid);
1064
- }
1065
- }
1066
- if (msg.key?.remoteJid && msg.key?.id && messageRetryManager) {
1067
- messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message);
1068
- logger.debug({
1069
- jid: msg.key.remoteJid,
1070
- id: msg.key.id
1071
- }, 'Added message to recent cache for retry receipts');
1072
- }
1073
- try {
1074
1047
  await messageMutex.mutex(async () => {
1075
1048
  await decrypt();
1049
+ if (msg.key?.remoteJid && msg.key?.id && msg.message && messageRetryManager) {
1050
+ messageRetryManager.addRecentMessage(msg.key.remoteJid, msg.key.id, msg.message);
1051
+ }
1076
1052
  // message failed to decrypt
1077
1053
  if (msg.messageStubType === proto.WebMessageInfo.StubType.CIPHERTEXT && msg.category !== 'peer') {
1078
1054
  if (msg?.messageStubParameters?.[0] === MISSING_KEYS_ERROR_TEXT) {
1055
+ acked = true;
1079
1056
  return sendMessageAck(node, NACK_REASONS.ParsingError);
1080
1057
  }
1081
1058
  if (msg.messageStubParameters?.[0] === NO_MESSAGE_FOUND_ERROR_TEXT) {
@@ -1087,11 +1064,13 @@ export const makeMessagesRecvSocket = (config) => {
1087
1064
  unavailableType === 'hosted_unavailable_fanout' ||
1088
1065
  unavailableType === 'view_once_unavailable_fanout') {
1089
1066
  logger.debug({ msgId: msg.key.id, unavailableType }, 'skipping placeholder resend for excluded unavailable type');
1067
+ acked = true;
1090
1068
  return sendMessageAck(node);
1091
1069
  }
1092
1070
  const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp);
1093
1071
  if (messageAge > PLACEHOLDER_MAX_AGE_SECONDS) {
1094
1072
  logger.debug({ msgId: msg.key.id, messageAge }, 'skipping placeholder resend for old message');
1073
+ acked = true;
1095
1074
  return sendMessageAck(node);
1096
1075
  }
1097
1076
  // Request the real content from the phone via placeholder resend PDO.
@@ -1128,6 +1107,7 @@ export const makeMessagesRecvSocket = (config) => {
1128
1107
  .catch(err => {
1129
1108
  logger.warn({ err, msgId: msg.key.id }, 'failed to request placeholder resend for unavailable message');
1130
1109
  });
1110
+ acked = true;
1131
1111
  await sendMessageAck(node);
1132
1112
  // Don't return — fall through to upsertMessage so the stub is emitted
1133
1113
  }
@@ -1137,6 +1117,7 @@ export const makeMessagesRecvSocket = (config) => {
1137
1117
  const messageAge = unixTimestampSeconds() - toNumber(msg.messageTimestamp);
1138
1118
  if (messageAge > STATUS_EXPIRY_SECONDS) {
1139
1119
  logger.debug({ msgId: msg.key.id, messageAge, remoteJid: msg.key.remoteJid }, 'skipping retry for expired status message');
1120
+ acked = true;
1140
1121
  return sendMessageAck(node);
1141
1122
  }
1142
1123
  }
@@ -1180,6 +1161,7 @@ export const makeMessagesRecvSocket = (config) => {
1180
1161
  logger.error({ retryErr }, 'Failed to send retry after error handling');
1181
1162
  }
1182
1163
  }
1164
+ acked = true;
1183
1165
  await sendMessageAck(node, NACK_REASONS.UnhandledError);
1184
1166
  });
1185
1167
  }
@@ -1208,6 +1190,7 @@ export const makeMessagesRecvSocket = (config) => {
1208
1190
  else if (!sendActiveReceipts) {
1209
1191
  type = 'inactive';
1210
1192
  }
1193
+ acked = true;
1211
1194
  await sendReceipt(msg.key.remoteJid, participant, [msg.key.id], type);
1212
1195
  // send ack for history message
1213
1196
  const isAnyHistoryMsg = getHistoryMsg(msg.message);
@@ -1217,6 +1200,7 @@ export const makeMessagesRecvSocket = (config) => {
1217
1200
  }
1218
1201
  }
1219
1202
  else {
1203
+ acked = true;
1220
1204
  await sendMessageAck(node);
1221
1205
  logger.debug({ key: msg.key }, 'processed newsletter message without receipts');
1222
1206
  }
@@ -1227,45 +1211,55 @@ export const makeMessagesRecvSocket = (config) => {
1227
1211
  }
1228
1212
  catch (error) {
1229
1213
  logger.error({ error, node: binaryNodeToString(node) }, 'error in handling message');
1214
+ if (!acked) {
1215
+ await sendMessageAck(node, NACK_REASONS.UnhandledError).catch(ackErr => logger.error({ ackErr }, 'failed to ack message after error'));
1216
+ }
1230
1217
  }
1231
1218
  };
1232
1219
  const handleCall = async (node) => {
1233
- const { attrs } = node;
1234
- const [infoChild] = getAllBinaryNodeChildren(node);
1235
- const status = getCallStatusFromNode(infoChild);
1236
- if (!infoChild) {
1237
- throw new Boom('Missing call info in call node');
1238
- }
1239
- const callId = infoChild.attrs['call-id'];
1240
- const from = infoChild.attrs.from || infoChild.attrs['call-creator'];
1241
- const call = {
1242
- chatId: attrs.from,
1243
- from,
1244
- callerPn: infoChild.attrs['caller_pn'],
1245
- id: callId,
1246
- date: new Date(+attrs.t * 1000),
1247
- offline: !!attrs.offline,
1248
- status
1249
- };
1250
- if (status === 'offer') {
1251
- call.isVideo = !!getBinaryNodeChild(infoChild, 'video');
1252
- call.isGroup = infoChild.attrs.type === 'group' || !!infoChild.attrs['group-jid'];
1253
- call.groupJid = infoChild.attrs['group-jid'];
1254
- await callOfferCache.set(call.id, call);
1220
+ try {
1221
+ const { attrs } = node;
1222
+ const [infoChild] = getAllBinaryNodeChildren(node);
1223
+ if (!infoChild) {
1224
+ throw new Boom('Missing call info in call node');
1225
+ }
1226
+ const status = getCallStatusFromNode(infoChild);
1227
+ const callId = infoChild.attrs['call-id'];
1228
+ const from = infoChild.attrs.from || infoChild.attrs['call-creator'];
1229
+ const call = {
1230
+ chatId: attrs.from,
1231
+ from,
1232
+ callerPn: infoChild.attrs['caller_pn'],
1233
+ id: callId,
1234
+ date: new Date(+attrs.t * 1000),
1235
+ offline: !!attrs.offline,
1236
+ status
1237
+ };
1238
+ if (status === 'offer') {
1239
+ call.isVideo = !!getBinaryNodeChild(infoChild, 'video');
1240
+ call.isGroup = infoChild.attrs.type === 'group' || !!infoChild.attrs['group-jid'];
1241
+ call.groupJid = infoChild.attrs['group-jid'];
1242
+ await callOfferCache.set(call.id, call);
1243
+ }
1244
+ const existingCall = await callOfferCache.get(call.id);
1245
+ // use existing call info to populate this event
1246
+ if (existingCall) {
1247
+ call.isVideo = existingCall.isVideo;
1248
+ call.isGroup = existingCall.isGroup;
1249
+ call.callerPn = call.callerPn || existingCall.callerPn;
1250
+ }
1251
+ // delete data once call has ended
1252
+ if (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate') {
1253
+ await callOfferCache.del(call.id);
1254
+ }
1255
+ ev.emit('call', [call]);
1255
1256
  }
1256
- const existingCall = await callOfferCache.get(call.id);
1257
- // use existing call info to populate this event
1258
- if (existingCall) {
1259
- call.isVideo = existingCall.isVideo;
1260
- call.isGroup = existingCall.isGroup;
1261
- call.callerPn = call.callerPn || existingCall.callerPn;
1257
+ catch (error) {
1258
+ logger.error({ error, node: binaryNodeToString(node) }, 'error in handling call');
1262
1259
  }
1263
- // delete data once call has ended
1264
- if (status === 'reject' || status === 'accept' || status === 'timeout' || status === 'terminate') {
1265
- await callOfferCache.del(call.id);
1260
+ finally {
1261
+ await sendMessageAck(node).catch(ackErr => logger.error({ ackErr }, 'failed to ack call'));
1266
1262
  }
1267
- ev.emit('call', [call]);
1268
- await sendMessageAck(node);
1269
1263
  };
1270
1264
  const handleBadAck = async ({ attrs }) => {
1271
1265
  const key = { remoteJid: attrs.from, fromMe: true, id: attrs.id };
@@ -1320,52 +1314,16 @@ export const makeMessagesRecvSocket = (config) => {
1320
1314
  return exec(node, false).catch(err => onUnexpectedError(err, identifier));
1321
1315
  }
1322
1316
  };
1323
- /** Yields control to the event loop to prevent blocking */
1324
- const yieldToEventLoop = () => {
1325
- return new Promise(resolve => setImmediate(resolve));
1326
- };
1327
- const makeOfflineNodeProcessor = () => {
1328
- const nodeProcessorMap = new Map([
1329
- ['message', handleMessage],
1330
- ['call', handleCall],
1331
- ['receipt', handleReceipt],
1332
- ['notification', handleNotification]
1333
- ]);
1334
- const nodes = [];
1335
- let isProcessing = false;
1336
- // Number of nodes to process before yielding to event loop
1337
- const BATCH_SIZE = 10;
1338
- const enqueue = (type, node) => {
1339
- nodes.push({ type, node });
1340
- if (isProcessing) {
1341
- return;
1342
- }
1343
- isProcessing = true;
1344
- const promise = async () => {
1345
- let processedInBatch = 0;
1346
- while (nodes.length && ws.isOpen) {
1347
- const { type, node } = nodes.shift();
1348
- const nodeProcessor = nodeProcessorMap.get(type);
1349
- if (!nodeProcessor) {
1350
- onUnexpectedError(new Error(`unknown offline node type: ${type}`), 'processing offline node');
1351
- continue;
1352
- }
1353
- await nodeProcessor(node);
1354
- processedInBatch++;
1355
- // Yield to event loop after processing a batch
1356
- // This prevents blocking the event loop for too long when there are many offline nodes
1357
- if (processedInBatch >= BATCH_SIZE) {
1358
- processedInBatch = 0;
1359
- await yieldToEventLoop();
1360
- }
1361
- }
1362
- isProcessing = false;
1363
- };
1364
- promise().catch(error => onUnexpectedError(error, 'processing offline nodes'));
1365
- };
1366
- return { enqueue };
1367
- };
1368
- const offlineNodeProcessor = makeOfflineNodeProcessor();
1317
+ const offlineNodeProcessor = makeOfflineNodeProcessor(new Map([
1318
+ ['message', handleMessage],
1319
+ ['call', handleCall],
1320
+ ['receipt', handleReceipt],
1321
+ ['notification', handleNotification]
1322
+ ]), {
1323
+ isWsOpen: () => ws.isOpen,
1324
+ onUnexpectedError,
1325
+ yieldToEventLoop: () => new Promise(resolve => setImmediate(resolve))
1326
+ });
1369
1327
  const processNode = async (type, node, identifier, exec) => {
1370
1328
  const isOffline = !!node.attrs.offline;
1371
1329
  if (isOffline) {
@@ -16,4 +16,5 @@ export * from './event-buffer.js';
16
16
  export * from './process-message.js';
17
17
  export * from './message-retry-manager.js';
18
18
  export * from './browser-utils.js';
19
- export * from './identity-change-handler.js';
19
+ export * from './identity-change-handler.js';
20
+ export * from './stanza-ack.js';
@@ -0,0 +1,39 @@
1
+ /**
2
+ * Creates a processor for offline stanza nodes that:
3
+ * - Queues nodes for sequential processing
4
+ * - Yields to the event loop periodically to avoid blocking
5
+ * - Catches handler errors to prevent the processing loop from crashing
6
+ */
7
+ export function makeOfflineNodeProcessor(nodeProcessorMap, deps, batchSize = 10) {
8
+ const nodes = [];
9
+ let isProcessing = false;
10
+ const enqueue = (type, node) => {
11
+ nodes.push({ type, node });
12
+ if (isProcessing) {
13
+ return;
14
+ }
15
+ isProcessing = true;
16
+ const promise = async () => {
17
+ let processedInBatch = 0;
18
+ while (nodes.length && deps.isWsOpen()) {
19
+ const { type, node } = nodes.shift();
20
+ const nodeProcessor = nodeProcessorMap.get(type);
21
+ if (!nodeProcessor) {
22
+ deps.onUnexpectedError(new Error(`unknown offline node type: ${type}`), 'processing offline node');
23
+ continue;
24
+ }
25
+ await nodeProcessor(node).catch(err => deps.onUnexpectedError(err, `processing offline ${type}`));
26
+ processedInBatch++;
27
+ // Yield to event loop after processing a batch
28
+ // This prevents blocking the event loop for too long when there are many offline nodes
29
+ if (processedInBatch >= batchSize) {
30
+ processedInBatch = 0;
31
+ await deps.yieldToEventLoop();
32
+ }
33
+ }
34
+ isProcessing = false;
35
+ };
36
+ promise().catch(error => deps.onUnexpectedError(error, 'processing offline nodes'));
37
+ };
38
+ return { enqueue };
39
+ }
@@ -0,0 +1,37 @@
1
+ /**
2
+ * Builds an ACK stanza for a received node.
3
+ * Pure function -- no I/O, no side effects.
4
+ *
5
+ * Mirrors WhatsApp Web's ACK construction:
6
+ * - WAWebHandleMsgSendAck.sendAck / sendNack
7
+ * - WAWebCreateNackFromStanza.createNackFromStanza
8
+ */
9
+ export function buildAckStanza(node, errorCode, meId) {
10
+ const { tag, attrs } = node;
11
+ const stanza = {
12
+ tag: 'ack',
13
+ attrs: {
14
+ id: attrs.id,
15
+ to: attrs.from,
16
+ class: tag
17
+ }
18
+ };
19
+ if (errorCode) {
20
+ stanza.attrs.error = errorCode.toString();
21
+ }
22
+ if (attrs.participant) {
23
+ stanza.attrs.participant = attrs.participant;
24
+ }
25
+ if (attrs.recipient) {
26
+ stanza.attrs.recipient = attrs.recipient;
27
+ }
28
+ // WA Web always includes type when present: `n.type || DROP_ATTR`
29
+ if (attrs.type) {
30
+ stanza.attrs.type = attrs.type;
31
+ }
32
+ // WA Web WAWebHandleMsgSendAck.sendAck/sendNack always include `from` for message-class ACKs
33
+ if (tag === 'message' && meId) {
34
+ stanza.attrs.from = meId;
35
+ }
36
+ return stanza;
37
+ }
@@ -124,12 +124,12 @@ export function binaryNodeToString(node, i = 0) {
124
124
  * @returns {object} A node with shape { tag, attrs, [content] } to inject into additionalNodes.
125
125
  */
126
126
  const FLOWS_MAP = {
127
- mpm: 1,
128
- cta_catalog: 1,
129
- send_location: 1,
130
- call_permission_request: 1,
131
- wa_payment_transaction_details: 1,
132
- automated_greeting_message_view_catalog: 1
127
+ mpm: true,
128
+ cta_catalog: true,
129
+ send_location: true,
130
+ call_permission_request: true,
131
+ wa_payment_transaction_details: true,
132
+ automated_greeting_message_view_catalog: true
133
133
  };
134
134
  const qualityAttribute = {
135
135
  tag: 'quality_control',
@@ -157,7 +157,7 @@ export const getBizBinaryNode = (message) => {
157
157
  content: defaultContent
158
158
  };
159
159
  }
160
- else if (buttonName && FLOWS_MAP[buttonName]) {
160
+ if (buttonName && FLOWS_MAP[buttonName]) {
161
161
  return {
162
162
  tag: 'biz',
163
163
  attrs: {},
@@ -195,7 +195,7 @@ export const getBizBinaryNode = (message) => {
195
195
  ]
196
196
  };
197
197
  }
198
- else if (message.listMessage) {
198
+ if (message.listMessage) {
199
199
  return {
200
200
  tag: 'biz',
201
201
  attrs: {},
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@itsliaaa/baileys",
3
- "version": "0.1.5",
3
+ "version": "0.1.6",
4
4
  "description": "A simple fork of Baileys for WhatsApp automation",
5
5
  "main": "lib/index.js",
6
6
  "type": "module",