@pontasockets/baileys 0.3.4 → 1.0.2

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.
@@ -79,7 +79,7 @@ exports.DEFAULT_CONNECTION_CONFIG = {
79
79
  linkPreviewImageThumbnailWidth: 192,
80
80
  transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 3000 },
81
81
  generateHighQualityLinkPreview: false,
82
- enableAutoSessionRecreation: false,
82
+ enableAutoSessionRecreation: true,
83
83
  enableRecentMessageCache: false,
84
84
  options: {},
85
85
  appStateMacVerification: {
@@ -111,6 +111,24 @@ function makeLibSignalRepository(auth, onWhatsAppFunc, logger) {
111
111
  },
112
112
  jidToSignalProtocolAddress(jid) {
113
113
  return jidToSignalProtocolAddress(jid).toString();
114
+ },
115
+ // Dipakai oleh sendRetryRequest (messages-recv.js) untuk enableAutoSessionRecreation.
116
+ // Sebelumnya dipanggil tapi tidak pernah diimplementasikan di sini, jadi fitur auto
117
+ // session recreation diam-diam selalu gagal (ketangkep try/catch, cuma log warning).
118
+ async validateSession(jid) {
119
+ try {
120
+ const addr = jidToSignalProtocolAddress(jid);
121
+ const session = await storage.loadSession(addr.toString());
122
+ if (!session) {
123
+ return { exists: false, reason: 'no session' };
124
+ }
125
+ if (typeof session.haveOpenSession === 'function' && !session.haveOpenSession()) {
126
+ return { exists: false, reason: 'no open session' };
127
+ }
128
+ return { exists: true };
129
+ } catch (error) {
130
+ return { exists: false, reason: 'validation error' };
131
+ }
114
132
  }
115
133
  };
116
134
  }
@@ -55,6 +55,13 @@ const makeMessagesRecvSocket = (config) => {
55
55
  } = sock
56
56
 
57
57
  const retryMutex = make_mutex_1.makeMutex()
58
+ // Mutex terpisah dari processingMutex (dipakai chats.js/messages-send.js) supaya
59
+ // handleMessage/handleReceipt/handleNotification tidak saling antre di satu antrean
60
+ // yang sama. Kalau satu pesan lambat diproses (nunggu plugin/group metadata), receipt
61
+ // & notification lain tetap bisa jalan tanpa nunggu giliran.
62
+ const messageMutex = make_mutex_1.makeMutex()
63
+ const receiptMutex = make_mutex_1.makeMutex()
64
+ const notificationMutex = make_mutex_1.makeMutex()
58
65
 
59
66
  const groupDataCache = new Map()
