@aunucore/baileys 1.0.83 → 1.0.85
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/chats.js +11 -5
- package/lib/Store/make-in-memory-store.js +87 -11
- package/lib/Utils/auth-utils.js +27 -3
- package/lib/Utils/baileys-event-stream.js +39 -4
- package/lib/Utils/event-buffer.js +38 -6
- package/lib/Utils/generics.js +20 -1
- package/lib/Utils/index.d.ts +1 -0
- package/lib/Utils/index.js +1 -0
- package/lib/Utils/lid-store.js +4 -1
- package/lib/Utils/link-preview.js +87 -35
- package/lib/Utils/messages-media.js +123 -20
- package/lib/Utils/messages.js +50 -8
- package/lib/Utils/safe-fetch.d.ts +57 -0
- package/lib/Utils/safe-fetch.js +466 -0
- package/package.json +1 -1
package/lib/Socket/chats.js
CHANGED
|
@@ -1,4 +1,3 @@
|
|
|
1
|
-
import NodeCache from '@cacheable/node-cache';
|
|
2
1
|
import { Boom } from '@hapi/boom';
|
|
3
2
|
import { proto } from '../../WAProto/index.js';
|
|
4
3
|
import { DEFAULT_CACHE_TTLS, PROCESSABLE_HISTORY_TYPES } from '../Defaults/index.js';
|
|
@@ -6,6 +5,7 @@ import { ALL_WA_PATCH_NAMES } from '../Types/index.js';
|
|
|
6
5
|
import { SyncState } from '../Types/State.js';
|
|
7
6
|
import { chatModificationToAppPatch, decodePatches, decodeSyncdSnapshot, encodeSyncdPatch, extractSyncdPatches, generateProfilePicture, getHistoryMsg, newLTHashState, processSyncAction } from '../Utils/index.js';
|
|
8
7
|
import { makeMutex } from '../Utils/make-mutex.js';
|
|
8
|
+
import { makeBoundedTTLCache } from '../Utils/bounded-cache.js';
|
|
9
9
|
import processMessage from '../Utils/process-message.js';
|
|
10
10
|
import { getBinaryNodeChild, getBinaryNodeChildren, isJidUser, isLidUser, jidDecode, jidNormalizedUser, reduceBinaryNodeToDictionary, S_WHATSAPP_NET } from '../WABinary/index.js';
|
|
11
11
|
import { USyncQuery, USyncUser } from '../WAUSync/index.js';
|
|
@@ -24,10 +24,16 @@ export const makeChatsSocket = (config) => {
|
|
|
24
24
|
// Timeout for AwaitingInitialSync state
|
|
25
25
|
let awaitingSyncTimeout;
|
|
26
26
|
const placeholderResendCache = config.placeholderResendCache ||
|
|
27
|
-
|
|
28
|
-
|
|
29
|
-
|
|
30
|
-
|
|
27
|
+
// AunuBaileys (1.0.84, HB-231): in the standard
|
|
28
|
+
// makeWASocket composition this factory runs BEFORE
|
|
29
|
+
// messages-recv creates its own default, and the
|
|
30
|
+
// write-back below means the instance created HERE is
|
|
31
|
+
// the one the whole receive path actually uses. It used
|
|
32
|
+
// to be a NodeCache with no size cap, silently shadowing
|
|
33
|
+
// the bounded default messages-recv intended ("bounded
|
|
34
|
+
// by default — a TTL alone lets these grow without
|
|
35
|
+
// limit"). Build the same bounded cache here.
|
|
36
|
+
makeBoundedTTLCache({ ttlSeconds: DEFAULT_CACHE_TTLS.MSG_RETRY, maxEntries: 1000 });
|
|
31
37
|
if (!config.placeholderResendCache) {
|
|
32
38
|
config.placeholderResendCache = placeholderResendCache;
|
|
33
39
|
}
|
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
// AunuCore fork backport: in-memory store dari Baileys 6.6.0, dikonversi ke ESM untuk basis 6.7.x
|
|
2
2
|
import KeyedDBModule from '@adiwajshing/keyed-db';
|
|
3
3
|
const KeyedDB = KeyedDBModule.default || KeyedDBModule;
|
|
4
|
-
import { readFileSync, writeFileSync, existsSync } from 'fs';
|
|
4
|
+
import { readFileSync, writeFileSync, existsSync, renameSync } from 'fs';
|
|
5
5
|
import { proto } from '../../WAProto/index.js';
|
|
6
6
|
import { DEFAULT_CONNECTION_CONFIG } from '../Defaults/index.js';
|
|
7
7
|
import { LabelAssociationType } from '../Types/LabelAssociation.js';
|
|
@@ -131,6 +131,16 @@ const makeInMemoryStore = ({ logger: _logger, chatKey, labelAssociationKey }) =>
|
|
|
131
131
|
for (const update of updates) {
|
|
132
132
|
if (contacts[update.id]) {
|
|
133
133
|
Object.assign(contacts[update.id], update);
|
|
134
|
+
// AunuBaileys (1.0.84, HB-245): a picture
|
|
135
|
+
// notification arrives as imgUrl 'changed' /
|
|
136
|
+
// 'removed' — a sentinel, not a URL. Drop the
|
|
137
|
+
// cached URL instead of storing the sentinel:
|
|
138
|
+
// fetchImageUrl used to hand the literal string
|
|
139
|
+
// 'changed' to the host as the avatar URL, and
|
|
140
|
+
// writeToFile persisted the poison to disk.
|
|
141
|
+
if (update.imgUrl === 'changed' || update.imgUrl === 'removed') {
|
|
142
|
+
delete contacts[update.id].imgUrl;
|
|
143
|
+
}
|
|
134
144
|
}
|
|
135
145
|
else {
|
|
136
146
|
logger.debug({ update }, 'got update for non-existant contact');
|
|
@@ -258,7 +268,18 @@ const makeInMemoryStore = ({ logger: _logger, chatKey, labelAssociationKey }) =>
|
|
|
258
268
|
if (metadata) {
|
|
259
269
|
switch (action) {
|
|
260
270
|
case 'add':
|
|
261
|
-
|
|
271
|
+
// AunuBaileys (1.0.84, HB-237): skip ids
|
|
272
|
+
// that are already listed. The metadata is
|
|
273
|
+
// usually populated by a fetch that already
|
|
274
|
+
// contains the member, and a redelivery
|
|
275
|
+
// replays the notification — both used to
|
|
276
|
+
// push a duplicate entry every single time,
|
|
277
|
+
// without bound.
|
|
278
|
+
for (const participantId of participants) {
|
|
279
|
+
if (!metadata.participants.some(p => p.id === participantId)) {
|
|
280
|
+
metadata.participants.push({ id: participantId, isAdmin: false, isSuperAdmin: false });
|
|
281
|
+
}
|
|
282
|
+
}
|
|
262
283
|
break;
|
|
263
284
|
case 'demote':
|
|
264
285
|
case 'promote':
|
|
@@ -301,14 +322,43 @@ const makeInMemoryStore = ({ logger: _logger, chatKey, labelAssociationKey }) =>
|
|
|
301
322
|
labelAssociations
|
|
302
323
|
});
|
|
303
324
|
const fromJSON = (json) => {
|
|
304
|
-
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
325
|
+
// AunuBaileys (1.0.84, HB-134 + HB-142 + addenda): every
|
|
326
|
+
// stage — and every entry — is isolated. One poisoned
|
|
327
|
+
// message entry used to throw out of fromJSON mid-loop:
|
|
328
|
+
// that jid's remaining messages and EVERY later jid were
|
|
329
|
+
// silently lost on every boot, while the stages before
|
|
330
|
+
// the throw stayed applied and readFromFile logged
|
|
331
|
+
// "starting with an empty store" — a claim the state did
|
|
332
|
+
// not match. A missing/null contacts stage threw the
|
|
333
|
+
// same way (Object.values of undefined), and a poisoned
|
|
334
|
+
// contacts/labels entry aborted every stage after it.
|
|
335
|
+
// Wrong-typed stages now read as empty, unusable entries
|
|
336
|
+
// are skipped, and a bad message entry is caught per
|
|
337
|
+
// entry with a warning — the healthy remainder of the
|
|
338
|
+
// file always loads.
|
|
339
|
+
if (!json || typeof json !== 'object') {
|
|
340
|
+
return;
|
|
341
|
+
}
|
|
342
|
+
const entriesOf = (value) => (Array.isArray(value) ? value : []).filter(e => !!e && typeof e === 'object');
|
|
343
|
+
const valuesOf = (value) => (value && typeof value === 'object' ? Object.values(value) : []);
|
|
344
|
+
chats.upsert(...entriesOf(json.chats));
|
|
345
|
+
labelAssociations.upsert(...entriesOf(json.labelAssociations));
|
|
346
|
+
contactsUpsert(valuesOf(json.contacts).filter(c => !!c && typeof c === 'object' && typeof c.id === 'string'));
|
|
347
|
+
labelsUpsert(valuesOf(json.labels).filter(l => !!l && typeof l === 'object' && typeof l.id === 'string'));
|
|
348
|
+
const messageSets = json.messages && typeof json.messages === 'object' ? json.messages : {};
|
|
349
|
+
for (const jid in messageSets) {
|
|
350
|
+
const entries = messageSets[jid];
|
|
351
|
+
if (!Array.isArray(entries)) {
|
|
352
|
+
continue;
|
|
353
|
+
}
|
|
309
354
|
const list = assertMessageList(jid);
|
|
310
|
-
for (const msg of
|
|
311
|
-
|
|
355
|
+
for (const msg of entries) {
|
|
356
|
+
try {
|
|
357
|
+
list.upsert(proto.WebMessageInfo.fromObject(msg), 'append');
|
|
358
|
+
}
|
|
359
|
+
catch (err) {
|
|
360
|
+
logger.warn({ jid, err }, 'aunu: skipped an unreadable message entry while loading the store');
|
|
361
|
+
}
|
|
312
362
|
}
|
|
313
363
|
}
|
|
314
364
|
};
|
|
@@ -329,6 +379,16 @@ const makeInMemoryStore = ({ logger: _logger, chatKey, labelAssociationKey }) =>
|
|
|
329
379
|
const cursorKey = !!cursor ? ('before' in cursor ? cursor.before : cursor.after) : undefined;
|
|
330
380
|
const cursorValue = cursorKey ? list.get(cursorKey.id) : undefined;
|
|
331
381
|
let messages;
|
|
382
|
+
// AunuBaileys (1.0.84, HB-177): a count of 0 means
|
|
383
|
+
// "no messages" — the slicing below used
|
|
384
|
+
// slice(-count), and slice(-0) is slice(0), so
|
|
385
|
+
// loadMessages(jid, 0) returned the chat's ENTIRE
|
|
386
|
+
// history (and negative counts mis-sliced as well).
|
|
387
|
+
// Every other path in this function already resolves
|
|
388
|
+
// to []. An absent count keeps the legacy behaviour.
|
|
389
|
+
if (typeof count === 'number' && count <= 0) {
|
|
390
|
+
return [];
|
|
391
|
+
}
|
|
332
392
|
if (list && mode === 'before' && (!cursorKey || cursorValue)) {
|
|
333
393
|
if (cursorValue) {
|
|
334
394
|
const msgIdx = list.array.findIndex(m => m.key.id === (cursorKey === null || cursorKey === void 0 ? void 0 : cursorKey.id));
|
|
@@ -386,7 +446,13 @@ const makeInMemoryStore = ({ logger: _logger, chatKey, labelAssociationKey }) =>
|
|
|
386
446
|
if (!contact) {
|
|
387
447
|
return sock === null || sock === void 0 ? void 0 : sock.profilePictureUrl(jid);
|
|
388
448
|
}
|
|
389
|
-
|
|
449
|
+
// AunuBaileys (1.0.84, HB-245): a store file written
|
|
450
|
+
// before this fix can still carry the 'changed' /
|
|
451
|
+
// 'removed' sentinel as imgUrl — treat it as no
|
|
452
|
+
// cached URL and fetch fresh; never hand a sentinel
|
|
453
|
+
// back as an avatar URL. A real cached URL is still
|
|
454
|
+
// returned as-is (and a fresh fetch re-caches it).
|
|
455
|
+
if (typeof contact.imgUrl === 'undefined' || contact.imgUrl === 'changed' || contact.imgUrl === 'removed') {
|
|
390
456
|
contact.imgUrl = await (sock === null || sock === void 0 ? void 0 : sock.profilePictureUrl(jid));
|
|
391
457
|
}
|
|
392
458
|
return contact.imgUrl;
|
|
@@ -419,7 +485,17 @@ const makeInMemoryStore = ({ logger: _logger, chatKey, labelAssociationKey }) =>
|
|
|
419
485
|
writeToFile: (path) => {
|
|
420
486
|
// require fs here so that in case "fs" is not available -- the app does not crash
|
|
421
487
|
|
|
422
|
-
|
|
488
|
+
// AunuBaileys (1.0.84, HB-120 + addendum): every
|
|
489
|
+
// other persistence writer in the lib is atomic and
|
|
490
|
+
// 0600 (auth files HB-116, the lid store); this one
|
|
491
|
+
// wrote the full chat/contact/message history
|
|
492
|
+
// world-readable AND non-atomically — a crash
|
|
493
|
+
// mid-write left a truncated file the next boot read
|
|
494
|
+
// as corrupt and discarded wholesale. Write a
|
|
495
|
+
// per-pid temp file, then rename it over the target.
|
|
496
|
+
const tmpPath = `${path}.tmp-${process.pid}`;
|
|
497
|
+
writeFileSync(tmpPath, JSON.stringify(toJSON()), { mode: 0o600 });
|
|
498
|
+
renameSync(tmpPath, path);
|
|
423
499
|
},
|
|
424
500
|
readFromFile: (path) => {
|
|
425
501
|
// require fs here so that in case "fs" is not available -- the app does not crash
|
package/lib/Utils/auth-utils.js
CHANGED
|
@@ -16,11 +16,35 @@ export function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
|
16
16
|
new NodeCache({
|
|
17
17
|
stdTTL: DEFAULT_CACHE_TTLS.SIGNAL_STORE, // 5 minutes
|
|
18
18
|
useClones: false,
|
|
19
|
-
deleteOnExpire: true
|
|
19
|
+
deleteOnExpire: true,
|
|
20
|
+
// AunuBaileys (1.0.84, HB-199): a TTL bounds an
|
|
21
|
+
// entry's AGE, not the entry COUNT — a burst of
|
|
22
|
+
// distinct sender addresses inside the 5-minute
|
|
23
|
+
// window grew this cache without limit (the exact
|
|
24
|
+
// warning written in bounded-cache.js). Cap the
|
|
25
|
+
// number of keys as well. NOTE: NodeCache's maxKeys
|
|
26
|
+
// does not evict — set() THROWS once the cap is hit —
|
|
27
|
+
// so every cache write below goes through cacheSet,
|
|
28
|
+
// which swallows that one error: the underlying store
|
|
29
|
+
// is authoritative, a skipped cache entry only costs
|
|
30
|
+
// a re-read, and the cap can never break a session
|
|
31
|
+
// operation.
|
|
32
|
+
maxKeys: 1000
|
|
20
33
|
});
|
|
21
34
|
function getUniqueId(type, id) {
|
|
22
35
|
return `${type}.${id}`;
|
|
23
36
|
}
|
|
37
|
+
// AunuBaileys (1.0.84, HB-199): see the maxKeys note above —
|
|
38
|
+
// a full cache must degrade to "not cached", never to a throw
|
|
39
|
+
// escaping into get()/set().
|
|
40
|
+
function cacheSet(key, value) {
|
|
41
|
+
try {
|
|
42
|
+
cache.set(key, value);
|
|
43
|
+
}
|
|
44
|
+
catch {
|
|
45
|
+
// cache is at its key cap; the store stays authoritative
|
|
46
|
+
}
|
|
47
|
+
}
|
|
24
48
|
return {
|
|
25
49
|
async get(type, ids) {
|
|
26
50
|
const data = {};
|
|
@@ -41,7 +65,7 @@ export function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
|
41
65
|
const item = fetched[id];
|
|
42
66
|
if (item) {
|
|
43
67
|
data[id] = item;
|
|
44
|
-
|
|
68
|
+
cacheSet(getUniqueId(type, id), item);
|
|
45
69
|
}
|
|
46
70
|
}
|
|
47
71
|
}
|
|
@@ -67,7 +91,7 @@ export function makeCacheableSignalKeyStore(store, logger, _cache) {
|
|
|
67
91
|
cache.del(getUniqueId(type, id));
|
|
68
92
|
}
|
|
69
93
|
else {
|
|
70
|
-
|
|
94
|
+
cacheSet(getUniqueId(type, id), data[type][id]);
|
|
71
95
|
}
|
|
72
96
|
keys += 1;
|
|
73
97
|
}
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import EventEmitter from 'events';
|
|
2
2
|
import { createReadStream } from 'fs';
|
|
3
|
-
import { writeFile } from 'fs/promises';
|
|
3
|
+
import { rename, writeFile } from 'fs/promises';
|
|
4
4
|
import { createInterface } from 'readline';
|
|
5
5
|
import { delay } from './generics.js';
|
|
6
6
|
import { makeMutex } from './make-mutex.js';
|
|
@@ -13,13 +13,48 @@ export const captureEventStream = (ev, filename) => {
|
|
|
13
13
|
const oldEmit = ev.emit;
|
|
14
14
|
// write mutex so data is appended in order
|
|
15
15
|
const writeMutex = makeMutex();
|
|
16
|
+
// AunuBaileys (1.0.84, HB-247): a creds.update payload is the
|
|
17
|
+
// full credential material (private noise / identity /
|
|
18
|
+
// signed-pre-key bytes, the adv secret). The capture below
|
|
19
|
+
// redacts it to the NAMES of the fields that changed, creates
|
|
20
|
+
// the file 0600, and rotates it to `<filename>.1` once per
|
|
21
|
+
// 10 MiB so a long-running debug capture cannot grow (or
|
|
22
|
+
// leak) without bound.
|
|
23
|
+
let capturedBytes = 0;
|
|
24
|
+
const MAX_CAPTURE_BYTES = 10 * 1024 * 1024;
|
|
16
25
|
// monkey patch eventemitter to capture all events
|
|
17
26
|
ev.emit = function (...args) {
|
|
18
|
-
const content = JSON.stringify({ timestamp: Date.now(), event: args[0], data: args[1] }) + '\n';
|
|
19
27
|
const result = oldEmit.apply(ev, args);
|
|
28
|
+
let content;
|
|
29
|
+
try {
|
|
30
|
+
const data = args[0] === 'creds.update' && args[1] && typeof args[1] === 'object'
|
|
31
|
+
? { redacted: 'creds.update payload withheld (credential material)', updatedFields: Object.keys(args[1]) }
|
|
32
|
+
: args[1];
|
|
33
|
+
content = JSON.stringify({ timestamp: Date.now(), event: args[0], data }) + '\n';
|
|
34
|
+
}
|
|
35
|
+
catch {
|
|
36
|
+
// AunuBaileys (1.0.84, HB-129 addendum): an
|
|
37
|
+
// unserialisable (circular) payload used to throw
|
|
38
|
+
// synchronously out of the patched emit — into
|
|
39
|
+
// whatever lib code had emitted the event — because
|
|
40
|
+
// the stringify ran before oldEmit. Serialisation now
|
|
41
|
+
// happens after the real emit, and a payload that
|
|
42
|
+
// cannot be serialised is simply not captured.
|
|
43
|
+
return result;
|
|
44
|
+
}
|
|
45
|
+
// AunuBaileys (1.0.84, HB-129): the mutex promise used to
|
|
46
|
+
// be discarded — a failed write (deleted directory, full
|
|
47
|
+
// disk, permissions) rejected with no handler attached
|
|
48
|
+
// and crashed the host process (Node >= 15 default). A
|
|
49
|
+
// debug capture must never take the host down with it.
|
|
20
50
|
writeMutex.mutex(async () => {
|
|
21
|
-
|
|
22
|
-
|
|
51
|
+
capturedBytes += Buffer.byteLength(content);
|
|
52
|
+
if (capturedBytes > MAX_CAPTURE_BYTES) {
|
|
53
|
+
capturedBytes = Buffer.byteLength(content);
|
|
54
|
+
await rename(filename, `${filename}.1`).catch(() => { });
|
|
55
|
+
}
|
|
56
|
+
await writeFile(filename, content, { flag: 'a', mode: 0o600 });
|
|
57
|
+
}).catch(() => { });
|
|
23
58
|
return result;
|
|
24
59
|
};
|
|
25
60
|
};
|
|
@@ -45,11 +45,25 @@ export const makeEventBuffer = (logger) => {
|
|
|
45
45
|
// had to run. Log the error and keep dispatching the
|
|
46
46
|
// remaining events of the batch; the event itself
|
|
47
47
|
// still fired, with its payload unchanged.
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
51
|
-
|
|
52
|
-
|
|
48
|
+
//
|
|
49
|
+
// AunuBaileys (1.0.84, HB-264): dispatch PER LISTENER.
|
|
50
|
+
// A single ev.emit stops at the first listener that
|
|
51
|
+
// throws, so every listener registered AFTER the
|
|
52
|
+
// thrower silently missed that emit — a throwing
|
|
53
|
+
// logging listener registered before the host's
|
|
54
|
+
// saveCreds made that credential update vanish.
|
|
55
|
+
// rawListeners returns the once-wrappers themselves,
|
|
56
|
+
// so once semantics are preserved, and each listener
|
|
57
|
+
// gets its own guard: a throw now costs only the
|
|
58
|
+
// listener that threw.
|
|
59
|
+
const listeners = ev.rawListeners(event);
|
|
60
|
+
for (const listener of listeners) {
|
|
61
|
+
try {
|
|
62
|
+
listener.call(ev, map[event]);
|
|
63
|
+
}
|
|
64
|
+
catch (err) {
|
|
65
|
+
logger.error({ err, event }, 'error in event listener — dispatch continues');
|
|
66
|
+
}
|
|
53
67
|
}
|
|
54
68
|
}
|
|
55
69
|
});
|
|
@@ -65,6 +79,17 @@ export const makeEventBuffer = (logger) => {
|
|
|
65
79
|
}
|
|
66
80
|
logger.debug('Flushing event buffer');
|
|
67
81
|
isBuffering = false;
|
|
82
|
+
// AunuBaileys (1.0.84, HB-130 + addendum): the history
|
|
83
|
+
// dedup set only has to live for ONE buffering session.
|
|
84
|
+
// It used to survive every flush (cleared only in
|
|
85
|
+
// destroy()), so a second history sync inside the same
|
|
86
|
+
// socket lifetime silently dropped every already-known
|
|
87
|
+
// chat — and every already-known message key, even when
|
|
88
|
+
// that message's content had changed: fresh unread
|
|
89
|
+
// counts and edits never reached the store. Dedup for
|
|
90
|
+
// THIS session's appends already happened above; the
|
|
91
|
+
// next session starts empty.
|
|
92
|
+
historyCache.clear();
|
|
68
93
|
const newData = makeBufferData();
|
|
69
94
|
const chatUpdates = Object.values(data.chatUpdates);
|
|
70
95
|
let conditionalChatUpdatesLeft = 0;
|
|
@@ -305,7 +330,14 @@ eventData, logger) {
|
|
|
305
330
|
data.contactUpserts[contact.id] = upsert;
|
|
306
331
|
}
|
|
307
332
|
if (data.contactUpdates[contact.id]) {
|
|
308
|
-
|
|
333
|
+
// AunuBaileys (1.0.84, HB-182): the pending
|
|
334
|
+
// update merges INTO the upsert. The old
|
|
335
|
+
// direction merged the contact into the pending
|
|
336
|
+
// update object and then deleted that object —
|
|
337
|
+
// the merged result went nowhere, and fields
|
|
338
|
+
// only the update carried (notify, verifiedName)
|
|
339
|
+
// silently vanished from the flushed upsert.
|
|
340
|
+
Object.assign(upsert, data.contactUpdates[contact.id]);
|
|
309
341
|
delete data.contactUpdates[contact.id];
|
|
310
342
|
}
|
|
311
343
|
}
|
package/lib/Utils/generics.js
CHANGED
|
@@ -46,7 +46,26 @@ export const BufferJSON = {
|
|
|
46
46
|
reviver: (_, value) => {
|
|
47
47
|
if (typeof value === 'object' && !!value && (value.buffer === true || value.type === 'Buffer')) {
|
|
48
48
|
const val = value.data || value.value;
|
|
49
|
-
|
|
49
|
+
if (typeof val === 'string') {
|
|
50
|
+
// AunuBaileys (1.0.84, HB-143): Node's base64
|
|
51
|
+
// decoder is lenient — it silently DROPS
|
|
52
|
+
// characters outside the alphabet, so a garbage
|
|
53
|
+
// or hand-mangled string in a creds/key file
|
|
54
|
+
// decoded to wrong-length, wrong-value key bytes
|
|
55
|
+
// with no error anywhere, and the corruption only
|
|
56
|
+
// surfaced later as baffling Curve / handshake
|
|
57
|
+
// failures. The replacer above only ever produces
|
|
58
|
+
// canonical base64; reject anything else loudly.
|
|
59
|
+
// Both in-tree readers contain the throw: the
|
|
60
|
+
// auth-state reader logs a corrupt file and starts
|
|
61
|
+
// clean, and the sender-key reader falls back to
|
|
62
|
+
// an empty (self-healing) record.
|
|
63
|
+
if (!/^(?:[A-Za-z0-9+/]{4})*(?:[A-Za-z0-9+/]{2}==|[A-Za-z0-9+/]{3}=)?$/.test(val)) {
|
|
64
|
+
throw new Error('BufferJSON: invalid base64 in serialized Buffer data');
|
|
65
|
+
}
|
|
66
|
+
return Buffer.from(val, 'base64');
|
|
67
|
+
}
|
|
68
|
+
return Buffer.from(val || []);
|
|
50
69
|
}
|
|
51
70
|
return value;
|
|
52
71
|
}
|
package/lib/Utils/index.d.ts
CHANGED
|
@@ -13,6 +13,7 @@ export * from './auth-utils.js';
|
|
|
13
13
|
export * from './baileys-event-stream.js';
|
|
14
14
|
export * from './use-multi-file-auth-state.js';
|
|
15
15
|
export * from './link-preview.js';
|
|
16
|
+
export * from './safe-fetch.js';
|
|
16
17
|
export * from './event-buffer.js';
|
|
17
18
|
export * from './process-message.js';
|
|
18
19
|
//# sourceMappingURL=index.d.ts.map
|
package/lib/Utils/index.js
CHANGED
|
@@ -31,6 +31,7 @@ export * from './auth-utils.js';
|
|
|
31
31
|
export * from './baileys-event-stream.js';
|
|
32
32
|
export * from './use-multi-file-auth-state.js';
|
|
33
33
|
export * from './link-preview.js';
|
|
34
|
+
export * from './safe-fetch.js';
|
|
34
35
|
export * from './event-buffer.js';
|
|
35
36
|
export * from './process-message.js';
|
|
36
37
|
//# sourceMappingURL=index.js.map
|
package/lib/Utils/lid-store.js
CHANGED
|
@@ -169,7 +169,10 @@ export const makeLIDStore = (options = {}) => {
|
|
|
169
169
|
// AunuBaileys (1.0.70): atomic tmp+rename — a crash mid-write
|
|
170
170
|
// used to leave a truncated store file behind.
|
|
171
171
|
const tmpPath = `${path}.tmp-${process.pid}`;
|
|
172
|
-
|
|
172
|
+
// AunuBaileys (1.0.84, HB-120): 0600, like the auth
|
|
173
|
+
// files (HB-116) — this file maps phone numbers to
|
|
174
|
+
// LIDs and must not land world-readable.
|
|
175
|
+
writeFileSync(tmpPath, JSON.stringify(Object.fromEntries(pnToLid)), { mode: 0o600 });
|
|
173
176
|
renameSync(tmpPath, path);
|
|
174
177
|
},
|
|
175
178
|
loadFromFile: (path) => {
|
|
@@ -1,15 +1,71 @@
|
|
|
1
1
|
import { prepareWAMessageMedia } from './messages.js';
|
|
2
|
-
import { extractImageThumb
|
|
2
|
+
import { extractImageThumb } from './messages-media.js';
|
|
3
|
+
import { readImageDimensions, safeHttpGet } from './safe-fetch.js';
|
|
3
4
|
const THUMBNAIL_WIDTH_PX = 192;
|
|
5
|
+
// AunuBaileys (1.0.85): everything in this module is fetched on behalf
|
|
6
|
+
// of a REMOTE PAGE — the URL comes from message text and the og:image
|
|
7
|
+
// from the fetched page itself — so all of it now goes through the
|
|
8
|
+
// SSRF-safe fetch (lib/Utils/safe-fetch.js): non-public addresses are
|
|
9
|
+
// refused at connect time (peer-supplied URLs used to be able to make
|
|
10
|
+
// the host GET loopback / RFC1918 / cloud-metadata addresses), and
|
|
11
|
+
// every response is size- and time-bounded. link-preview-js is used
|
|
12
|
+
// for HTML parsing ONLY (getPreviewFromContent); it no longer
|
|
13
|
+
// performs any network access, so the protections do not depend on
|
|
14
|
+
// the installed link-preview-js version. This also closes HB-205:
|
|
15
|
+
// the HTML body used to be read in full with no cap and no overall
|
|
16
|
+
// timeout (the library's abort timer covered only time-to-headers).
|
|
17
|
+
const MAX_HTML_BYTES = 1024 * 1024; // og: tags live in <head>; the rest is cut off
|
|
18
|
+
const MAX_THUMBNAIL_BYTES = 5 * 1024 * 1024;
|
|
19
|
+
const MAX_THUMBNAIL_PIXELS = 40000000; // refuse decompression bombs before the image library sees them
|
|
20
|
+
const MAX_PREVIEW_TIMEOUT_MS = 30000;
|
|
21
|
+
const HTMLISH = /^(text\/html|application\/xhtml\+xml|text\/xml|application\/xml)\b/i;
|
|
22
|
+
/** Only these keys of the caller's fetchOpts are honoured now. */
|
|
23
|
+
const transportOpts = (fetchOpts = {}) => ({
|
|
24
|
+
// Sanitised at the boundary: a finite positive timeout is honored
|
|
25
|
+
// (clamped to a sane preview ceiling), anything else gets the
|
|
26
|
+
// historical 3000 ms default.
|
|
27
|
+
timeout: typeof fetchOpts.timeout === 'number' && Number.isFinite(fetchOpts.timeout) && fetchOpts.timeout > 0
|
|
28
|
+
? Math.min(fetchOpts.timeout, MAX_PREVIEW_TIMEOUT_MS)
|
|
29
|
+
: 3000,
|
|
30
|
+
headers: fetchOpts.headers,
|
|
31
|
+
// Explicit, host-controlled hooks only — never derived from
|
|
32
|
+
// message content: an opt-in for deployments that really want
|
|
33
|
+
// previews of intranet links, and resolver/policy overrides.
|
|
34
|
+
allowPrivateNetworks: fetchOpts.allowPrivateNetworks === true,
|
|
35
|
+
isBlocked: typeof fetchOpts.isBlockedAddress === 'function' ? fetchOpts.isBlockedAddress : undefined,
|
|
36
|
+
lookup: typeof fetchOpts.lookup === 'function' ? fetchOpts.lookup : undefined
|
|
37
|
+
});
|
|
38
|
+
const decodeBody = (body, contentType) => {
|
|
39
|
+
const charset = /charset=["']?([\w-]+)/i.exec(contentType || '')?.[1];
|
|
40
|
+
try {
|
|
41
|
+
return new TextDecoder(charset || 'utf-8').decode(body);
|
|
42
|
+
}
|
|
43
|
+
catch {
|
|
44
|
+
return new TextDecoder('utf-8').decode(body);
|
|
45
|
+
}
|
|
46
|
+
};
|
|
47
|
+
/** Fetches an og:image through the safe fetch, with byte + pixel bounds. */
|
|
48
|
+
const fetchThumbnailBuffer = async (imageUrl, fetchOpts) => {
|
|
49
|
+
const res = await safeHttpGet(imageUrl, {
|
|
50
|
+
...transportOpts(fetchOpts),
|
|
51
|
+
maxBytes: MAX_THUMBNAIL_BYTES,
|
|
52
|
+
onLimit: 'throw'
|
|
53
|
+
});
|
|
54
|
+
const dims = readImageDimensions(res.body);
|
|
55
|
+
if (dims && dims.width * dims.height > MAX_THUMBNAIL_PIXELS) {
|
|
56
|
+
throw new Error(`thumbnail dimensions ${dims.width}x${dims.height} exceed the safety limit`);
|
|
57
|
+
}
|
|
58
|
+
return res.body;
|
|
59
|
+
};
|
|
4
60
|
/** Fetches an image and generates a thumbnail for it */
|
|
5
|
-
// AunuBaileys (1.0.72): an og:image only ever becomes a small JPEG —
|
|
6
|
-
// cap the source download instead of buffering a hostile endless
|
|
7
|
-
// stream in full (HB-89). getUrlInfo catches thumbnail errors and
|
|
8
|
-
// degrades to no thumbnail.
|
|
9
|
-
const MAX_THUMBNAIL_SOURCE_BYTES = 8 * 1024 * 1024;
|
|
10
61
|
const getCompressedJpegThumbnail = async (url, { thumbnailWidth, fetchOpts }) => {
|
|
11
|
-
|
|
12
|
-
|
|
62
|
+
// AunuBaileys (1.0.72): an og:image only ever becomes a small
|
|
63
|
+
// JPEG — cap the source download instead of buffering a hostile
|
|
64
|
+
// endless stream in full (HB-89). (1.0.85): the cap is now
|
|
65
|
+
// enforced by the safe fetch (5 MiB, throw) before the bytes
|
|
66
|
+
// reach the image library, and pixel bombs are refused up front.
|
|
67
|
+
const buffer = await fetchThumbnailBuffer(url, fetchOpts);
|
|
68
|
+
const result = await extractImageThumb(buffer, thumbnailWidth);
|
|
13
69
|
return result;
|
|
14
70
|
};
|
|
15
71
|
/**
|
|
@@ -23,34 +79,22 @@ export const getUrlInfo = async (text, opts = {
|
|
|
23
79
|
fetchOpts: { timeout: 3000 }
|
|
24
80
|
}) => {
|
|
25
81
|
try {
|
|
26
|
-
|
|
27
|
-
const retries = 0;
|
|
28
|
-
const maxRetry = 5;
|
|
29
|
-
const { getLinkPreview } = await import('link-preview-js');
|
|
82
|
+
const { getPreviewFromContent } = await import('link-preview-js');
|
|
30
83
|
let previewLink = text;
|
|
31
84
|
if (!text.startsWith('https://') && !text.startsWith('http://')) {
|
|
32
85
|
previewLink = 'https://' + previewLink;
|
|
33
86
|
}
|
|
34
|
-
const
|
|
35
|
-
...opts.fetchOpts,
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
|
|
39
|
-
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
'www.' + forwardedURLObj.hostname === urlObj.hostname) {
|
|
46
|
-
retries + 1;
|
|
47
|
-
return true;
|
|
48
|
-
}
|
|
49
|
-
else {
|
|
50
|
-
return false;
|
|
51
|
-
}
|
|
52
|
-
},
|
|
53
|
-
headers: opts.fetchOpts
|
|
87
|
+
const res = await safeHttpGet(previewLink, {
|
|
88
|
+
...transportOpts(opts.fetchOpts),
|
|
89
|
+
maxBytes: MAX_HTML_BYTES,
|
|
90
|
+
onLimit: 'truncate',
|
|
91
|
+
shouldReadBody: (_status, headers) => !headers['content-type'] || HTMLISH.test(headers['content-type'])
|
|
92
|
+
});
|
|
93
|
+
const contentType = res.headers['content-type'];
|
|
94
|
+
const info = await getPreviewFromContent({
|
|
95
|
+
url: res.url,
|
|
96
|
+
data: decodeBody(res.body, contentType),
|
|
97
|
+
headers: contentType ? { 'content-type': contentType } : {}
|
|
54
98
|
});
|
|
55
99
|
if (info && 'title' in info && info.title) {
|
|
56
100
|
const [image] = info.images;
|
|
@@ -61,8 +105,16 @@ export const getUrlInfo = async (text, opts = {
|
|
|
61
105
|
description: info.description,
|
|
62
106
|
originalThumbnailUrl: image
|
|
63
107
|
};
|
|
64
|
-
if (opts.uploadImage) {
|
|
65
|
-
|
|
108
|
+
if (opts.uploadImage && image) {
|
|
109
|
+
// AunuBaileys (1.0.85): the high-quality thumbnail is
|
|
110
|
+
// fetched through the safe fetch as a Buffer instead
|
|
111
|
+
// of handing the page-supplied URL to the generic
|
|
112
|
+
// media pipeline. The `&& image` guard also closes
|
|
113
|
+
// HB-150: a page without an og:image used to explode
|
|
114
|
+
// here with a raw TypeError and lose the whole
|
|
115
|
+
// preview.
|
|
116
|
+
const thumb = await fetchThumbnailBuffer(image, opts.fetchOpts);
|
|
117
|
+
const { imageMessage } = await prepareWAMessageMedia({ image: thumb }, {
|
|
66
118
|
upload: opts.uploadImage,
|
|
67
119
|
mediaTypeOverride: 'thumbnail-link',
|
|
68
120
|
options: opts.fetchOpts
|
|
@@ -87,4 +139,4 @@ export const getUrlInfo = async (text, opts = {
|
|
|
87
139
|
}
|
|
88
140
|
}
|
|
89
141
|
};
|
|
90
|
-
//# sourceMappingURL=link-preview.js.map
|
|
142
|
+
//# sourceMappingURL=link-preview.js.map
|