@pontasockets/baileys 1.0.1 → 1.0.3

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/README.md CHANGED
@@ -242,6 +242,27 @@ sock.sendMessage(jid, content, options?)
242
242
 
243
243
  #### Standard Messages
244
244
 
245
+ <details>
246
+ <summary><b>📦 Sticker Pack</b></summary>
247
+ <br>
248
+
249
+ ```js
250
+ sock.sendMessage(jid, {
251
+ stickerPack: {
252
+ name: 'Koleksi Ponta',
253
+ publisher: 'PontaCT',
254
+ description: 'Sticker pack pertama',
255
+ cover: { url: 'https://example.jpg' },
256
+ stickers: [
257
+ { sticker: { url: 'https://example.webp' }, emojis: ['😂'] },
258
+ { sticker: { url: 'https://example.webp' }, emojis: ['🔥'] }
259
+ ]
260
+ }
261
+ })
262
+ ```
263
+
264
+ </details>
265
+
245
266
  <details>
246
267
  <summary><b>📝 Text Message</b></summary>
247
268
  <br>
@@ -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) => {
@@ -674,7 +681,17 @@ const makeMessagesRecvSocket = (config) => {
674
681
 
675
682
  const handleMexNotification = (id, node) => {
676
683
  const operation = node?.attrs?.op_name
677
- const content = JSON.parse(node?.content)
684
+ let content
685
+ try {
686
+ content = JSON.parse(node?.content)
687
+ } catch (parseErr) {
688
+ logger.debug({ parseErr, node }, 'failed to parse mex notification content, skipping')
689
+ return
690
+ }
691
+ if (!content?.data) {
692
+ logger.debug({ operation, node }, 'mex notification missing content.data, skipping')
693
+ return
694
+ }
678
695
 
679
696
  let contentPath
680
697
  let action
@@ -716,6 +733,17 @@ const makeMessagesRecvSocket = (config) => {
716
733
  contentPath = content.data[Types_1.XWAPaths.DEMOTE]
717
734
  }
718
735
 
736
+ // FIX: sebelumnya contentPath.actor.pn diakses langsung tanpa cek dulu.
737
+ // Kalau operation-nya bukan promote/demote yang beneran (WA ngirim tipe
738
+ // operasi MEX baru yang belum dikenal library ini), content.data[path]
739
+ // bisa undefined, dan ini crash dengan "Cannot read properties of
740
+ // undefined (reading 'actor')". Sekarang di-skip aja kalau gak lengkap,
741
+ // gak bikin seluruh proses notification gagal.
742
+ if (!contentPath || !contentPath.actor || !contentPath.user) {
743
+ logger.debug({ operation, node }, 'unhandled/incomplete mex notification, skipping')
744
+ return
745
+ }
746
+
719
747
  ev.emit('newsletter-participants.update', {
720
748
  id,
721
749
  author: contentPath.actor.pn,
@@ -1105,7 +1133,7 @@ const makeMessagesRecvSocket = (config) => {
1105
1133
 
1106
1134
  try {
1107
1135
  await Promise.all([
1108
- processingMutex.mutex(async () => {
1136
+ receiptMutex.mutex(async () => {
1109
1137
  const status = Utils_1.getStatusFromReceiptType(attrs.type)
1110
1138
 
1111
1139
  if (typeof status !== 'undefined' && (
@@ -1195,7 +1223,7 @@ const makeMessagesRecvSocket = (config) => {
1195
1223
 
1196
1224
  try {
1197
1225
  await Promise.all([
1198
- processingMutex.mutex(async () => {
1226
+ notificationMutex.mutex(async () => {
1199
1227
  const msg = await processNotification(node)
1200
1228
 
1201
1229
  if (msg) {
@@ -1241,7 +1269,13 @@ const makeMessagesRecvSocket = (config) => {
1241
1269
 
1242
1270
  if (WABinary_1.getBinaryNodeChild(node, 'unavailable') && !encNode) {
1243
1271
  await sendMessageAck(node)
1244
- const { key } = Utils_1.decodeMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '').fullMessage
1272
+ let key
1273
+ try {
1274
+ key = Utils_1.decodeMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '').fullMessage.key
1275
+ } catch (error) {
1276
+ logger.error({ err: error, node }, 'failed to decode unavailable message node, skipping placeholder resend')
1277
+ return
1278
+ }
1245
1279
  response = await requestPlaceholderResend(key);
1246
1280
  if (response === 'RESOLVED') {
1247
1281
  return
@@ -1253,12 +1287,30 @@ const makeMessagesRecvSocket = (config) => {
1253
1287
  }
1254
1288
  }
1255
1289
 
1290
+ let decodeResult
1291
+ try {
1292
+ // decryptMessageNode bisa throw sebelum sempat masuk try/catch utama di bawah
1293
+ // (contoh: bug upstream baileys "No participant in group message" pada node
1294
+ // yang malformed/gak lengkap). Kalau gak ditangkep di sini, exception ini lolos
1295
+ // sebagai unhandled rejection dari handleMessage - baik pas diproses langsung
1296
+ // (live message) maupun lewat offline queue.
1297
+ decodeResult = Utils_1.decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '', signalRepository, logger)
1298
+ } catch (error) {
1299
+ logger.error({ err: error, node }, 'failed to decrypt/decode message node, dropping')
1300
+ try {
1301
+ await sendMessageAck(node, Utils_1.NACK_REASONS.ParsingError)
1302
+ } catch (ackError) {
1303
+ logger.error({ err: ackError }, 'failed to ack undecodable message node')
1304
+ }
1305
+ return
1306
+ }
1307
+
1256
1308
  const {
1257
1309
  fullMessage: msg,
1258
1310
  category,
1259
1311
  author,
1260
1312
  decrypt
1261
- } = Utils_1.decryptMessageNode(node, authState.creds.me.id, authState.creds.me.lid || '', signalRepository, logger)
1313
+ } = decodeResult
1262
1314
  if (response && msg?.messageStubParameters?.[0] === Utils_1.NO_MESSAGE_FOUND_ERROR_TEXT) {
1263
1315
  msg.messageStubParameters = [Utils_1.NO_MESSAGE_FOUND_ERROR_TEXT, response]
1264
1316
  }
@@ -1285,7 +1337,7 @@ const makeMessagesRecvSocket = (config) => {
1285
1337
 
1286
1338
  try {
1287
1339
  await Promise.all([
1288
- processingMutex.mutex(async () => {
1340
+ messageMutex.mutex(async () => {
1289
1341
  await decrypt()
1290
1342
  if (msg.messageStubType === WAProto_1.proto.WebMessageInfo.StubType.CIPHERTEXT) {
1291
1343
  if (msg?.messageStubParameters?.[0] === Utils_1.MISSING_KEYS_ERROR_TEXT) {
@@ -1296,6 +1348,12 @@ const makeMessagesRecvSocket = (config) => {
1296
1348
  const isPreKeyError = errorMessage.includes('PreKey')
1297
1349
  logger.debug(`[handleMessage] Attempting retry request for failed decryption`)
1298
1350
 
1351
+ // .catch() wajib ada: call ini sengaja tidak di-await (biar retry
1352
+ // request gak ngeblok proses pesan lain), tapi kalau dibiarkan tanpa
1353
+ // .catch() dan mutex-nya reject, itu jadi unhandled promise rejection
1354
+ // yang bisa nge-crash seluruh proses Node (default Node behavior sejak
1355
+ // v15). Ini yang paling mungkin jadi penyebab bot "tiba-tiba disconnect
1356
+ // dan gak recover" setelah jalan beberapa jam.
1299
1357
  retryMutex.mutex(async () => {
1300
1358
  try {
1301
1359
  if (!ws.isOpen) {
@@ -1321,7 +1379,7 @@ const makeMessagesRecvSocket = (config) => {
1321
1379
  await Utils_1.delay(1000)
1322
1380
  } catch (uploadErr) {
1323
1381
  logger.error({
1324
- uploadErr
1382
+ err: uploadErr
1325
1383
  }, 'Pre-key upload failed, proceeding with retry anyway')
1326
1384
  }
1327
1385
  }
@@ -1341,10 +1399,12 @@ const makeMessagesRecvSocket = (config) => {
1341
1399
  await sendRetryRequest(node, !encNode)
1342
1400
  } catch (retryErr) {
1343
1401
  logger.error({
1344
- retryErr
1402
+ err: retryErr
1345
1403
  }, 'Failed to send retry after error handling')
1346
1404
  }
1347
1405
  }
1406
+ }).catch((mutexErr) => {
1407
+ logger.error({ err: mutexErr }, 'retryMutex task failed unexpectedly')
1348
1408
  })
1349
1409
  } else {
1350
1410
  let type = undefined
@@ -1490,9 +1550,17 @@ const makeMessagesRecvSocket = (config) => {
1490
1550
  ])
1491
1551
  } catch (error) {
1492
1552
  logger.error({
1493
- error,
1553
+ err: error,
1494
1554
  node
1495
1555
  }, 'error in handling message')
1556
+ // Kalau exception terjadi sebelum sempat ack (misal error di plugin/group
1557
+ // metadata sebelum baris sendMessageAck), stanza ini wajib tetap di-ack.
1558
+ // Tanpa ini, server WA bisa terus retry/redeliver pesan yang sama berulang-ulang.
1559
+ try {
1560
+ await sendMessageAck(node, Utils_1.NACK_REASONS.UnhandledError)
1561
+ } catch (ackError) {
1562
+ logger.error({ err: ackError }, 'failed to ack message after error')
1563
+ }
1496
1564
  }
1497
1565
  }
1498
1566
 
@@ -1697,19 +1765,36 @@ const makeMessagesRecvSocket = (config) => {
1697
1765
  isProcessing = true
1698
1766
 
1699
1767
  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
1768
+ // PENTING: sebelumnya kalau nodeProcessor(node) throw (misal bug upstream
1769
+ // baileys "No participant in group message" pas decrypt node offline yang
1770
+ // malformed), exception itu keluar dari while-loop ini SEBELUM sempat
1771
+ // `isProcessing = false`. Akibatnya isProcessing kepentok true SELAMANYA,
1772
+ // dan enqueue() di bawah selalu langsung `return` tanpa pernah mulai
1773
+ // proses lagi -> semua pesan sesudahnya cuma numpuk di array `nodes` dan
1774
+ // gak pernah diproses lagi sampai bot di-restart total. Ini kemungkinan
1775
+ // besar penyebab "reconnect tapi gak pernah nerima pesan lagi".
1776
+ try {
1777
+ while (nodes.length && ws.isOpen) {
1778
+ const {
1779
+ type,
1780
+ node
1781
+ } = nodes.shift()
1782
+ const nodeProcessor = nodeProcessorMap.get(type)
1783
+ if (!nodeProcessor) {
1784
+ onUnexpectedError(new Error(`unknown offline node type: ${type}`), 'processing offline node')
1785
+ continue
1786
+ }
1787
+ try {
1788
+ await nodeProcessor(node)
1789
+ } catch (error) {
1790
+ // Satu node gagal (misal pesan corrupt/participant kosong) TIDAK
1791
+ // boleh ngeblok node-node lain di belakangnya dalam antrian.
1792
+ onUnexpectedError(error, 'processing offline node')
1793
+ }
1709
1794
  }
1710
- await nodeProcessor(node)
1795
+ } finally {
1796
+ isProcessing = false
1711
1797
  }
1712
- isProcessing = false
1713
1798
  }
1714
1799
  promise().catch(error => onUnexpectedError(error, 'processing offline nodes'))
1715
1800
  }
@@ -1721,28 +1806,31 @@ const makeMessagesRecvSocket = (config) => {
1721
1806
 
1722
1807
  const offlineNodeProcessor = makeOfflineNodeProcessor()
1723
1808
 
1724
- const processNode = (type, node, identifier, exec) => {
1809
+ const processNode = async (type, node, identifier, exec) => {
1725
1810
  const isOffline = !!node.attrs.offline
1726
1811
 
1727
1812
  if (isOffline) {
1728
1813
  offlineNodeProcessor.enqueue(type, node)
1729
1814
  } else {
1730
- processNodeWithBuffer(node, identifier, exec)
1815
+ await processNodeWithBuffer(node, identifier, exec)
1731
1816
  }
1732
1817
  }
1733
1818
 
1734
1819
  // recv a message
1735
- ws.on('CB:message', (node) => {
1736
- processNode('message', node, 'processing message', handleMessage)
1820
+ // Semua handler di-await supaya event 'CB:message'/'CB:call'/'CB:receipt'/'CB:notification'
1821
+ // yang datang beruntun tidak diproses bersamaan tanpa urutan (race condition di state
1822
+ // bersama seperti session & cache).
1823
+ ws.on('CB:message', async (node) => {
1824
+ await processNode('message', node, 'processing message', handleMessage)
1737
1825
  })
1738
1826
  ws.on('CB:call', async (node) => {
1739
- processNode('call', node, 'handling call', handleCall)
1827
+ await processNode('call', node, 'handling call', handleCall)
1740
1828
  })
1741
- ws.on('CB:receipt', node => {
1742
- processNode('receipt', node, 'handling receipt', handleReceipt)
1829
+ ws.on('CB:receipt', async (node) => {
1830
+ await processNode('receipt', node, 'handling receipt', handleReceipt)
1743
1831
  })
1744
1832
  ws.on('CB:notification', async (node) => {
1745
- processNode('notification', node, 'handling notification', handleNotification)
1833
+ await processNode('notification', node, 'handling notification', handleNotification)
1746
1834
  })
1747
1835
  ws.on('CB:ack,class:message', (node) => {
1748
1836
  handleBadAck(node)
@@ -826,7 +826,7 @@ const makeMessagesSocket = (config) => {
826
826
  }
827
827
  }
828
828
 
829
- if (!ai && isPrivate) {
829
+ if (ai && isPrivate) {
830
830
  const botNode = {
831
831
  tag: 'bot',
832
832
  attrs: {
@@ -23,13 +23,19 @@ const fileLocks = new Map()
23
23
  const getFileLock = (path) => {
24
24
  let mutex = fileLocks.get(path)
25
25
  if (!mutex) {
26
- mutex = new async_mutex_1.Mutex()
26
+ mutex = new async_mutex_1.Mutex()
27
27
  fileLocks.set(path, mutex)
28
28
  }
29
29
 
30
30
  return mutex
31
31
  }
32
32
 
33
+ // Lepas mutex dari Map begitu tidak dipakai lagi supaya tidak numpuk terus (memory leak)
34
+ // selama proses hidup lama (bot jalan berbulan-bulan dengan ribuan file sesi berbeda).
35
+ const releaseFileLock = (path) => {
36
+ fileLocks.delete(path)
37
+ }
38
+
33
39
  /**
34
40
  * stores the full authentication state in a single folder.
35
41
  * Far more efficient than singlefileauthstate
@@ -38,51 +44,56 @@ const getFileLock = (path) => {
38
44
  * Would recommend writing an auth state for use with a proper SQL or No-SQL DB
39
45
  * */
40
46
  const useMultiFileAuthState = async (folder) => {
41
- // Debounce timers per file to avoid rapid successive writes hammering disk
42
- const writeDebounceTimers = new Map()
43
- const WRITE_DEBOUNCE_MS = 300
44
-
45
47
  // eslint-disable-next-line @typescript-eslint/no-explicit-any
46
48
  const writeData = async (data, file) => {
47
49
  const filePath = path_1.join(folder, fixFileName(file))
48
50
  const mutex = getFileLock(filePath)
49
51
  return mutex.acquire().then(async (release) => {
50
52
  try {
51
- // Atomic write: write to temp file then rename to prevent corruption
53
+ // Atomic write: write to temp file then rename, agar file tidak pernah
54
+ // kebaca dalam kondisi setengah tertulis kalau proses mati di tengah jalan.
52
55
  const tmpPath = filePath + '.tmp'
53
56
  await promises_1.writeFile(tmpPath, JSON.stringify(data, generics_1.BufferJSON.replacer))
54
57
  await fsRename(tmpPath, filePath)
55
58
  } finally {
56
59
  release()
60
+ releaseFileLock(filePath)
57
61
  }
58
62
  })
59
63
  }
60
64
  const readData = async (file) => {
65
+ const filePath = path_1.join(folder, fixFileName(file))
61
66
  try {
62
- const filePath = path_1.join(folder, fixFileName(file))
63
67
  const mutex = getFileLock(filePath)
64
68
  const data = await mutex.acquire().then(async (release) => {
65
69
  try {
66
70
  return await promises_1.readFile(filePath, { encoding: 'utf-8' })
67
71
  } finally {
68
72
  release()
73
+ releaseFileLock(filePath)
69
74
  }
70
75
  })
71
76
 
72
77
  return JSON.parse(data, generics_1.BufferJSON.reviver)
73
78
  } catch (error) {
79
+ // ENOENT (file belum ada) itu normal, tapi kalau isinya rusak/corrupt (JSON parse
80
+ // gagal) itu wajib kelihatan di log, bukan diam-diam dianggap "null"/hilang.
81
+ if (error?.code !== 'ENOENT') {
82
+ console.warn(`[useMultiFileAuthState] Gagal membaca/parse ${filePath}:`, error?.message || error)
83
+ }
74
84
  return null
75
85
  }
76
86
  }
77
87
  const removeData = async (file) => {
88
+ const filePath = path_1.join(folder, fixFileName(file))
78
89
  try {
79
- const filePath = path_1.join(folder, fixFileName(file))
80
90
  const mutex = getFileLock(filePath)
81
91
  await mutex.acquire().then(async (release) => {
82
92
  try {
83
93
  await promises_1.unlink(filePath)
84
94
  } finally {
85
95
  release()
96
+ releaseFileLock(filePath)
86
97
  }
87
98
  })
88
99
  } catch {}
@@ -96,9 +107,22 @@ const useMultiFileAuthState = async (folder) => {
96
107
  else {
97
108
  await promises_1.mkdir(folder, { recursive: true })
98
109
  }
99
- const fixFileName = (file) => {
100
- return file?.replace(/\//g, '__')?.replace(/:/g, '-')
110
+ const fixFileName = (file) => {
111
+ return file?.replace(/\//g, '__')?.replace(/:/g, '-')
101
112
  }
113
+
114
+ // Bersihkan file .tmp "yatim" yang tertinggal dari proses sebelumnya yang mati
115
+ // di tengah proses rename (atomic write). File-file ini tidak berguna dan kalau
116
+ // dibiarkan menumpuk, cuma buang-buang disk.
117
+ try {
118
+ const entries = await promises_1.readdir(folder)
119
+ await Promise.all(
120
+ entries
121
+ .filter((name) => name.endsWith('.tmp'))
122
+ .map((name) => promises_1.unlink(path_1.join(folder, name)).catch(() => {}))
123
+ )
124
+ } catch {}
125
+
102
126
  const creds = await readData('creds.json') || auth_utils_1.initAuthCreds()
103
127
  return {
104
128
  state: {
@@ -128,24 +152,15 @@ const useMultiFileAuthState = async (folder) => {
128
152
  }
129
153
  }
130
154
  },
131
- // saveCreds debounced by default aman dipanggil tiap creds.update
132
- // tanpa spam disk. Untuk force immediate write, pakai saveCredsNow.
133
- saveCreds: () => {
134
- const existing = writeDebounceTimers.get('creds.json')
135
- if (existing) clearTimeout(existing)
136
- const timer = setTimeout(() => {
137
- writeDebounceTimers.delete('creds.json')
138
- writeData(creds, 'creds.json').catch(err => {
139
- console.error('[useMultiFileAuthState] Failed to save creds.json:', err)
140
- })
141
- }, WRITE_DEBOUNCE_MS)
142
- writeDebounceTimers.set('creds.json', timer)
155
+ // saveCreds sekarang selalu langsung menulis (tidak didebounce). creds.json adalah
156
+ // file paling kritis kalau proses mati tepat di jendela debounce, sesi bisa hilang
157
+ // atau balik ke state lama sehingga semua pesan gagal didekripsi setelah reconnect.
158
+ saveCreds: async () => {
159
+ return writeData(creds, 'creds.json')
143
160
  },
144
- // Force immediate write pakai saat disconnect/cleanup agar creds tidak hilang
161
+ // Tetap disediakan sebagai alias untuk kompatibilitas kode lama yang memanggil
162
+ // saveCredsNow() secara eksplisit (misal di handler disconnect/cleanup).
145
163
  saveCredsNow: async () => {
146
- const existing = writeDebounceTimers.get('creds.json')
147
- if (existing) clearTimeout(existing)
148
- writeDebounceTimers.delete('creds.json')
149
164
  return writeData(creds, 'creds.json')
150
165
  }
151
166
  }
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@pontasockets/baileys",
3
- "version": "1.0.1",
3
+ "version": "1.0.3",
4
4
  "description": "Baileys is a lightweight JavaScript library for interacting with the WhatsApp Web API using WebSocket.",
5
5
  "keywords": [
6
6
  "facebook",