@aunucore/baileys 1.0.84 → 1.0.86
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/aunu-experimental.js +42 -8
- package/lib/Socket/chats.js +19 -2
- package/lib/Socket/messages-recv.js +14 -1
- package/lib/Socket/messages-send.js +11 -1
- package/lib/Socket/newsletter.d.ts +1 -1
- package/lib/Socket/newsletter.js +7 -1
- package/lib/Store/make-in-memory-store.js +7 -0
- package/lib/Utils/event-buffer.js +67 -15
- package/lib/Utils/history.js +11 -0
- package/lib/Utils/index.d.ts +1 -0
- package/lib/Utils/index.js +1 -0
- package/lib/Utils/link-preview.js +87 -35
- package/lib/Utils/messages-media.js +123 -20
- package/lib/Utils/messages.js +65 -12
- package/lib/Utils/process-message.js +18 -6
- package/lib/Utils/safe-fetch.d.ts +57 -0
- package/lib/Utils/safe-fetch.js +466 -0
- package/package.json +1 -1
|
@@ -5,6 +5,10 @@ import { generateWAMessageFromContent, prepareWAMessageMedia } from '../Utils/me
|
|
|
5
5
|
import { proto } from '../../WAProto/index.js';
|
|
6
6
|
import { randomBytes } from 'crypto';
|
|
7
7
|
const delay = (ms) => new Promise((resolve) => setTimeout(resolve, ms));
|
|
8
|
+
// AunuBaileys (1.0.86, HB-112 short series): Node clamps any
|
|
9
|
+
// setTimeout delay above 2^31-1 ms (~24.8 days) down to 1 ms, so
|
|
10
|
+
// a longer delay must be armed in slices of at most this size.
|
|
11
|
+
const MAX_TIMER_DELAY_MS = 2 ** 31 - 1;
|
|
8
12
|
/**
|
|
9
13
|
* AunuBaileys EXPERIMENTAL socket features — these exist only in
|
|
10
14
|
* AunuBaileys and may change or disappear in any release:
|
|
@@ -125,7 +129,8 @@ export const attachAunuExperimental = (sock) => {
|
|
|
125
129
|
sock.aunuSchedule = (jid, content, when, options = {}) => {
|
|
126
130
|
const at = when instanceof Date ? when.getTime() : Date.now() + Math.max(0, Number(when) || 0);
|
|
127
131
|
const id = `aunu-sched-${++seq}`;
|
|
128
|
-
const
|
|
132
|
+
const entry = { jid, at, timer: undefined };
|
|
133
|
+
const fire = () => {
|
|
129
134
|
scheduled.delete(id);
|
|
130
135
|
// AunuBaileys (1.0.72): failures used to be swallowed by
|
|
131
136
|
// an empty catch — a scheduled message that failed (socket
|
|
@@ -133,9 +138,23 @@ export const attachAunuExperimental = (sock) => {
|
|
|
133
138
|
Promise.resolve(sock.sendMessage(jid, content, options)).catch((err) => {
|
|
134
139
|
logger?.warn?.({ err, jid, id }, 'aunu: scheduled message failed to send');
|
|
135
140
|
});
|
|
136
|
-
}
|
|
137
|
-
|
|
138
|
-
|
|
141
|
+
};
|
|
142
|
+
// AunuBaileys (1.0.86, HB-112 short series): arm in slices.
|
|
143
|
+
// A single setTimeout for a send scheduled more than ~24.8
|
|
144
|
+
// days out used to be clamped by Node to 1 ms, so the
|
|
145
|
+
// message fired IMMEDIATELY and the entry vanished as if
|
|
146
|
+
// sent on schedule. entry.timer always holds the CURRENT
|
|
147
|
+
// slice's handle, so cancel and socket-end cleanup (which
|
|
148
|
+
// read entry.timer) keep working unchanged.
|
|
149
|
+
const arm = () => {
|
|
150
|
+
const remaining = at - Date.now();
|
|
151
|
+
entry.timer = remaining > MAX_TIMER_DELAY_MS
|
|
152
|
+
? setTimeout(arm, MAX_TIMER_DELAY_MS)
|
|
153
|
+
: setTimeout(fire, Math.max(0, remaining));
|
|
154
|
+
entry.timer.unref?.();
|
|
155
|
+
};
|
|
156
|
+
arm();
|
|
157
|
+
scheduled.set(id, entry);
|
|
139
158
|
return { id, at, cancel: () => sock.aunuCancelScheduled(id) };
|
|
140
159
|
};
|
|
141
160
|
sock.aunuCancelScheduled = (id) => {
|
|
@@ -152,14 +171,29 @@ export const attachAunuExperimental = (sock) => {
|
|
|
152
171
|
const sent = await sock.sendMessage(jid, content, options);
|
|
153
172
|
const key = sent?.key;
|
|
154
173
|
if (key?.id) {
|
|
155
|
-
|
|
174
|
+
// AunuBaileys (1.0.86, HB-112 short series): same
|
|
175
|
+
// slice-arming as aunuSchedule — a self-destruct set
|
|
176
|
+
// more than ~24.8 days out used to be clamped by Node
|
|
177
|
+
// to 1 ms and deleted the message almost immediately
|
|
178
|
+
// after it was sent. The map always holds the current
|
|
179
|
+
// slice's handle, so cancel and socket-end cleanup are
|
|
180
|
+
// unchanged.
|
|
181
|
+
const fireAt = Date.now() + Math.max(0.05, Number(seconds) || 0) * 1000;
|
|
182
|
+
const fire = () => {
|
|
156
183
|
destructs.delete(key.id);
|
|
157
184
|
Promise.resolve(sock.sendMessage(jid, { delete: key })).catch((err) => {
|
|
158
185
|
logger?.warn?.({ err, jid, id: key.id }, 'aunu: self-destruct delete failed');
|
|
159
186
|
});
|
|
160
|
-
}
|
|
161
|
-
|
|
162
|
-
|
|
187
|
+
};
|
|
188
|
+
const arm = () => {
|
|
189
|
+
const remaining = fireAt - Date.now();
|
|
190
|
+
const timer = remaining > MAX_TIMER_DELAY_MS
|
|
191
|
+
? setTimeout(arm, MAX_TIMER_DELAY_MS)
|
|
192
|
+
: setTimeout(fire, Math.max(0, remaining));
|
|
193
|
+
timer.unref?.();
|
|
194
|
+
destructs.set(key.id, timer);
|
|
195
|
+
};
|
|
196
|
+
arm();
|
|
163
197
|
}
|
|
164
198
|
return sent;
|
|
165
199
|
};
|
package/lib/Socket/chats.js
CHANGED
|
@@ -906,16 +906,33 @@ export const makeChatsSocket = (config) => {
|
|
|
906
906
|
}
|
|
907
907
|
}
|
|
908
908
|
const historyMsg = getHistoryMsg(msg.message);
|
|
909
|
-
|
|
909
|
+
// AunuBaileys (1.0.86, HB-113 short series): the
|
|
910
|
+
// shouldSyncHistoryMessage preference governs PASSIVE sync
|
|
911
|
+
// only. An ON_DEMAND payload is the answer to an explicit
|
|
912
|
+
// host request (fetchMessageHistory /
|
|
913
|
+
// requestPlaceholderResend); the single shared gate used
|
|
914
|
+
// to discard it silently whenever the preference was off
|
|
915
|
+
// — including the syncFullHistory:false default — so the
|
|
916
|
+
// host's request vanished without an error and without
|
|
917
|
+
// the messaging-history.set event. The initial-sync state
|
|
918
|
+
// machine below keeps the preference-based predicate
|
|
919
|
+
// (syncPreferenceAllows): on-demand responses must not
|
|
920
|
+
// drive it. For every non-ON_DEMAND input, and for
|
|
921
|
+
// ON_DEMAND with the preference on, syncPreferenceAllows
|
|
922
|
+
// equals the old gate exactly.
|
|
923
|
+
const syncPreferenceAllows = historyMsg
|
|
910
924
|
? shouldSyncHistoryMessage(historyMsg) && PROCESSABLE_HISTORY_TYPES.includes(historyMsg.syncType)
|
|
911
925
|
: false;
|
|
926
|
+
const shouldProcessHistoryMsg = historyMsg?.syncType === proto.HistorySync.HistorySyncType.ON_DEMAND
|
|
927
|
+
? true
|
|
928
|
+
: syncPreferenceAllows;
|
|
912
929
|
// State machine: decide on sync and flush
|
|
913
930
|
if (historyMsg && syncState === SyncState.AwaitingInitialSync) {
|
|
914
931
|
if (awaitingSyncTimeout) {
|
|
915
932
|
clearTimeout(awaitingSyncTimeout);
|
|
916
933
|
awaitingSyncTimeout = undefined;
|
|
917
934
|
}
|
|
918
|
-
if (
|
|
935
|
+
if (syncPreferenceAllows) {
|
|
919
936
|
syncState = SyncState.Syncing;
|
|
920
937
|
logger.info('Transitioned to Syncing state');
|
|
921
938
|
// Let doAppStateSync handle the final flush after it's done
|
|
@@ -1986,9 +1986,22 @@ export const makeMessagesRecvSocket = (config) => {
|
|
|
1986
1986
|
msg.messageStubType = call.isVideo ? WAMessageStubType.CALL_MISSED_VIDEO : WAMessageStubType.CALL_MISSED_VOICE;
|
|
1987
1987
|
}
|
|
1988
1988
|
}
|
|
1989
|
-
else {
|
|
1989
|
+
else if (call.id) {
|
|
1990
1990
|
msg.message = { call: { callKey: Buffer.from(call.id) } };
|
|
1991
1991
|
}
|
|
1992
|
+
else {
|
|
1993
|
+
// AunuBaileys (1.0.86, HB-216): a group call offer
|
|
1994
|
+
// without a call-id (an optional stanza attr the
|
|
1995
|
+
// decoder does not validate) made Buffer.from throw
|
|
1996
|
+
// a raw TypeError inside this listener; the throw
|
|
1997
|
+
// unwound the whole 'call' emit, so host listeners
|
|
1998
|
+
// registered after this one never saw the offer at
|
|
1999
|
+
// all. There is no call key to reference and the
|
|
2000
|
+
// message key would carry no id either — skip the
|
|
2001
|
+
// notification message; the 'call' event itself
|
|
2002
|
+
// still reaches every listener.
|
|
2003
|
+
return;
|
|
2004
|
+
}
|
|
1992
2005
|
const protoMsg = proto.WebMessageInfo.fromObject(msg);
|
|
1993
2006
|
// AunuBaileys: this floated unawaited and uncaught — a
|
|
1994
2007
|
// rejection here was an unhandledRejection that could kill
|
|
@@ -279,7 +279,17 @@ export const makeMessagesSocket = (config) => {
|
|
|
279
279
|
try {
|
|
280
280
|
const { recipientJid: jid, ...patchedMessage } = patchedMessageWithJid;
|
|
281
281
|
if (!jid) {
|
|
282
|
-
return
|
|
282
|
+
// AunuBaileys (1.0.86, HB-136): this used to return
|
|
283
|
+
// {} — truthy, so it passed the filter below and
|
|
284
|
+
// counted as a SUCCESSFUL node. A patch whose every
|
|
285
|
+
// entry lacked a recipientJid therefore "sent" a
|
|
286
|
+
// stanza with an empty <participants> list and
|
|
287
|
+
// relayMessage resolved, instead of failing with
|
|
288
|
+
// 'All encryptions failed' (the encoder silently
|
|
289
|
+
// drops tag-less children, so the stanza really
|
|
290
|
+
// went out). Upstream returns null here — the same
|
|
291
|
+
// value the per-recipient catch below already uses.
|
|
292
|
+
return null;
|
|
283
293
|
}
|
|
284
294
|
const bytes = encodeWAMessage(patchedMessage);
|
|
285
295
|
let encResult;
|
|
@@ -22,7 +22,7 @@ export declare const makeNewsletterSocket: (sock: GroupsSocket) => {
|
|
|
22
22
|
subscribeNewsletterUpdates: (jid: string) => Promise<{
|
|
23
23
|
duration: string;
|
|
24
24
|
} | null>;
|
|
25
|
-
newsletterAdminCount: (jid: string) => Promise<number>;
|
|
25
|
+
newsletterAdminCount: (jid: string) => Promise<number | undefined>;
|
|
26
26
|
newsletterChangeOwner: (jid: string, newOwnerJid: string) => Promise<void>;
|
|
27
27
|
newsletterDemote: (jid: string, userJid: string) => Promise<void>;
|
|
28
28
|
newsletterDelete: (jid: string) => Promise<void>;
|
package/lib/Socket/newsletter.js
CHANGED
|
@@ -181,7 +181,13 @@ export const makeNewsletterSocket = (sock) => {
|
|
|
181
181
|
},
|
|
182
182
|
newsletterAdminCount: async (jid) => {
|
|
183
183
|
const response = await executeWMexQuery({ newsletter_id: jid }, QueryIds.ADMIN_COUNT, XWAPaths.xwa2_newsletter_admin_count);
|
|
184
|
-
|
|
184
|
+
// AunuBaileys (1.0.86, HB-111 short series): the guard in
|
|
185
|
+
// executeWMexQuery only rejects an UNDEFINED response —
|
|
186
|
+
// a GraphQL null for the admin path passes through
|
|
187
|
+
// verbatim, and this bare deref threw a raw TypeError at
|
|
188
|
+
// the host. A channel without admin info simply has no
|
|
189
|
+
// count to report.
|
|
190
|
+
return response?.admin_count;
|
|
185
191
|
},
|
|
186
192
|
newsletterChangeOwner: async (jid, newOwnerJid) => {
|
|
187
193
|
await executeWMexQuery({ newsletter_id: jid, user_id: newOwnerJid }, QueryIds.CHANGE_OWNER, XWAPaths.xwa2_newsletter_change_owner);
|
|
@@ -306,6 +306,13 @@ const makeInMemoryStore = ({ logger: _logger, chatKey, labelAssociationKey }) =>
|
|
|
306
306
|
});
|
|
307
307
|
ev.on('messages.reaction', (reactions) => {
|
|
308
308
|
for (const { key, reaction } of reactions) {
|
|
309
|
+
// AunuBaileys (1.0.86, HB-187 addendum): a key-less
|
|
310
|
+
// reaction (host-emitted, or delivered unbuffered)
|
|
311
|
+
// dereferenced key.remoteJid bare here and threw
|
|
312
|
+
// inside the listener. It has no target; skip it.
|
|
313
|
+
if (!key?.remoteJid) {
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
309
316
|
const obj = messages[key.remoteJid];
|
|
310
317
|
const msg = obj === null || obj === void 0 ? void 0 : obj.get(key.id);
|
|
311
318
|
if (msg) {
|
|
@@ -100,7 +100,7 @@ export const makeEventBuffer = (logger) => {
|
|
|
100
100
|
delete data.chatUpdates[update.id];
|
|
101
101
|
}
|
|
102
102
|
}
|
|
103
|
-
const
|
|
103
|
+
const consolidatedMaps = consolidateEvents(data);
|
|
104
104
|
// AunuBaileys (1.0.81, HB-166 + HB-224): swap the fresh
|
|
105
105
|
// buffer in BEFORE emitting, and guard the emit. The old
|
|
106
106
|
// order assigned `data = newData` after the emit, so
|
|
@@ -116,12 +116,14 @@ export const makeEventBuffer = (logger) => {
|
|
|
116
116
|
// paths (processNodeWithBuffer), where a throw became an
|
|
117
117
|
// uncaught exception / unhandled rejection.
|
|
118
118
|
data = newData;
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
|
|
119
|
+
for (const consolidatedData of consolidatedMaps) {
|
|
120
|
+
if (Object.keys(consolidatedData).length) {
|
|
121
|
+
try {
|
|
122
|
+
ev.emit('event', consolidatedData);
|
|
123
|
+
}
|
|
124
|
+
catch (err) {
|
|
125
|
+
logger.error({ err }, 'error emitting flushed events');
|
|
126
|
+
}
|
|
125
127
|
}
|
|
126
128
|
}
|
|
127
129
|
logger.trace({ conditionalChatUpdatesLeft }, 'released buffered events');
|
|
@@ -433,6 +435,17 @@ eventData, logger) {
|
|
|
433
435
|
case 'messages.reaction':
|
|
434
436
|
const reactions = eventData;
|
|
435
437
|
for (const { key, reaction } of reactions) {
|
|
438
|
+
// AunuBaileys (1.0.86, HB-187): a reaction whose
|
|
439
|
+
// target key is missing (an optional proto field)
|
|
440
|
+
// used to throw a raw TypeError inside
|
|
441
|
+
// stringifyMessageKey — inside upsertMessage, after
|
|
442
|
+
// the delivery receipt had already been sent, so
|
|
443
|
+
// the stanza ALSO earned a NACK 500 and the
|
|
444
|
+
// reaction was lost. A key-less reaction has no
|
|
445
|
+
// target to attach to; skip it.
|
|
446
|
+
if (!key) {
|
|
447
|
+
continue;
|
|
448
|
+
}
|
|
436
449
|
const keyStr = stringifyMessageKey(key);
|
|
437
450
|
const existing = data.messageUpserts[keyStr];
|
|
438
451
|
if (existing) {
|
|
@@ -462,10 +475,15 @@ eventData, logger) {
|
|
|
462
475
|
const groupUpdates = eventData;
|
|
463
476
|
for (const update of groupUpdates) {
|
|
464
477
|
const id = update.id;
|
|
465
|
-
|
|
466
|
-
|
|
467
|
-
|
|
468
|
-
|
|
478
|
+
// AunuBaileys (1.0.86, HB-176): a second update for
|
|
479
|
+
// the same group inside one buffer window used to
|
|
480
|
+
// be discarded whole — the merge only ran when no
|
|
481
|
+
// update was stored yet, so a subject change
|
|
482
|
+
// followed by a description change delivered only
|
|
483
|
+
// the subject and the store kept stale metadata.
|
|
484
|
+
// Merge into the stored update, like chats.update
|
|
485
|
+
// and contacts.update already do.
|
|
486
|
+
data.groupUpdates[id] = Object.assign(data.groupUpdates[id] || {}, update);
|
|
469
487
|
}
|
|
470
488
|
break;
|
|
471
489
|
default:
|
|
@@ -507,6 +525,11 @@ eventData, logger) {
|
|
|
507
525
|
}
|
|
508
526
|
function consolidateEvents(data) {
|
|
509
527
|
const map = {};
|
|
528
|
+
// AunuBaileys (1.0.86, HB-159): extra maps emitted right after
|
|
529
|
+
// the main one (see the messages.upsert grouping below). The
|
|
530
|
+
// common single-type case leaves this empty and behaviour is
|
|
531
|
+
// exactly what it was before.
|
|
532
|
+
const extraMaps = [];
|
|
510
533
|
if (!data.historySets.empty) {
|
|
511
534
|
map['messaging-history.set'] = {
|
|
512
535
|
chats: Object.values(data.historySets.chats),
|
|
@@ -532,11 +555,40 @@ function consolidateEvents(data) {
|
|
|
532
555
|
}
|
|
533
556
|
const messageUpsertList = Object.values(data.messageUpserts);
|
|
534
557
|
if (messageUpsertList.length) {
|
|
535
|
-
|
|
558
|
+
// AunuBaileys (1.0.86, HB-159): the whole batch used to
|
|
559
|
+
// take the FIRST message's type. A live 'notify' message
|
|
560
|
+
// buffered behind offline 'append' messages (the initial
|
|
561
|
+
// offline drain, or a history resync racing live traffic)
|
|
562
|
+
// was delivered as 'append' — the store never created its
|
|
563
|
+
// chat, the unread count vanished, and hosts that only act
|
|
564
|
+
// on 'notify' never saw a live message. Group by type
|
|
565
|
+
// instead: same-type batches keep the single consolidated
|
|
566
|
+
// event; a mixed batch emits one messages.upsert per type
|
|
567
|
+
// (the extra maps flush right after the main one), every
|
|
568
|
+
// message keeping its own type, in arrival order.
|
|
569
|
+
const messagesByType = new Map();
|
|
570
|
+
for (const entry of messageUpsertList) {
|
|
571
|
+
const list = messagesByType.get(entry.type);
|
|
572
|
+
if (list) {
|
|
573
|
+
list.push(entry.message);
|
|
574
|
+
}
|
|
575
|
+
else {
|
|
576
|
+
messagesByType.set(entry.type, [entry.message]);
|
|
577
|
+
}
|
|
578
|
+
}
|
|
579
|
+
const [firstType, ...otherTypes] = [...messagesByType.keys()];
|
|
536
580
|
map['messages.upsert'] = {
|
|
537
|
-
messages:
|
|
538
|
-
type
|
|
581
|
+
messages: messagesByType.get(firstType),
|
|
582
|
+
type: firstType
|
|
539
583
|
};
|
|
584
|
+
for (const type of otherTypes) {
|
|
585
|
+
extraMaps.push({
|
|
586
|
+
'messages.upsert': {
|
|
587
|
+
messages: messagesByType.get(type),
|
|
588
|
+
type
|
|
589
|
+
}
|
|
590
|
+
});
|
|
591
|
+
}
|
|
540
592
|
}
|
|
541
593
|
const messageUpdateList = Object.values(data.messageUpdates);
|
|
542
594
|
if (messageUpdateList.length) {
|
|
@@ -566,7 +618,7 @@ function consolidateEvents(data) {
|
|
|
566
618
|
if (groupUpdateList.length) {
|
|
567
619
|
map['groups.update'] = groupUpdateList;
|
|
568
620
|
}
|
|
569
|
-
return map;
|
|
621
|
+
return [map, ...extraMaps];
|
|
570
622
|
}
|
|
571
623
|
function concatChats(a, b) {
|
|
572
624
|
if (b.unreadCount === null && // neutralize unread counter
|
package/lib/Utils/history.js
CHANGED
|
@@ -39,6 +39,17 @@ export const processHistoryMessage = (item) => {
|
|
|
39
39
|
delete chat.messages;
|
|
40
40
|
for (const item of msgs) {
|
|
41
41
|
const message = item.message;
|
|
42
|
+
// AunuBaileys (1.0.86, HB-181): HistorySyncMsg.
|
|
43
|
+
// message is an OPTIONAL proto field — a wrapper
|
|
44
|
+
// carrying only msgOrderId decodes with message
|
|
45
|
+
// === null, and the bare derefs below (starting
|
|
46
|
+
// at message.key) threw a TypeError that cost
|
|
47
|
+
// the WHOLE history chunk (every conversation
|
|
48
|
+
// in the blob was NACKed and lost), not just the
|
|
49
|
+
// empty entry. Skip the entry.
|
|
50
|
+
if (!message) {
|
|
51
|
+
continue;
|
|
52
|
+
}
|
|
42
53
|
messages.push(message);
|
|
43
54
|
if (!chat.messages?.length) {
|
|
44
55
|
// keep only the most recent message in the chat array
|
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
|
|
@@ -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
|