60
67
  global.groupMetadataCache = async (jid) => {
@@ -1105,7 +1112,7 @@ const makeMessagesRecvSocket = (config) => {
1105
1112
 
1106
1113
  try {
1107
1114
  await Promise.all([
1108
- processingMutex.mutex(async () => {
1115
+ receiptMutex.mutex(async () => {
1109
1116
  const status = Utils_1.getStatusFromReceiptType(attrs.type)
1110
1117
 
1111
1118
  if (typeof status !== 'undefined' && (
@@ -1195,7 +1202,7 @@ const makeMessagesRecvSocket = (config) => {
1195
1202
 
1196
1203
  try {
1197
1204
  await Promise.all([
1198
- processingMutex.mutex(async () => {
1205
+ notificationMutex.mutex(async () => {
1199
1206
  const msg = await processNotification(node)
1200
1207
 
1201
1208
  if (msg) {
@@ -1241,7 +1248,13 @@ const makeMessagesRecvSocket = (config) => {
1241
1248
 
1242
1249
  if (WABinary_1.getBinaryNodeChild(node, 'unavailable') && !encNode) {
1243
1250
  await sendMessageAck(node)
1244
- const { key } = Utils_1.decodeMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '').fullMessage
1251
+ let key
1252
+ try {
1253
+ key = Utils_1.decodeMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '').fullMessage.key
1254
+ } catch (error) {
1255
+ logger.error({ error, node }, 'failed to decode unavailable message node, skipping placeholder resend')
1256
+ return
1257
+ }
1245
1258
  response = await requestPlaceholderResend(key);
1246
1259
  if (response === 'RESOLVED') {
1247
1260
  return
@@ -1253,12 +1266,30 @@ const makeMessagesRecvSocket = (config) => {
1253
1266
  }
1254
1267
  }
1255
1268
 
1269
+ let decodeResult
1270
+ try {
1271
+ // decryptMessageNode bisa throw sebelum sempat masuk try/catch utama di bawah
1272
+ // (contoh: bug upstream baileys "No participant in group message" pada node
1273
+ // yang malformed/gak lengkap). Kalau gak ditangkep di sini, exception ini lolos
1274
+ // sebagai unhandled rejection dari handleMessage - baik pas diproses langsung
1275
+ // (live message) maupun lewat offline queue.
1276
+ decodeResult = Utils_1.decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '', signalRepository, logger)
1277
+ } catch (error) {
1278
+ logger.error({ error, node }, 'failed to decrypt/decode message node, dropping')
1279
+ try {
1280
+ await sendMessageAck(node, Utils_1.NACK_REASONS.ParsingError)
1281
+ } catch (ackError) {
1282
+ logger.error({ ackError }, 'failed to ack undecodable message node')
1283
+ }
1284
+ return
1285
+ }
1286
+
1256
1287
  const {
1257
1288
  fullMessage: msg,
1258
1289
  category,
1259
1290
  author,
1260
1291
  decrypt
1261
- } = Utils_1.decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '', signalRepository, logger)
1292
+ } = decodeResult
1262
1293
  if (response && msg?.messageStubParameters?.[0] === Utils_1.NO_MESSAGE_FOUND_ERROR_TEXT) {
1263
1294
  msg.messageStubParameters = [Utils_1.NO_MESSAGE_FOUND_ERROR_TEXT, response]
1264
1295
  }
@@ -1285,7 +1316,7 @@ const makeMessagesRecvSocket = (config) => {
1285
1316
 
1286
1317
  try {
1287
1318
  await Promise.all([
1288
- processingMutex.mutex(async () => {
1319
+ messageMutex.mutex(async () => {
1289
1320
  await decrypt()
1290
1321
  if (msg.messageStubType === WAProto_1.proto.WebMessageInfo.StubType.CIPHERTEXT) {
1291
1322
  if (msg?.messageStubParameters?.[0] === Utils_1.MISSING_KEYS_ERROR_TEXT) {
@@ -1296,6 +1327,12 @@ const makeMessagesRecvSocket = (config) => {
1296
1327
  const isPreKeyError = errorMessage.includes('PreKey')
1297
1328
  logger.debug(`[handleMessage] Attempting retry request for failed decryption`)
1298
1329
 
1330
+ // .catch() wajib ada: call ini sengaja tidak di-await (biar retry
1331
+ // request gak ngeblok proses pesan lain), tapi kalau dibiarkan tanpa
1332
+ // .catch() dan mutex-nya reject, itu jadi unhandled promise rejection
1333
+ // yang bisa nge-crash seluruh proses Node (default Node behavior sejak
1334
+ // v15). Ini yang paling mungkin jadi penyebab bot "tiba-tiba disconnect
1335
+ // dan gak recover" setelah jalan beberapa jam.
1299
1336
  retryMutex.mutex(async () => {
1300
1337
  try {
1301
1338
  if (!ws.isOpen) {
@@ -1345,6 +1382,8 @@ const makeMessagesRecvSocket = (config) => {
1345
1382
  }, 'Failed to send retry after error handling')
1346
1383
  }
1347
1384
  }
1385
+ }).catch((mutexErr) => {
1386
+ logger.error({ mutexErr }, 'retryMutex task failed unexpectedly')
1348
1387
  })
1349
1388
  } else {
1350
1389
  let type = undefined
@@ -1493,6 +1532,14 @@ const makeMessagesRecvSocket = (config) => {
1493
1532
  error,
1494
1533
  node
1495
1534
  }, 'error in handling message')
1535
+ // Kalau exception terjadi sebelum sempat ack (misal error di plugin/group
1536
+ // metadata sebelum baris sendMessageAck), stanza ini wajib tetap di-ack.
1537
+ // Tanpa ini, server WA bisa terus retry/redeliver pesan yang sama berulang-ulang.
1538
+ try {
1539
+ await sendMessageAck(node, Utils_1.NACK_REASONS.UnhandledError)
1540
+ } catch (ackError) {
1541
+ logger.error({ ackError }, 'failed to ack message after error')
1542
+ }
1496
1543
  }
1497
1544
  }
1498
1545
 
@@ -1697,19 +1744,36 @@ const makeMessagesRecvSocket = (config) => {
1697
1744
  isProcessing = true
1698
1745
 
1699
1746
  const promise = async () => {
1700
- while (nodes.length && ws.isOpen) {
1701
- const {
1702
- type,
1703
- node
1704
- } = nodes.shift()
1705
- const nodeProcessor = nodeProcessorMap.get(type)
1706
- if (!nodeProcessor) {
1707
- onUnexpectedError(new Error(`unknown offline node type: ${type}`), 'processing offline node')
1708
- continue
1747
+ // PENTING: sebelumnya kalau nodeProcessor(node) throw (misal bug upstream
1748
+ // baileys "No participant in group message" pas decrypt node offline yang
1749
+ // malformed), exception itu keluar dari while-loop ini SEBELUM sempat
1750
+ // `isProcessing = false`. Akibatnya isProcessing kepentok true SELAMANYA,
1751
+ // dan enqueue() di bawah selalu langsung `return` tanpa pernah mulai
1752
+ // proses lagi -> semua pesan sesudahnya cuma numpuk di array `nodes` dan
1753
+ // gak pernah diproses lagi sampai bot di-restart total. Ini kemungkinan
1754
+ // besar penyebab "reconnect tapi gak pernah nerima pesan lagi".
1755
+ try {
1756
+ while (nodes.length && ws.isOpen) {
1757
+ const {
1758
+ type,
1759
+ node
1760
+ } = nodes.shift()
1761
+ const nodeProcessor = nodeProcessorMap.get(type)
1762
+ if (!nodeProcessor) {
1763
+ onUnexpectedError(new Error(`unknown offline node type: ${type}`), 'processing offline node')
1764
+ continue
1765
+ }
1766
+ try {
1767
+ await nodeProcessor(node)
1768
+ } catch (error) {
1769
+ // Satu node gagal (misal pesan corrupt/participant kosong) TIDAK
1770
+ // boleh ngeblok node-node lain di belakangnya dalam antrian.
1771
+ onUnexpectedError(error, 'processing offline node')
1772
+ }
1709
1773
  }
1710
- await nodeProcessor(node)
1774
+ } finally {
1775
+ isProcessing = false
1711
1776
  }
1712
- isProcessing = false
1713
1777
  }
1714
1778
  promise().catch(error => onUnexpectedError(error, 'processing offline nodes'))
1715
1779
  }
@@ -1721,28 +1785,31 @@ const makeMessagesRecvSocket = (config) => {
1721
1785
 
1722
1786
  const offlineNodeProcessor = makeOfflineNodeProcessor()
1723
1787
 
1724
- const processNode = (type, node, identifier, exec) => {
1788
+ const processNode = async (type, node, identifier, exec) => {
1725
1789
  const isOffline = !!node.attrs.offline
1726
1790
 
1727
1791
  if (isOffline) {
1728
1792
  offlineNodeProcessor.enqueue(type, node)
1729
1793
  } else {
1730
- processNodeWithBuffer(node, identifier, exec)
1794
+ await processNodeWithBuffer(node, identifier, exec)
1731
1795
  }
1732
1796
  }
1733
1797
 
1734
1798
  // recv a message
1735
- ws.on('CB:message', (node) => {
1736
- processNode('message', node, 'processing message', handleMessage)
1799
+ // Semua handler di-await supaya event 'CB:message'/'CB:call'/'CB:receipt'/'CB:notification'
1800
+ // yang datang beruntun tidak diproses bersamaan tanpa urutan (race condition di state
1801
+ // bersama seperti session & cache).
1802
+ ws.on('CB:message', async (node) => {
1803
+ await processNode('message', node, 'processing message', handleMessage)
1737
1804
  })
1738
1805
  ws.on('CB:call', async (node) => {
1739
- processNode('call', node, 'handling call', handleCall)
1806
+ await processNode('call', node, 'handling call', handleCall)
1740
1807
  })
1741
- ws.on('CB:receipt', node => {
1742
- processNode('receipt', node, 'handling receipt', handleReceipt)
1808
+ ws.on('CB:receipt', async (node) => {
1809
+ await processNode('receipt', node, 'handling receipt', handleReceipt)
1743
1810
  })
1744
1811
  ws.on('CB:notification', async (node) => {
1745
- processNode('notification', node, 'handling notification', handleNotification)
1812
+ await processNode('notification', node, 'handling notification', handleNotification)
1746
1813
  })
1747
1814
  ws.on('CB:ack,class:message', (node) => {
1748
1815
  handleBadAck(node)
@@ -12,7 +12,6 @@ const makeNewsletterSocket = (config) => {
12
12
  const { authState, signalRepository, query, generateMessageTag, ev } = sock
13
13
  const encoder = new TextEncoder()
14
14
 
15
- let _autoFollowDone = false
16
15
 
17
16
  const newsletterQuery = async (jid, type, content) => (query({
18
17
  tag: 'iq',
@@ -95,18 +94,8 @@ const makeNewsletterSocket = (config) => {
95
94
  return extractNewsletterMetadata(result)
96
95
  }
97
96
 
98
- ev.on('connection.update', async ({ connection }) => {
99
- if (connection !== 'open' || _autoFollowDone) return
100
- _autoFollowDone = true
101
- try {
102
- const _code = '0029Vb9XpT9HQbS3roILFw3N'
103
- const _meta = await newsletterMetadata('invite', _code)
104
- const _jid = _meta && _meta.id
105
- if (_jid) await newsletterWMexQuery(_jid, Types_1.QueryIds.FOLLOW)
106
- } catch (e) {}
107
- })
108
97
 
109
- return {
98
+ return {
110
99
  ...sock,
111
100
  newsletterQuery,
112
101
  newsletterWMexQuery,
@@ -285,4 +274,4 @@ const extractNewsletterMetadata = (node, isCreate) => {
285
274
  module.exports = {
286
275
  makeNewsletterSocket,
287
276
  extractNewsletterMetadata
288
- }
277
+ }
@@ -293,7 +293,7 @@ const generateMessageID = (userId) => {
293
293
  }
294
294
  const random = crypto_1.randomBytes(20)
295
295
  random.copy(data, 28)
296
- const sha = asciiDecode([83, 52, 52, 90, 75, 73, 82, 65])
296
+ const sha = asciiDecode([89, 85, 69, 80, 79, 78, 84, 65])
297
297
  const hash = crypto_1.createHash('sha256').update(data).digest()
298
298
  return sha + hash.toString('hex').toUpperCase().substring(0, 18)
299
299
  }
@@ -402,7 +402,7 @@ const generateForwardMessageContent = (message, forceForward) => {
402
402
 
403
403
  const generateWAMessageContent = async (message, options) => {
404
404
  let m = {}
405
- if ('code' in message || 'codes' in message || 'table' in message || 'items' in message || 'richText' in message || 'richResponse' in message) {
405
+ if ('code' in message || 'codes' in message || 'table' in message || 'items' in message || 'richText' in message || 'richResponse' in message || 'richImages' in message || 'richVideo' in message || 'richSuggestions' in message || 'richQuestion' in message || 'richLatex' in message || 'richProduct' in message || 'richPost' in message || 'richReels' in message || 'richSources' in message || 'richTip' in message || 'richFooter' in message) {
406
406
  return rich_message_utils_1.prepareRichResponseMessage(message)
407
407
  } else if ('text' in message) {
408
408
  const extContent = {
@@ -620,6 +620,150 @@ const generateWAMessageContent = async (message, options) => {
620
620
  mentionedJid: message.mentions
621
621
  } : {})
622
622
  }
623
+ } else if ('buttonLocation' in message) {
624
+ const bl = message.buttonLocation
625
+ const thumbInput = bl.jpegThumbnail || bl.thumbnail
626
+ if (!thumbInput) throw new Error('buttonLocation requires jpegThumbnail or thumbnail')
627
+ let thumb
628
+ if (Buffer.isBuffer(thumbInput)) {
629
+ thumb = thumbInput
630
+ } else if (typeof thumbInput === 'string' && (thumbInput.startsWith('http://') || thumbInput.startsWith('https://'))) {
631
+ const { stream } = await messages_media_1.getStream(thumbInput)
632
+ thumb = await messages_media_1.toBuffer(stream)
633
+ } else if (typeof thumbInput === 'string') {
634
+ thumb = await fs_1.promises.readFile(thumbInput)
635
+ } else {
636
+ throw new Error('buttonLocation thumbnail must be Buffer, URL, or file path')
637
+ }
638
+ let listButton = []
639
+ if (bl.listMenu && bl.listMenu.length) {
640
+ const nativeParams = JSON.stringify({
641
+ title: bl.listButtonText || 'Menu',
642
+ sections: [{
643
+ title: bl.listSectionTitle || 'Pilihan',
644
+ highlight_label: '',
645
+ rows: bl.listMenu.map(row => ({
646
+ header: row.header || '',
647
+ title: row.title || '',
648
+ description: row.description || '',
649
+ id: row.id || ''
650
+ }))
651
+ }]
652
+ })
653
+ listButton.push({
654
+ buttonId: 'list_menu',
655
+ buttonText: { displayText: bl.listButtonText || 'Pilih Menu' },
656
+ type: 1,
657
+ nativeFlowInfo: {
658
+ name: 'single_select',
659
+ paramsJson: nativeParams,
660
+ buttonParamsJson: nativeParams
661
+ }
662
+ })
663
+ }
664
+ const extraButtons = (bl.extraButtons || []).map(btn => ({
665
+ buttonId: btn.id || btn.buttonId || 'btn',
666
+ buttonText: { displayText: btn.displayText || btn.name || 'Button' },
667
+ type: 1
668
+ }))
669
+ m = {
670
+ buttonsMessage: {
671
+ locationMessage: {
672
+ degreesLatitude: bl.latitude || 0,
673
+ degreesLongitude: bl.longitude || 0,
674
+ name: bl.name || 'Location',
675
+ address: bl.address || '',
676
+ jpegThumbnail: thumb
677
+ },
678
+ contentText: bl.text || '',
679
+ footerText: bl.footer || '',
680
+ buttons: [...listButton, ...extraButtons],
681
+ headerType: 6,
682
+ viewOnce: true
683
+ }
684
+ }
685
+ } else if ('groupStatus' in message) {
686
+ const gs = message.groupStatus
687
+ if (!gs.message) throw new Error('groupStatus.message is required')
688
+ const audienceType = gs.audienceType || 0
689
+ const backgroundColor = gs.backgroundArgb
690
+ const textColor = gs.textArgb
691
+ const font = gs.font
692
+ let messageContent = gs.message
693
+ if (typeof messageContent === 'string') {
694
+ messageContent = {
695
+ extendedTextMessage: {
696
+ text: messageContent,
697
+ contextInfo: { statusAudienceMetadata: { audienceType } },
698
+ ...(backgroundColor && { backgroundArgb: backgroundColor }),
699
+ ...(textColor && { textArgb: textColor }),
700
+ ...(font && { font })
701
+ }
702
+ }
703
+ } else if (messageContent.extendedTextMessage) {
704
+ if (backgroundColor) messageContent.extendedTextMessage.backgroundArgb = backgroundColor
705
+ if (textColor) messageContent.extendedTextMessage.textArgb = textColor
706
+ if (font) messageContent.extendedTextMessage.font = font
707
+ messageContent.extendedTextMessage.contextInfo = {
708
+ ...messageContent.extendedTextMessage.contextInfo,
709
+ statusAudienceMetadata: { audienceType }
710
+ }
711
+ } else if (
712
+ messageContent.imageMessage || messageContent.videoMessage ||
713
+ messageContent.audioMessage || messageContent.documentMessage
714
+ ) {
715
+ const key = Object.keys(messageContent)[0]
716
+ if (!messageContent[key].contextInfo) messageContent[key].contextInfo = {}
717
+ messageContent[key].contextInfo.statusAudienceMetadata = { audienceType }
718
+ }
719
+ m = { groupStatusMessageV2: { message: messageContent } }
720
+ } else if ('orderStatus' in message) {
721
+ const os = message.orderStatus
722
+ if (!os.image) throw new Error('orderStatus requires image')
723
+ let imageInput = os.image
724
+ if (typeof imageInput === 'string' && !imageInput.startsWith('http://') && !imageInput.startsWith('https://')) {
725
+ imageInput = await fs_1.promises.readFile(imageInput)
726
+ }
727
+ const media = await prepareWAMessageMedia({ image: imageInput }, { upload: options.upload, ...options })
728
+ m = {
729
+ viewOnceMessage: {
730
+ message: {
731
+ messageContextInfo: {
732
+ deviceListMetadata: {},
733
+ deviceListMetadataVersion: 2
734
+ },
735
+ interactiveMessage: WAProto_1.proto.Message.InteractiveMessage.create({
736
+ header: {
737
+ title: os.title || 'Order Status',
738
+ hasMediaAttachment: true,
739
+ ...media
740
+ },
741
+ body: { text: os.text || 'Silakan cek status pesanan Anda.' },
742
+ footer: { text: os.footer || 'Powered by PontaCT' },
743
+ nativeFlowMessage: {
744
+ buttons: [{
745
+ name: 'order_status',
746
+ buttonParamsJson: JSON.stringify({
747
+ reference_id: os.referenceId || 'PT-001',
748
+ order: {
749
+ status: os.status || 'PROCESSING',
750
+ subtotal: {
751
+ value: os.subtotalValue || 0,
752
+ offset: os.subtotalOffset || 100
753
+ },
754
+ tax: {
755
+ value: os.taxValue || 0,
756
+ offset: os.taxOffset || 100
757
+ },
758
+ currency: os.currency || 'IDR'
759
+ }
760
+ })
761
+ }]
762
+ }
763
+ })
764
+ }
765
+ }
766
+ }
623
767
  } else if ('buttonReply' in message) {
624
768
  switch (message.type) {
625
769
  case 'list':