@vanzxy/baileys 1.5.7 → 1.5.9

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.
@@ -712,35 +712,82 @@ export const makeChatsSocket = (config) => {
712
712
  // has a courtesy retry via blockedCollections + the creds.update listener below — the key
713
713
  // arrives async after connect (APP_STATE_SYNC_KEY_SHARE), so calling e.g. updateProfileName()
714
714
  // right after connection.update === 'open' would reliably fail on a fresh/early connection.
715
- // Now appPatch() waits (bounded, 10s default) for the same creds.update event before giving up,
715
+ // Now appPatch() waits (bounded, 20s default) for the same creds.update event before giving up,
716
716
  // instead of failing hard on what's usually just a timing race.
717
- const waitForAppStateKeyId = (timeoutMs = 10_000) => {
717
+ //
718
+ // Vanz@Fix 25-08-26 --- the event-only wait had a blind spot: if `creds.update` never actually
719
+ // reaches this listener (swallowed/filtered by a wrapper around `ev`, a custom auth-state
720
+ // adapter that patches `authState.creds` directly without re-emitting the event, etc.) this
721
+ // would sit for the full timeout and fail even though `authState.creds.myAppStateKeyId` was
722
+ // genuinely set the whole time. Added a belt-and-suspenders poll of the creds object itself
723
+ // alongside the event listener — whichever one notices the key first wins, and both are torn
724
+ // down together so there's no leaked timer/listener either way.
725
+ const waitForAppStateKeyId = (timeoutMs = 20_000, pollMs = 500) => {
718
726
  if (authState.creds.myAppStateKeyId) {
719
727
  return Promise.resolve(authState.creds.myAppStateKeyId);
720
728
  }
721
729
  return new Promise((resolve, reject) => {
722
- const timeout = setTimeout(() => {
730
+ let settled = false;
731
+ const cleanup = () => {
732
+ settled = true;
733
+ clearTimeout(timeout);
734
+ clearInterval(poller);
723
735
  ev.off('creds.update', onUpdate);
736
+ };
737
+ const timeout = setTimeout(() => {
738
+ if (settled) return;
739
+ cleanup();
740
+ logger.warn(
741
+ { hasMyAppStateKeyId: !!authState.creds.myAppStateKeyId, registered: authState.creds.registered },
742
+ 'waitForAppStateKeyId timed out — APP_STATE_SYNC_KEY_SHARE was never received/applied for this session'
743
+ );
724
744
  reject(new Boom('App state key not present!', { statusCode: 400 }));
725
745
  }, timeoutMs);
746
+ const poller = setInterval(() => {
747
+ if (settled || !authState.creds.myAppStateKeyId) return;
748
+ const id = authState.creds.myAppStateKeyId;
749
+ cleanup();
750
+ resolve(id);
751
+ }, pollMs);
726
752
  const onUpdate = ({ myAppStateKeyId }) => {
727
- if (!myAppStateKeyId) return;
728
- clearTimeout(timeout);
729
- ev.off('creds.update', onUpdate);
753
+ if (settled || !myAppStateKeyId) return;
754
+ cleanup();
730
755
  resolve(myAppStateKeyId);
731
756
  };
732
757
  ev.on('creds.update', onUpdate);
733
758
  });
734
759
  };
760
+ // Vanz@Fix (perf) --- appPatch() unconditionally round-tripped resyncAppState() to the WA
761
+ // server on EVERY single call, even for back-to-back patches (e.g. updateProfileName then
762
+ // immediately archiving/muting a chat) where the local version was already fresh from the
763
+ // resync a few hundred ms ago. That round trip is the main source of the multi-second delay
764
+ // people notice on things like "ganti nama". Cache the last-synced timestamp per collection
765
+ // name and skip the resync if it happened within RESYNC_TTL_MS — WA's own client does the
766
+ // same kind of short-lived debouncing. Set RESYNC_TTL_MS to 0 to restore the old always-resync
767
+ // behavior if this ever causes a stale-version conflict in practice.
768
+ const RESYNC_TTL_MS = 4000;
769
+ const lastResyncAt = new Map();
735
770
  const appPatch = async (patchCreate) => {
736
771
  const name = patchCreate.type;
772
+ const t0 = Date.now();
737
773
  const myAppStateKeyId = await waitForAppStateKeyId();
774
+ const tKey = Date.now();
738
775
  let initial;
739
776
  let encodeResult;
740
777
  await appStatePatchMutex.mutex(async () => {
741
778
  await authState.keys.transaction(async () => {
742
779
  logger.debug({ patch: patchCreate }, 'applying app patch');
743
- await resyncAppState([name], false);
780
+ const lastSync = lastResyncAt.get(name) ?? 0;
781
+ const freshEnough = RESYNC_TTL_MS > 0 && Date.now() - lastSync < RESYNC_TTL_MS;
782
+ if (!freshEnough) {
783
+ await resyncAppState([name], false);
784
+ lastResyncAt.set(name, Date.now());
785
+ }
786
+ const tResync = Date.now();
787
+ logger.debug(
788
+ { patch: name, waitedForKeyMs: tKey - t0, resyncMs: freshEnough ? 0 : tResync - tKey, skippedResync: freshEnough },
789
+ 'appPatch timing'
790
+ );
744
791
  const { [name]: currentSyncVersion } = await authState.keys.get('app-state-sync-version', [name]);
745
792
  initial = currentSyncVersion ? ensureLTHashStateVersion(currentSyncVersion) : newLTHashState();
746
793
  encodeResult = await encodeSyncdPatch(patchCreate, myAppStateKeyId, initial, getAppStateSyncKey);
@@ -1068,6 +1115,11 @@ export const makeChatsSocket = (config) => {
1068
1115
  if (syncState === SyncState.Syncing) {
1069
1116
  // All collections will be synced, so clear any blocked ones
1070
1117
  blockedCollections.clear();
1118
+ // Vanz@Fix (perf cache invalidation) --- a full sync is about to run for every
1119
+ // collection, so any per-collection "recently resynced" cache from appPatch()
1120
+ // is now stale by definition; drop it so the next appPatch() call resyncs fresh
1121
+ // instead of trusting a pre-full-sync timestamp.
1122
+ lastResyncAt.clear();
1071
1123
  logger.info('Doing app state sync');
1072
1124
  await resyncAppState(ALL_WA_PATCH_NAMES, true);
1073
1125
  // Sync is complete, go online and flush everything
@@ -1129,6 +1181,9 @@ export const makeChatsSocket = (config) => {
1129
1181
  ev.on('connection.update', ({ connection, receivedPendingNotifications }) => {
1130
1182
  if (connection === 'close') {
1131
1183
  blockedCollections.clear();
1184
+ // Vanz@Fix (perf cache invalidation) --- connection dropped, so any "recently
1185
+ // resynced" timestamps are no longer trustworthy once we reconnect.
1186
+ lastResyncAt.clear();
1132
1187
  clearTimeout(historySyncPausedTimeout);
1133
1188
  historySyncPausedTimeout = undefined;
1134
1189
  }
@@ -336,6 +336,14 @@ class Toolkit {
336
336
 
337
337
  const isWAUrl = (str) => /^https?:\/\/[^/]*\.whatsapp\.net\//i.test(str);
338
338
 
339
+ // Vanz@Fix (crash guard) --- keep the original raw url around so that if the
340
+ // resolveUrl=true upload-to-'@newsletter' round trip (Toolkit.toUrl -> prepareWAMessageMedia)
341
+ // throws/rejects (blocked account, network hiccup, WA server refusal, etc.), we can
342
+ // gracefully fall back to the raw url instead of letting the rejection bubble up
343
+ // unhandled through waitAllPromises() and take the whole process down. Only applies
344
+ // when the input was actually a url string; buffers/base64 have no such fallback.
345
+ const rawUrlFallback = typeof media === 'string' && isUrl(media) ? media : undefined;
346
+
339
347
  if (Array.isArray(media)) {
340
348
  return Promise.all(
341
349
  media.map((item) =>
@@ -395,7 +403,16 @@ class Toolkit {
395
403
  // same `Toolkit.toUrl(_client, media, mediaType)` call (dead branching left over from an
396
404
  // earlier version that must have treated buffer vs non-buffer input differently). Collapsed
397
405
  // to a single return; `originalIsBuffer` is now unused and removed below.
398
- return Toolkit.toUrl(_client, media, mediaType);
406
+ //
407
+ // Vanz@Fix (crash guard) --- toUrl() uploads to WA's media server under a spoofed
408
+ // '@newsletter' jid; if that upload fails for any reason, fall back to the raw url
409
+ // (when we have one) instead of letting the exception propagate and crash the caller.
410
+ try {
411
+ return await Toolkit.toUrl(_client, media, mediaType);
412
+ } catch (err) {
413
+ if (rawUrlFallback) return rawUrlFallback;
414
+ throw err;
415
+ }
399
416
  }
400
417
 
401
418
  /** Read an mp4 buffer's duration (seconds) straight from its moov atom, no ffprobe needed. */
@@ -1,5 +1,5 @@
1
1
  import { Mutex } from 'async-mutex';
2
- import { mkdir, readFile, stat, unlink, writeFile } from 'fs/promises';
2
+ import { mkdir, readFile, readdir, stat, unlink, writeFile } from 'fs/promises';
3
3
  import { join } from 'path';
4
4
  import { proto } from '../../WAProto/index.js';
5
5
  import { initAuthCreds } from './auth-utils.js';
@@ -18,6 +18,49 @@ const getFileLock = (path) => {
18
18
  }
19
19
  return mutex;
20
20
  };
21
+ // Vanz@Fix 25-08-26 (ENOSPC guard) --- useMultiFileAuthState writes one small file PER key id
22
+ // (session-*, sender-key-*, sender-key-memory-*, pre-key-*, app-state-sync-key-*, ...) and never
23
+ // prunes any of them on its own. Over months of uptime — especially in busy groups, where a
24
+ // sender-key-memory file is created per (group, participant) pair — this silently accumulates into
25
+ // hundreds of thousands of tiny files. That can hit the filesystem's inode/file-count ceiling and
26
+ // start throwing ENOSPC on writes long before disk *space* usage looks anywhere near full (a
27
+ // classic symptom: panel shows plenty of free MiB/GiB, but writes still fail with ENOSPC). When
28
+ // that happens mid-session, auth writes (including the app-state-sync key from the "App state key
29
+ // not present!" issue) fail silently and never actually persist.
30
+ //
31
+ // `sender-key-memory-*` entries are pure dedup/anti-replay cache — safe to delete anytime; Baileys
32
+ // regenerates them on demand with zero functional loss (worst case: one redundant re-decrypt). They
33
+ // also tend to be the single biggest contributor to file count in active groups, so they're the
34
+ // safest and highest-impact thing to prune automatically. Everything else (creds, real sessions,
35
+ // pre-keys, app-state-sync-keys) is left completely untouched — this never touches those.
36
+ export const pruneStaleAuthFiles = async (folder, { maxAgeDays = 14, categories = ['sender-key-memory'], dryRun = false } = {}) => {
37
+ let entries;
38
+ try {
39
+ entries = await readdir(folder);
40
+ }
41
+ catch {
42
+ return { scanned: 0, removed: 0 };
43
+ }
44
+ const cutoff = Date.now() - maxAgeDays * 24 * 60 * 60 * 1000;
45
+ let scanned = 0;
46
+ let removed = 0;
47
+ await Promise.all(entries.map(async (name) => {
48
+ if (!categories.some((cat) => name.startsWith(`${cat}-`) && name.endsWith('.json'))) return;
49
+ scanned++;
50
+ const filePath = join(folder, name);
51
+ try {
52
+ const info = await stat(filePath);
53
+ if (info.mtimeMs < cutoff) {
54
+ if (!dryRun) await unlink(filePath).catch(() => { });
55
+ removed++;
56
+ }
57
+ }
58
+ catch {
59
+ // file vanished between readdir and stat — fine, nothing to prune
60
+ }
61
+ }));
62
+ return { scanned, removed };
63
+ };
21
64
  /**
22
65
  * stores the full authentication state in a single folder.
23
66
  * Far more efficient than singlefileauthstate
@@ -83,6 +126,9 @@ export const useMultiFileAuthState = async (folder) => {
83
126
  else {
84
127
  await mkdir(folder, { recursive: true });
85
128
  }
129
+ // Vanz@Fix 25-08-26 (ENOSPC guard) --- best-effort prune on every startup. Failures here must
130
+ // never block auth from loading, so this is intentionally fire-and-forget with its own catch.
131
+ pruneStaleAuthFiles(folder).catch(() => { });
86
132
  const fixFileName = (file) => file?.replace(/\//g, '__')?.replace(/:/g, '-');
87
133
  const creds = (await readData('creds.json')) || initAuthCreds();
88
134
  return {
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@vanzxy/baileys",
3
- "version": "1.5.7",
3
+ "version": "1.5.9",
4
4
  "description": "Enhanced Baileys fork by Vanzxy — based on @itsliaaa/baileys + @whiskeysockets/baileys with fixes for audio group status and clean media without newsletter button.",
5
5
  "type": "module",
6
6
  "module": "./lib/index.js",