@pontalabs/baileys 1.0.2 → 1.0.4
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/messages-send.js +10 -4
- package/lib/Socket/socket.js +17 -0
- package/lib/Utils/auth-utils.js +111 -96
- package/lib/Utils/decode-wa-message.js +18 -1
- package/lib/Utils/index.d.ts +2 -1
- package/lib/Utils/index.js +2 -1
- package/lib/Utils/signal-housekeeping.d.ts +19 -0
- package/lib/Utils/signal-housekeeping.js +73 -0
- package/package.json +1 -1
|
@@ -360,11 +360,17 @@ const makeMessagesSocket = (config) => {
|
|
|
360
360
|
if (force) {
|
|
361
361
|
jidsRequiringFetch = jids;
|
|
362
362
|
} else {
|
|
363
|
-
|
|
364
|
-
|
|
363
|
+
// FIX: sebelumnya cuma ngecek "ada record tersimpan atau enggak"
|
|
364
|
+
// (truthy check di authState.keys.get('session', ...)). Record
|
|
365
|
+
// yang kosong/stale/rusak (misal device grup yang session-nya
|
|
366
|
+
// kebentuk tapi gak pernah kepakai/keisi ratchet) tetep truthy,
|
|
367
|
+
// jadi dianggap "udah punya sesi" padahal libsignal-nya beneran
|
|
368
|
+
// kosong -> pas encrypt muncul "SessionError: No sessions".
|
|
369
|
+
// validateSession ngecek session-nya beneran ada & "open" enggak,
|
|
370
|
+
// bukan cuma record-nya ada.
|
|
365
371
|
for (const jid of jids) {
|
|
366
|
-
const
|
|
367
|
-
if (!
|
|
372
|
+
const sessionValidation = await signalRepository.validateSession(jid);
|
|
373
|
+
if (!sessionValidation.exists) {
|
|
368
374
|
jidsRequiringFetch.push(jid);
|
|
369
375
|
}
|
|
370
376
|
}
|
package/lib/Socket/socket.js
CHANGED
|
@@ -197,6 +197,12 @@ const makeSocket = (config) => {
|
|
|
197
197
|
let lastDateRecv
|
|
198
198
|
let epoch = 1
|
|
199
199
|
let keepAliveReq
|
|
200
|
+
// FIX: sebelumnya uploadPreKeysToServerIfRequired() cuma dipanggil SEKALI pas
|
|
201
|
+
// 'CB:success' (baru connect/reconnect). Kalau proses bot jalan lama (berhari-hari)
|
|
202
|
+
// tanpa reconnect, prekey yang kepakai server gak pernah dicek ulang sampai jumlahnya
|
|
203
|
+
// abis - baru ketahuan pas sudah kejadian error decrypt di penerima. Interval ini
|
|
204
|
+
// bikin prekey selalu "terupdate", dicek ulang berkala tanpa perlu nunggu reconnect.
|
|
205
|
+
let preKeyCheckInterval
|
|
200
206
|
let qrTimer
|
|
201
207
|
let closed = false
|
|
202
208
|
|
|
@@ -516,6 +522,7 @@ const makeSocket = (config) => {
|
|
|
516
522
|
trace: error?.stack
|
|
517
523
|
}, error ? 'connection errored' : 'connection closed')
|
|
518
524
|
clearInterval(keepAliveReq)
|
|
525
|
+
clearInterval(preKeyCheckInterval)
|
|
519
526
|
clearTimeout(qrTimer)
|
|
520
527
|
ws.removeAllListeners('close')
|
|
521
528
|
ws.removeAllListeners('open')
|
|
@@ -822,6 +829,16 @@ const makeSocket = (config) => {
|
|
|
822
829
|
ws.on('CB:success', async (node) => {
|
|
823
830
|
try {
|
|
824
831
|
await uploadPreKeysToServerIfRequired()
|
|
832
|
+
// Jalankan ulang pengecekan prekey tiap 6 jam selama koneksi hidup, bukan
|
|
833
|
+
// cuma sekali pas connect. uploadPreKeysToServerIfRequired() sendiri sudah
|
|
834
|
+
// pintar (cuma benar-benar upload kalau count di server rendah/ada yang
|
|
835
|
+
// hilang), jadi interval ini murah dan aman dijalankan berkala.
|
|
836
|
+
clearInterval(preKeyCheckInterval)
|
|
837
|
+
preKeyCheckInterval = setInterval(() => {
|
|
838
|
+
uploadPreKeysToServerIfRequired().catch((err) => {
|
|
839
|
+
logger.warn({ err }, 'periodic pre-key check failed')
|
|
840
|
+
})
|
|
841
|
+
}, 6 * 60 * 60 * 1000)
|
|
825
842
|
await sendPassiveIq('active')
|
|
826
843
|
} catch (err) {
|
|
827
844
|
logger.warn({
|
package/lib/Utils/auth-utils.js
CHANGED
|
@@ -81,92 +81,96 @@ function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
|
81
81
|
}
|
|
82
82
|
}
|
|
83
83
|
|
|
84
|
-
// Module-level specialized mutexes for pre-key operations
|
|
85
|
-
const preKeyMutex = new mutex_1.Mutex()
|
|
86
|
-
const signedPreKeyMutex = new mutex_1.Mutex()
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Get the appropriate mutex for the key type
|
|
90
|
-
*/
|
|
91
|
-
const getPreKeyMutex = (keyType) => {
|
|
92
|
-
return keyType === 'signed-pre-key' ? signedPreKeyMutex : preKeyMutex
|
|
93
|
-
}
|
|
94
|
-
|
|
95
84
|
/**
|
|
96
|
-
* Handles pre-key operations
|
|
85
|
+
* Handles pre-key operations.
|
|
86
|
+
*
|
|
87
|
+
* IMPORTANT: locking for this function is provided by the CALLER via the
|
|
88
|
+
* session-scoped `getKeyTypeMutex(keyType)` mutex (the same one used by
|
|
89
|
+
* get()/set() for that key type). We intentionally do NOT acquire our own
|
|
90
|
+
* mutex here anymore.
|
|
91
|
+
*
|
|
92
|
+
* Previously this used module-level singleton mutexes (`preKeyMutex`,
|
|
93
|
+
* `signedPreKeyMutex`) declared OUTSIDE `addTransactionCapability`. Because
|
|
94
|
+
* those were created once per process (not once per auth state / socket),
|
|
95
|
+
* every session running in the same Node process shared the exact same
|
|
96
|
+
* lock. That caused two problems:
|
|
97
|
+
* 1. Unrelated sessions serialized on each other's pre-key writes
|
|
98
|
+
* (needless contention/latency when running multiple accounts).
|
|
99
|
+
* 2. The lock domain didn't match the one used by get()/withMutexes()
|
|
100
|
+
* (`getKeyTypeMutex`), so reads/writes and deletion-validation could
|
|
101
|
+
* interleave without real mutual exclusion (TOCTOU race on the
|
|
102
|
+
* "does this key still exist" check), which is what let pre-key /
|
|
103
|
+
* signed-pre-key state get corrupted under concurrent access.
|
|
97
104
|
*/
|
|
98
105
|
async function handlePreKeyOperations(data, keyType, transactionCache, mutations, logger, isInTransaction, state) {
|
|
99
|
-
const
|
|
100
|
-
|
|
101
|
-
|
|
102
|
-
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
110
|
-
|
|
111
|
-
|
|
112
|
-
|
|
113
|
-
|
|
114
|
-
if (keyData[keyId] === null) {
|
|
115
|
-
deletionKeys.push(keyId)
|
|
116
|
-
}
|
|
117
|
-
else {
|
|
118
|
-
updateKeys.push(keyId)
|
|
119
|
-
}
|
|
106
|
+
const keyData = data[keyType]
|
|
107
|
+
if (!keyData)
|
|
108
|
+
return
|
|
109
|
+
|
|
110
|
+
// Ensure structures exist
|
|
111
|
+
transactionCache[keyType] = transactionCache[keyType] || {}
|
|
112
|
+
mutations[keyType] = mutations[keyType] || {}
|
|
113
|
+
|
|
114
|
+
// Separate deletions from updates for batch processing
|
|
115
|
+
const deletionKeys = []
|
|
116
|
+
const updateKeys = []
|
|
117
|
+
|
|
118
|
+
for (const keyId in keyData) {
|
|
119
|
+
if (keyData[keyId] === null) {
|
|
120
|
+
deletionKeys.push(keyId)
|
|
120
121
|
}
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
for (const keyId of updateKeys) {
|
|
124
|
-
if (transactionCache[keyType]) {
|
|
125
|
-
transactionCache[keyType][keyId] = keyData[keyId]
|
|
126
|
-
}
|
|
127
|
-
if (mutations[keyType]) {
|
|
128
|
-
mutations[keyType][keyId] = keyData[keyId]
|
|
129
|
-
}
|
|
122
|
+
else {
|
|
123
|
+
updateKeys.push(keyId)
|
|
130
124
|
}
|
|
131
|
-
|
|
132
|
-
|
|
133
|
-
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
// In transaction, only allow deletion if key exists in cache
|
|
138
|
-
for (const keyId of deletionKeys) {
|
|
139
|
-
if (transactionCache[keyType]) {
|
|
140
|
-
transactionCache[keyType][keyId] = null
|
|
141
|
-
if (mutations[keyType]) {
|
|
142
|
-
// Mark for deletion in mutations
|
|
143
|
-
mutations[keyType][keyId] = null
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
else {
|
|
147
|
-
logger.warn(`Skipping deletion of non-existent ${keyType} in transaction: ${keyId}`)
|
|
148
|
-
}
|
|
149
|
-
}
|
|
150
|
-
return
|
|
125
|
+
}
|
|
126
|
+
|
|
127
|
+
// Process updates first (no validation needed)
|
|
128
|
+
for (const keyId of updateKeys) {
|
|
129
|
+
if (transactionCache[keyType]) {
|
|
130
|
+
transactionCache[keyType][keyId] = keyData[keyId]
|
|
151
131
|
}
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
132
|
+
if (mutations[keyType]) {
|
|
133
|
+
mutations[keyType][keyId] = keyData[keyId]
|
|
134
|
+
}
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
// Process deletions with validation
|
|
138
|
+
if (deletionKeys.length === 0)
|
|
139
|
+
return
|
|
140
|
+
|
|
141
|
+
if (isInTransaction) {
|
|
142
|
+
// In transaction, only allow deletion if key exists in cache
|
|
158
143
|
for (const keyId of deletionKeys) {
|
|
159
|
-
if (
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
144
|
+
if (transactionCache[keyType]) {
|
|
145
|
+
transactionCache[keyType][keyId] = null
|
|
146
|
+
if (mutations[keyType]) {
|
|
147
|
+
// Mark for deletion in mutations
|
|
163
148
|
mutations[keyType][keyId] = null
|
|
149
|
+
}
|
|
164
150
|
}
|
|
165
151
|
else {
|
|
166
|
-
logger.warn(`Skipping deletion of non-existent ${keyType}: ${keyId}`)
|
|
152
|
+
logger.warn(`Skipping deletion of non-existent ${keyType} in transaction: ${keyId}`)
|
|
167
153
|
}
|
|
168
154
|
}
|
|
169
|
-
|
|
155
|
+
return
|
|
156
|
+
}
|
|
157
|
+
|
|
158
|
+
// Outside transaction, batch validate all deletions
|
|
159
|
+
if (!state)
|
|
160
|
+
return
|
|
161
|
+
|
|
162
|
+
const existingKeys = await state.get(keyType, deletionKeys)
|
|
163
|
+
for (const keyId of deletionKeys) {
|
|
164
|
+
if (existingKeys[keyId]) {
|
|
165
|
+
if (transactionCache[keyType])
|
|
166
|
+
transactionCache[keyType][keyId] = null
|
|
167
|
+
if (mutations[keyType])
|
|
168
|
+
mutations[keyType][keyId] = null
|
|
169
|
+
}
|
|
170
|
+
else {
|
|
171
|
+
logger.warn(`Skipping deletion of non-existent ${keyType}: ${keyId}`)
|
|
172
|
+
}
|
|
173
|
+
}
|
|
170
174
|
}
|
|
171
175
|
|
|
172
176
|
/**
|
|
@@ -179,27 +183,34 @@ function handleNormalKeyOperations(data, key, transactionCache, mutations) {
|
|
|
179
183
|
}
|
|
180
184
|
|
|
181
185
|
/**
|
|
182
|
-
* Process pre-key deletions with validation
|
|
186
|
+
* Process pre-key / signed-pre-key deletions with validation.
|
|
187
|
+
*
|
|
188
|
+
* NOTE: this is only ever called from inside a `withMutexes([..., keyType, ...])`
|
|
189
|
+
* block in `set()` below, which already holds the session-scoped
|
|
190
|
+
* `getKeyTypeMutex(keyType)` lock for the duration of the call. It must NOT
|
|
191
|
+
* acquire its own mutex here - `async-mutex`'s Mutex is not reentrant, so
|
|
192
|
+
* doing so would either deadlock (if it tried to re-acquire the same lock)
|
|
193
|
+
* or - as in the previous version - silently acquire a *different*, global
|
|
194
|
+
* lock that gave no real protection at all.
|
|
183
195
|
*/
|
|
184
196
|
async function processPreKeyDeletions(data, keyType, state, logger) {
|
|
185
|
-
const
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
}
|
|
197
|
+
const keyData = data[keyType]
|
|
198
|
+
if (!keyData)
|
|
199
|
+
return
|
|
200
|
+
|
|
201
|
+
// Validate deletions
|
|
202
|
+
const deletionIds = Object.keys(keyData).filter(id => keyData[id] === null)
|
|
203
|
+
if (deletionIds.length === 0)
|
|
204
|
+
return
|
|
205
|
+
|
|
206
|
+
const existingKeys = await state.get(keyType, deletionIds)
|
|
207
|
+
for (const keyId of deletionIds) {
|
|
208
|
+
if (!existingKeys[keyId]) {
|
|
209
|
+
logger.warn(`Skipping deletion of non-existent ${keyType}: ${keyId}`)
|
|
210
|
+
if (data[keyType])
|
|
211
|
+
delete data[keyType][keyId]
|
|
201
212
|
}
|
|
202
|
-
}
|
|
213
|
+
}
|
|
203
214
|
}
|
|
204
215
|
|
|
205
216
|
/**
|
|
@@ -403,9 +414,13 @@ const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetwee
|
|
|
403
414
|
for (const key_ in data) {
|
|
404
415
|
const key = key_
|
|
405
416
|
transactionCache[key] = transactionCache[key] || {}
|
|
406
|
-
// Special handling for pre-keys and signed-pre-keys
|
|
417
|
+
// Special handling for pre-keys and signed-pre-keys.
|
|
418
|
+
// Locked via the session-scoped getKeyTypeMutex(key) so this can't
|
|
419
|
+
// interleave with a concurrent non-transactional get()/set() for the
|
|
420
|
+
// same key type on this SAME auth state - no cross-session locking,
|
|
421
|
+
// no lock-domain mismatch.
|
|
407
422
|
if (key === 'pre-key' || key === 'signed-pre-key') {
|
|
408
|
-
await handlePreKeyOperations(data, key, transactionCache, mutations, logger, true)
|
|
423
|
+
await getKeyTypeMutex(key).runExclusive(() => handlePreKeyOperations(data, key, transactionCache, mutations, logger, true))
|
|
409
424
|
}
|
|
410
425
|
else {
|
|
411
426
|
// Normal handling for other key types
|
|
@@ -442,7 +457,7 @@ const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetwee
|
|
|
442
457
|
// Process pre-keys and signed-pre-keys separately with specialized mutexes
|
|
443
458
|
for (const key_ in nonSenderKeyData) {
|
|
444
459
|
const keyType = key_
|
|
445
|
-
if (keyType === 'pre-key') {
|
|
460
|
+
if (keyType === 'pre-key' || keyType === 'signed-pre-key') {
|
|
446
461
|
await processPreKeyDeletions(nonSenderKeyData, keyType, state, logger)
|
|
447
462
|
}
|
|
448
463
|
}
|
|
@@ -457,7 +472,7 @@ const addTransactionCapability = (state, logger, { maxCommitRetries, delayBetwee
|
|
|
457
472
|
// Process pre-keys and signed-pre-keys separately with specialized mutexes
|
|
458
473
|
for (const key_ in data) {
|
|
459
474
|
const keyType = key_
|
|
460
|
-
if (keyType === 'pre-key') {
|
|
475
|
+
if (keyType === 'pre-key' || keyType === 'signed-pre-key') {
|
|
461
476
|
await processPreKeyDeletions(data, keyType, state, logger)
|
|
462
477
|
}
|
|
463
478
|
}
|
|
@@ -222,7 +222,24 @@ const decryptMessageNode = (stanza, meId, meLid, repository, logger) => {
|
|
|
222
222
|
async decrypt() {
|
|
223
223
|
let decryptables = 0
|
|
224
224
|
if (Array.isArray(stanza.content)) {
|
|
225
|
-
|
|
225
|
+
// FIX: proses node non-skmsg (msg/pkmsg/plaintext) LEBIH DULU sebelum node
|
|
226
|
+
// skmsg. Root cause "bot diam/nunggu" pas ada member baru join grup lalu
|
|
227
|
+
// langsung kirim pesan: satu stanza berisi 2 node <enc> - satu tipe
|
|
228
|
+
// pkmsg/msg (isinya SenderKeyDistributionMessage buat kamu) dan satu lagi
|
|
229
|
+
// tipe skmsg (isi pesan asli, dienkripsi pakai sender-key grup). Urutan
|
|
230
|
+
// array stanza.content dari server TIDAK dijamin naruh pkmsg/msg duluan.
|
|
231
|
+
// Kalau skmsg kebetulan diproses duluan, sender-key belum ada di store ->
|
|
232
|
+
// decryptGroupMessage() gagal "No SenderKeyRecord found" -> pesan pertama
|
|
233
|
+
// dari member baru selalu gagal decrypt & jadi stub CIPHERTEXT, padahal
|
|
234
|
+
// SKDM-nya baru "sampai" sepersekian detik kemudian di iterasi berikutnya.
|
|
235
|
+
// Reorder di sini (stable, gak ubah urutan relatif node lain seperti
|
|
236
|
+
// verified_name/unavailable) supaya skmsg selalu diproses PALING TERAKHIR,
|
|
237
|
+
// setelah semua kemungkinan SKDM sudah didaftarkan ke sender-key store.
|
|
238
|
+
const orderedContent = [
|
|
239
|
+
...stanza.content.filter(({ tag, attrs }) => !(tag === 'enc' && attrs.type === 'skmsg')),
|
|
240
|
+
...stanza.content.filter(({ tag, attrs }) => tag === 'enc' && attrs.type === 'skmsg')
|
|
241
|
+
]
|
|
242
|
+
for (const { tag, attrs, content } of orderedContent) {
|
|
226
243
|
if (tag === 'verified_name' && content instanceof Uint8Array) {
|
|
227
244
|
const cert = WAProto_1.proto.VerifiedNameCertificate.decode(content)
|
|
228
245
|
const details = WAProto_1.proto.VerifiedNameCertificate.Details.decode(cert.details)
|
package/lib/Utils/index.d.ts
CHANGED
|
@@ -17,4 +17,5 @@ export * from './use-single-file-auth-state'
|
|
|
17
17
|
export * from './use-multi-file-auth-state'
|
|
18
18
|
export * from './link-preview'
|
|
19
19
|
export * from './event-buffer'
|
|
20
|
-
export * from './process-message'
|
|
20
|
+
export * from './process-message'
|
|
21
|
+
export * from './signal-housekeeping'
|
package/lib/Utils/index.js
CHANGED
|
@@ -37,4 +37,5 @@ __exportStar(require("./use-single-file-auth-state"), exports)
|
|
|
37
37
|
__exportStar(require("./use-multi-file-auth-state"), exports)
|
|
38
38
|
__exportStar(require("./link-preview"), exports)
|
|
39
39
|
__exportStar(require("./event-buffer"), exports)
|
|
40
|
-
__exportStar(require("./process-message"), exports)
|
|
40
|
+
__exportStar(require("./process-message"), exports)
|
|
41
|
+
__exportStar(require("./signal-housekeeping"), exports)
|
|
@@ -0,0 +1,19 @@
|
|
|
1
|
+
import type { Logger } from 'pino'
|
|
2
|
+
|
|
3
|
+
export interface CleanupSignalAuthFolderOptions {
|
|
4
|
+
/** File yang mtime-nya lebih tua dari ini (dalam hari) akan dihapus. Default 30. */
|
|
5
|
+
maxAgeDays?: number
|
|
6
|
+
/** Prefix nama file yang boleh dibersihkan. Default ['session-', 'sender-key-']. */
|
|
7
|
+
prefixes?: string[]
|
|
8
|
+
logger?: Logger
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
export interface CleanupSignalAuthFolderResult {
|
|
12
|
+
removed: string[]
|
|
13
|
+
scanned: number
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
export function cleanupSignalAuthFolder(
|
|
17
|
+
folder: string,
|
|
18
|
+
options?: CleanupSignalAuthFolderOptions
|
|
19
|
+
): Promise<CleanupSignalAuthFolderResult>
|
|
@@ -0,0 +1,73 @@
|
|
|
1
|
+
"use strict"
|
|
2
|
+
|
|
3
|
+
Object.defineProperty(exports, "__esModule", { value: true })
|
|
4
|
+
exports.cleanupSignalAuthFolder = cleanupSignalAuthFolder
|
|
5
|
+
|
|
6
|
+
const fs = require("fs/promises")
|
|
7
|
+
const path = require("path")
|
|
8
|
+
|
|
9
|
+
/**
|
|
10
|
+
* Bersihkan file session/sender-key yang sudah usang di folder auth yang dipakai
|
|
11
|
+
* bareng useMultiFileAuthState(). Tujuannya supaya store signal tidak numpuk terus
|
|
12
|
+
* selama bot jalan berbulan-bulan dengan ribuan kontak/grup yang sudah gak aktif.
|
|
13
|
+
*
|
|
14
|
+
* Aman dipakai karena: mtime sebuah file session-*.json / sender-key-*.json selalu
|
|
15
|
+
* ke-update setiap kali ada pertukaran pesan dengan kontak/grup itu (storeSession /
|
|
16
|
+
* storeSenderKey menulis ulang filenya). Jadi kalau sebuah file gak pernah disentuh
|
|
17
|
+
* lagi lebih dari `maxAgeDays`, itu tandanya kontak/grup itu memang sudah gak aktif -
|
|
18
|
+
* hapus filenya aman, nanti kalau ada pesan baru sesi/sender-key otomatis dibuat ulang
|
|
19
|
+
* dari prekey bundle / SenderKeyDistributionMessage yang baru.
|
|
20
|
+
*
|
|
21
|
+
* creds.json, app-state-sync-key-*.json, dan pre-key-*.json SENGAJA tidak disentuh
|
|
22
|
+
* di sini secara default - itu bukan sumber "numpuk", dan prekey yang sudah dipakai
|
|
23
|
+
* memang sudah dihapus otomatis lewat removePreKey() di libsignal.js.
|
|
24
|
+
*
|
|
25
|
+
* @param {string} folder - folder yang sama dipakai di useMultiFileAuthState()
|
|
26
|
+
* @param {object} [options]
|
|
27
|
+
* @param {number} [options.maxAgeDays=30] - file yang mtime-nya lebih tua dari ini akan dihapus
|
|
28
|
+
* @param {string[]} [options.prefixes] - prefix nama file yang boleh dibersihkan
|
|
29
|
+
* @param {import('pino').Logger} [options.logger]
|
|
30
|
+
* @returns {Promise<{ removed: string[], scanned: number }>}
|
|
31
|
+
*/
|
|
32
|
+
async function cleanupSignalAuthFolder(folder, options = {}) {
|
|
33
|
+
const {
|
|
34
|
+
maxAgeDays = 30,
|
|
35
|
+
prefixes = ['session-', 'sender-key-'],
|
|
36
|
+
logger
|
|
37
|
+
} = options
|
|
38
|
+
|
|
39
|
+
const removed = []
|
|
40
|
+
let scanned = 0
|
|
41
|
+
const maxAgeMs = maxAgeDays * 24 * 60 * 60 * 1000
|
|
42
|
+
const now = Date.now()
|
|
43
|
+
|
|
44
|
+
let entries
|
|
45
|
+
try {
|
|
46
|
+
entries = await fs.readdir(folder)
|
|
47
|
+
} catch (err) {
|
|
48
|
+
logger?.warn?.({ err, folder }, '[cleanupSignalAuthFolder] gagal baca folder auth')
|
|
49
|
+
return { removed, scanned }
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
for (const name of entries) {
|
|
53
|
+
if (!prefixes.some((p) => name.startsWith(p))) {
|
|
54
|
+
continue
|
|
55
|
+
}
|
|
56
|
+
scanned++
|
|
57
|
+
const filePath = path.join(folder, name)
|
|
58
|
+
try {
|
|
59
|
+
const stat = await fs.stat(filePath)
|
|
60
|
+
if (now - stat.mtimeMs > maxAgeMs) {
|
|
61
|
+
await fs.unlink(filePath)
|
|
62
|
+
removed.push(name)
|
|
63
|
+
}
|
|
64
|
+
} catch {
|
|
65
|
+
// File mungkin sudah kehapus proses lain (race antar-worker), aman diabaikan.
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
logger?.info?.({ scanned, removedCount: removed.length, maxAgeDays }, '[cleanupSignalAuthFolder] selesai')
|
|
70
|
+
return { removed, scanned }
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
module.exports = { cleanupSignalAuthFolder }
|