@crysnovax/baileys 2.6.3 → 2.6.5

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
@@ -721,6 +721,62 @@ sock.sendMessage(jid, {
721
721
  ]
722
722
  })
723
723
  ```
724
+ ### Rich preview
725
+ ```
726
+ await sock.sendMessage(jid, {
727
+ text: 'https://chat.whatsapp.com/CODE',
728
+ richPreview: true,
729
+ previewTitle: 'My Group',
730
+ previewDescription: '42 members · Tap to join',
731
+ previewImage: imageBufferOrUrl // Buffer, or a URL string
732
+ })
733
+ ```
734
+
735
+ ### Rich preview for group status
736
+ ```
737
+ // Fully auto — fetches title, description, image from the URL
738
+ await sock.sendMessage(jid, {
739
+ text: 'https://facebook.com/share/v/14i...',
740
+ richPreview: true
741
+ })
742
+
743
+ // Group status with auto-fetched preview
744
+ await sock.sendMessage(jid, {
745
+ text: 'https://chat.whatsapp.com/CODE',
746
+ richPreview: true,
747
+ groupStatus: true
748
+ })
749
+ ```
750
+ ### SEND MESSAGES EXACTLY AS IS NO PROCESSING
751
+ ```
752
+ // Relay a received message exactly as-is
753
+ await sock.sendMessage(jid, {
754
+ likeThis: true,
755
+ ...m.message // spread the captured message directly
756
+ })
757
+
758
+ // Send a manually constructed proto without normalization
759
+ await sock.sendMessage(jid, {
760
+ likeThis: true,
761
+ imageMessage: { ...rawFields }
762
+ })
763
+
764
+ // Re-forward with original quality intact
765
+ await sock.sendMessage(jid, {
766
+ likeThis: true,
767
+ ...capturedMsg.message
768
+ })
769
+ ```
770
+
771
+ ### WHATSAPP VERIFIED ☑️ ⚉
772
+ send Media messages with verified WhatsApp check ✔️
773
+ ```
774
+ await sock.sendMessage(jid, {
775
+ image: buffer,
776
+ caption: 'Hello',
777
+ verifiedMe: true
778
+ })
779
+ ```
724
780
 
725
781
  ---
726
782
 
@@ -3,7 +3,7 @@ import { Boom } from '@hapi/boom';
3
3
  import { randomBytes } from 'crypto';
4
4
  import { proto } from '../../WAProto/index.js';
5
5
  import { BIZ_BOT_SUPPORT_PAYLOAD, DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
6
- import { aggregateMessageKeysNotFromMe, assertMediaContent, assertMeId, bindWaitForEvent, decryptMediaRetryData, DEF_MEDIA_HOST, delay, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2, generateParticipantHashV2, generateWAMessage, generateWAMessageFromContent, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, hasValidAlbumMedia, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, shouldIncludeBizBinaryNode, unixTimestampSeconds } from '../Utils/index.js';
6
+ import { aggregateMessageKeysNotFromMe, assertMediaContent, assertMeId, bindWaitForEvent, buildLinkPreview, decryptMediaRetryData, DEF_MEDIA_HOST, delay, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, extractImageThumb, generateMessageIDV2, generateParticipantHashV2, generateWAMessage, generateWAMessageFromContent, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, hasValidAlbumMedia, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, prepareWAMessageMedia, shouldIncludeBizBinaryNode, unixTimestampSeconds } from '../Utils/index.js';
7
7
  import { AssociationType } from '../Types/index.js';
8
8
  import { getUrlInfo } from '../Utils/link-preview.js';
9
9
  import { makeKeyedMutex, makeMutex } from '../Utils/make-mutex.js';
@@ -1248,14 +1248,206 @@ export const makeMessagesSocket = (config) => {
1248
1248
  }
1249
1249
  return fullMsg;
1250
1250
  }
1251
- else {
1252
- const fullMsg = await generateWAMessage(jid, content, {
1253
- logger,
1254
- userJid,
1255
- getUrlInfo: text => getUrlInfo(text, {
1256
- thumbnailWidth: linkPreviewImageThumbnailWidth,
1257
- fetchOpts: {
1258
- timeout: 3000,
1251
+ // crysnovax@RichPreview --- richPreview:true auto-builds a manually-constructed
1252
+ // extendedTextMessage with a large preview image, bypassing generateWAMessage's
1253
+ // getUrlInfo scraper (which fails on bot-blocked domains like chat.whatsapp.com)
1254
+ // and avoiding the undocumented ~7KB jpegThumbnail size ceiling that causes a
1255
+ // blank/black render on WA clients when exceeded.
1256
+ //
1257
+ // Usage:
1258
+ // sock.sendMessage(jid, {
1259
+ // text: 'https://chat.whatsapp.com/CODE',
1260
+ // richPreview: true,
1261
+ // previewTitle: 'Group Name',
1262
+ // previewDescription: '12 members · Tap to join',
1263
+ // previewImage: imageBufferOrUrl // Buffer, or a URL string to fetch
1264
+ // })
1265
+ else if ('richPreview' in content && content.richPreview === true) {
1266
+ const {
1267
+ richPreview: __rp,
1268
+ text: previewLink,
1269
+ previewTitle,
1270
+ previewDescription,
1271
+ previewImage,
1272
+ groupStatus: isGroupStatus,
1273
+ quoted,
1274
+ ...restContent
1275
+ } = content;
1276
+
1277
+ if (!previewLink || typeof previewLink !== 'string') {
1278
+ throw new Boom('richPreview requires a `text` field containing the URL', { statusCode: 400 });
1279
+ }
1280
+
1281
+ // Auto-fetch metadata (title, description, image) when not provided.
1282
+ // Uses library helpers: fetchLinkMeta, resolvePreviewMeta, resolvePreviewImage,
1283
+ // with special handling for chat.whatsapp.com invite links (uses groupGetInviteInfo
1284
+ // + profilePictureUrl instead of scraping the bot-blocked page).
1285
+ const _preview = await buildLinkPreview(
1286
+ previewLink,
1287
+ sock,
1288
+ {
1289
+ customTitle: previewTitle || '',
1290
+ customDesc: previewDescription || '',
1291
+ customImage: Buffer.isBuffer(previewImage) ? previewImage : null
1292
+ }
1293
+ );
1294
+
1295
+ let imageBuffer = _preview.imageBuffer;
1296
+
1297
+ // If previewImage was a URL string, fetch it (overrides auto-fetch)
1298
+ if (!imageBuffer && typeof previewImage === 'string') {
1299
+ try {
1300
+ const res = await fetch(previewImage);
1301
+ imageBuffer = Buffer.from(await res.arrayBuffer());
1302
+ } catch (err) {
1303
+ logger?.warn({ err }, 'richPreview: failed to fetch previewImage URL');
1304
+ }
1305
+ }
1306
+
1307
+ const resolvedTitle = previewTitle || _preview.title || '';
1308
+ const resolvedDescription = previewDescription || _preview.description || '';
1309
+
1310
+ // Small embedded jpegThumbnail — kept under the ~7KB ceiling
1311
+ // that causes WA clients to render a blank/black preview if exceeded.
1312
+ // 296px @ quality 50 (fixed internally by extractImageThumb) sits
1313
+ // safely under that threshold while staying as sharp as reliably possible.
1314
+ let smallThumb = null;
1315
+ if (imageBuffer) {
1316
+ try {
1317
+ const { buffer } = await extractImageThumb(imageBuffer, 296);
1318
+ smallThumb = buffer;
1319
+ }
1320
+ catch (err) {
1321
+ logger?.warn({ err }, 'richPreview: failed to generate small thumbnail');
1322
+ }
1323
+ }
1324
+
1325
+ // Upload the image through the real media pipeline to get a
1326
+ // proper highQualityThumbnail reference (directPath/mediaKey/etc)
1327
+ let hq = null;
1328
+ if (imageBuffer) {
1329
+ try {
1330
+ const prepared = await prepareWAMessageMedia({ image: imageBuffer }, { upload: waUploadToServer });
1331
+ hq = prepared.imageMessage;
1332
+ }
1333
+ catch (err) {
1334
+ logger?.warn({ err }, 'richPreview: failed to upload HQ thumbnail');
1335
+ }
1336
+ }
1337
+
1338
+ const extendedText = {
1339
+ text: previewLink,
1340
+ matchedText: previewLink,
1341
+ canonicalUrl: previewLink,
1342
+ title: resolvedTitle,
1343
+ description: resolvedDescription,
1344
+ previewType: 5, // IMAGE
1345
+ jpegThumbnail: smallThumb || undefined,
1346
+ ...(hq
1347
+ ? {
1348
+ thumbnailDirectPath: hq.directPath,
1349
+ mediaKey: hq.mediaKey,
1350
+ mediaKeyTimestamp: hq.mediaKeyTimestamp,
1351
+ thumbnailWidth: hq.width,
1352
+ thumbnailHeight: hq.height,
1353
+ thumbnailSha256: hq.fileSha256,
1354
+ thumbnailEncSha256: hq.fileEncSha256
1355
+ }
1356
+ : {}),
1357
+ ...restContent
1358
+ };
1359
+
1360
+ if (quoted) {
1361
+ extendedText.contextInfo = {
1362
+ ...(extendedText.contextInfo || {}),
1363
+ stanzaId: quoted.key.id,
1364
+ participant: jidNormalizedUser(quoted.key.fromMe ? userJid : (quoted.key.participant || quoted.key.remoteJid)),
1365
+ quotedMessage: quoted.message
1366
+ };
1367
+ }
1368
+
1369
+ // Support groupStatus:true alongside richPreview:true
1370
+ if (isGroupStatus) {
1371
+ extendedText.contextInfo = { ...(extendedText.contextInfo || {}), isGroupStatus: true };
1372
+ }
1373
+
1374
+ const previewMessage = isGroupStatus
1375
+ ? { groupStatusMessageV2: { message: { extendedTextMessage: extendedText } } }
1376
+ : { extendedTextMessage: extendedText };
1377
+
1378
+ const msgId = options.messageId || generateMessageIDV2(userJid);
1379
+
1380
+ await relayMessage(jid, previewMessage, {
1381
+ messageId: msgId,
1382
+ additionalAttributes: options.additionalAttributes || {},
1383
+ additionalNodes: options.additionalNodes || []
1384
+ });
1385
+
1386
+ const fullMsg = {
1387
+ key: {
1388
+ remoteJid: jid,
1389
+ fromMe: true,
1390
+ id: msgId
1391
+ },
1392
+ message: previewMessage,
1393
+ messageTimestamp: unixTimestampSeconds()
1394
+ };
1395
+
1396
+ if (config.emitOwnEvents) {
1397
+ process.nextTick(async () => {
1398
+ await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
1399
+ });
1400
+ }
1401
+ return fullMsg;
1402
+ }
1403
+ // crysnovax@LikeThis --- likeThis:true relays the message object exactly as-is,
1404
+ // bypassing generateWAMessage/generateWAMessageContent entirely.
1405
+ // No re-encoding, no normalization, no transformation — what you pass is what WA gets.
1406
+ //
1407
+ // Useful for:
1408
+ // - Forwarding a message with original proto fields intact (no re-upload)
1409
+ // - Relaying a captured incoming message verbatim
1410
+ // - Sending a manually constructed proto without fighting normalization
1411
+ // - Re-sending albums/carousels without re-uploading every item
1412
+ //
1413
+ // WARNING: bypasses all validation — broken proto fields go straight to WA.
1414
+ //
1415
+ // Usage:
1416
+ // sock.sendMessage(jid, {
1417
+ // likeThis: true,
1418
+ // imageMessage: { ...rawProtoFields }
1419
+ // })
1420
+ // sock.sendMessage(jid, {
1421
+ // likeThis: true,
1422
+ // ...capturedMessage.message // spread a received message directly
1423
+ // })
1424
+ else if ('likeThis' in content && content.likeThis === true) {
1425
+ const { likeThis: _, ...rawMessage } = content;
1426
+ const msgId = options.messageId || generateMessageIDV2(userJid);
1427
+
1428
+ await relayMessage(jid, rawMessage, {
1429
+ messageId: msgId,
1430
+ additionalAttributes: options.additionalAttributes || {},
1431
+ additionalNodes: options.additionalNodes || []
1432
+ });
1433
+
1434
+ const fullMsg = {
1435
+ key: {
1436
+ remoteJid: jid,
1437
+ fromMe: true,
1438
+ id: msgId
1439
+ },
1440
+ message: rawMessage,
1441
+ messageTimestamp: unixTimestampSeconds()
1442
+ };
1443
+
1444
+ if (config.emitOwnEvents) {
1445
+ process.nextTick(async () => {
1446
+ await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
1447
+ });
1448
+ }
1449
+ return fullMsg;
1450
+ }
1259
1451
  ...(httpRequestOptions || {})
1260
1452
  },
1261
1453
  logger,
@@ -3,7 +3,7 @@ import { Boom } from '@hapi/boom';
3
3
  import { randomBytes } from 'crypto';
4
4
  import { proto } from '../../WAProto/index.js';
5
5
  import { BIZ_BOT_SUPPORT_PAYLOAD, DEFAULT_CACHE_TTLS, WA_DEFAULT_EPHEMERAL } from '../Defaults/index.js';
6
- import { aggregateMessageKeysNotFromMe, assertMediaContent, assertMeId, bindWaitForEvent, decryptMediaRetryData, DEF_MEDIA_HOST, delay, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, generateMessageIDV2, generateParticipantHashV2, generateWAMessage, generateWAMessageFromContent, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, hasValidAlbumMedia, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, shouldIncludeBizBinaryNode, unixTimestampSeconds } from '../Utils/index.js';
6
+ import { aggregateMessageKeysNotFromMe, assertMediaContent, assertMeId, bindWaitForEvent, decryptMediaRetryData, DEF_MEDIA_HOST, delay, encodeNewsletterMessage, encodeSignedDeviceIdentity, encodeWAMessage, encryptMediaRetryRequest, extractDeviceJids, extractImageThumb, generateMessageIDV2, generateParticipantHashV2, generateWAMessage, generateWAMessageFromContent, getStatusCodeForMediaRetry, getUrlFromDirectPath, getWAUploadToServer, hasValidAlbumMedia, MessageRetryManager, normalizeMessageContent, parseAndInjectE2ESessions, prepareWAMessageMedia, shouldIncludeBizBinaryNode, unixTimestampSeconds } from '../Utils/index.js';
7
7
  import { AssociationType } from '../Types/index.js';
8
8
  import { getUrlInfo } from '../Utils/link-preview.js';
9
9
  import { makeKeyedMutex, makeMutex } from '../Utils/make-mutex.js';
@@ -1218,6 +1218,169 @@ export const makeMessagesSocket = (config) => {
1218
1218
  : disappearingMessagesInChat;
1219
1219
  await groupToggleEphemeral(jid, value);
1220
1220
  }
1221
+ // crysnovax@Status --- status:true flag routes any message type to status@broadcast
1222
+ // Supports: text with backgroundColor + font, image, video, audio, sticker
1223
+ // Usage: sock.sendMessage(jid, { text: 'Hello', status: true, backgroundColor: '#FF0000', font: 2 })
1224
+ // sock.sendMessage(jid, { audio: buffer, status: true, ptt: false })
1225
+ else if ('status' in content && content.status === true) {
1226
+ const { status: _, backgroundColor, font, statusJidList: contentJidList, ...statusContent } = content;
1227
+ const fullMsg = await generateWAMessage('status@broadcast', statusContent, {
1228
+ logger,
1229
+ userJid,
1230
+ upload: waUploadToServer,
1231
+ mediaCache: config.mediaCache,
1232
+ options: config.options,
1233
+ ...options,
1234
+ ...(backgroundColor ? { backgroundColor } : {}),
1235
+ ...(font !== undefined ? { font } : {}),
1236
+ messageId: generateMessageIDV2(userJid)
1237
+ });
1238
+ await relayMessage('status@broadcast', fullMsg.message, {
1239
+ messageId: fullMsg.key.id,
1240
+ statusJidList: contentJidList || options.statusJidList,
1241
+ additionalAttributes: options.additionalAttributes || {},
1242
+ additionalNodes: options.additionalNodes || []
1243
+ });
1244
+ if (config.emitOwnEvents) {
1245
+ process.nextTick(async () => {
1246
+ await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
1247
+ });
1248
+ }
1249
+ return fullMsg;
1250
+ }
1251
+ // crysnovax@RichPreview --- richPreview:true auto-builds a manually-constructed
1252
+ // extendedTextMessage with a large preview image, bypassing generateWAMessage's
1253
+ // getUrlInfo scraper (which fails on bot-blocked domains like chat.whatsapp.com)
1254
+ // and avoiding the undocumented ~7KB jpegThumbnail size ceiling that causes a
1255
+ // blank/black render on WA clients when exceeded.
1256
+ //
1257
+ // Usage:
1258
+ // sock.sendMessage(jid, {
1259
+ // text: 'https://chat.whatsapp.com/CODE',
1260
+ // richPreview: true,
1261
+ // previewTitle: 'Group Name',
1262
+ // previewDescription: '12 members · Tap to join',
1263
+ // previewImage: imageBufferOrUrl // Buffer, or a URL string to fetch
1264
+ // })
1265
+ else if ('richPreview' in content && content.richPreview === true) {
1266
+ const {
1267
+ richPreview: __rp,
1268
+ text: previewLink,
1269
+ previewTitle,
1270
+ previewDescription,
1271
+ previewImage,
1272
+ quoted,
1273
+ ...restContent
1274
+ } = content;
1275
+
1276
+ if (!previewLink || typeof previewLink !== 'string') {
1277
+ throw new Boom('richPreview requires a `text` field containing the URL', { statusCode: 400 });
1278
+ }
1279
+
1280
+ // Resolve previewImage into a raw Buffer, whether given as
1281
+ // a Buffer directly or a URL string to fetch
1282
+ let imageBuffer = null;
1283
+ if (previewImage) {
1284
+ if (Buffer.isBuffer(previewImage)) {
1285
+ imageBuffer = previewImage;
1286
+ }
1287
+ else if (typeof previewImage === 'string') {
1288
+ try {
1289
+ const res = await fetch(previewImage);
1290
+ imageBuffer = Buffer.from(await res.arrayBuffer());
1291
+ }
1292
+ catch (err) {
1293
+ logger?.warn({ err }, 'richPreview: failed to fetch previewImage URL');
1294
+ }
1295
+ }
1296
+ }
1297
+
1298
+ // Small embedded jpegThumbnail — kept under the ~7KB ceiling
1299
+ // that causes WA clients to render a blank/black preview if exceeded.
1300
+ // 296px @ quality 50 (fixed internally by extractImageThumb) sits
1301
+ // safely under that threshold while staying as sharp as reliably possible.
1302
+ let smallThumb = null;
1303
+ if (imageBuffer) {
1304
+ try {
1305
+ const { buffer } = await extractImageThumb(imageBuffer, 296);
1306
+ smallThumb = buffer;
1307
+ }
1308
+ catch (err) {
1309
+ logger?.warn({ err }, 'richPreview: failed to generate small thumbnail');
1310
+ }
1311
+ }
1312
+
1313
+ // Upload the image through the real media pipeline to get a
1314
+ // proper highQualityThumbnail reference (directPath/mediaKey/etc)
1315
+ let hq = null;
1316
+ if (imageBuffer) {
1317
+ try {
1318
+ const prepared = await prepareWAMessageMedia({ image: imageBuffer }, { upload: waUploadToServer });
1319
+ hq = prepared.imageMessage;
1320
+ }
1321
+ catch (err) {
1322
+ logger?.warn({ err }, 'richPreview: failed to upload HQ thumbnail');
1323
+ }
1324
+ }
1325
+
1326
+ const previewMessage = {
1327
+ extendedTextMessage: {
1328
+ text: previewLink,
1329
+ matchedText: previewLink,
1330
+ canonicalUrl: previewLink,
1331
+ title: previewTitle || '',
1332
+ description: previewDescription || '',
1333
+ previewType: 5, // IMAGE
1334
+ jpegThumbnail: smallThumb || undefined,
1335
+ ...(hq
1336
+ ? {
1337
+ thumbnailDirectPath: hq.directPath,
1338
+ mediaKey: hq.mediaKey,
1339
+ mediaKeyTimestamp: hq.mediaKeyTimestamp,
1340
+ thumbnailWidth: hq.width,
1341
+ thumbnailHeight: hq.height,
1342
+ thumbnailSha256: hq.fileSha256,
1343
+ thumbnailEncSha256: hq.fileEncSha256
1344
+ }
1345
+ : {}),
1346
+ ...restContent
1347
+ }
1348
+ };
1349
+
1350
+ const msgId = options.messageId || generateMessageIDV2(userJid);
1351
+
1352
+ if (quoted) {
1353
+ previewMessage.extendedTextMessage.contextInfo = {
1354
+ ...(previewMessage.extendedTextMessage.contextInfo || {}),
1355
+ stanzaId: quoted.key.id,
1356
+ participant: jidNormalizedUser(quoted.key.fromMe ? userJid : (quoted.key.participant || quoted.key.remoteJid)),
1357
+ quotedMessage: quoted.message
1358
+ };
1359
+ }
1360
+
1361
+ await relayMessage(jid, previewMessage, {
1362
+ messageId: msgId,
1363
+ additionalAttributes: options.additionalAttributes || {},
1364
+ additionalNodes: options.additionalNodes || []
1365
+ });
1366
+
1367
+ const fullMsg = {
1368
+ key: {
1369
+ remoteJid: jid,
1370
+ fromMe: true,
1371
+ id: msgId
1372
+ },
1373
+ message: previewMessage,
1374
+ messageTimestamp: unixTimestampSeconds()
1375
+ };
1376
+
1377
+ if (config.emitOwnEvents) {
1378
+ process.nextTick(async () => {
1379
+ await messageMutex.mutex(() => upsertMessage(fullMsg, 'append'));
1380
+ });
1381
+ }
1382
+ return fullMsg;
1383
+ }
1221
1384
  else {
1222
1385
  const fullMsg = await generateWAMessage(jid, content, {
1223
1386
  logger,
@@ -294,6 +294,96 @@ export const generateProfilePictureHD = async (mediaUpload) => {
294
294
  console.log(`[HD-Profile] Output: ${result.length} bytes, dims preserved (max 720px)`);
295
295
  return { img: result };
296
296
  };
297
+ // crysnovax@LinkPreview --- Helper functions for building rich link previews
298
+ // Used internally by the richPreview flag in messages-send.js
299
+ // Can be imported directly for custom preview building in bot commands
300
+ const PREVIEW_USER_AGENTS = [
301
+ 'facebookexternalhit/1.1 (+http://www.facebook.com/externalhit_uatext.php)',
302
+ 'Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36',
303
+ 'Twitterbot/1.0'
304
+ ];
305
+ export const decodeHtmlEntities = (str = '') => str
306
+ .replace(/&/g, '&')
307
+ .replace(/&lt;/g, '<')
308
+ .replace(/&gt;/g, '>')
309
+ .replace(/&quot;/g, '"')
310
+ .replace(/&#39;/g, "'")
311
+ .replace(/&nbsp;/g, ' ')
312
+ .replace(/&#xa0;/gi, ' ')
313
+ .replace(/&#xb7;/gi, '·')
314
+ .replace(/&#(\d+);/g, (_, dec) => String.fromCharCode(Number(dec)))
315
+ .replace(/&#x([0-9a-f]+);/gi, (_, hex) => String.fromCharCode(parseInt(hex, 16)))
316
+ .trim();
317
+ export const fetchLinkMeta = async (url) => {
318
+ for (const ua of PREVIEW_USER_AGENTS) {
319
+ try {
320
+ const controller = new AbortController();
321
+ const t = setTimeout(() => controller.abort(), 5000);
322
+ const res = await fetch(url, { headers: { 'User-Agent': ua }, signal: controller.signal });
323
+ clearTimeout(t);
324
+ if (!res.ok) continue;
325
+ const html = await res.text();
326
+ const ogTitle = html.match(/<meta[^>]*property=["']og:title["'][^>]*content=["']([^"']*)["'][^>]*>/i);
327
+ const titleTag = html.match(/<title[^>]*>([^<]*)<\/title>/i);
328
+ const ogDesc = html.match(/<meta[^>]*property=["']og:description["'][^>]*content=["']([^"']*)["'][^>]*>/i);
329
+ const metaDesc = html.match(/<meta[^>]*name=["']description["'][^>]*content=["']([^"']*)["'][^>]*>/i);
330
+ const ogImg = html.match(/<meta[^>]*property=["']og:image(?::secure_url)?["'][^>]*content=["']([^"']*)["'][^>]*>/i);
331
+ return {
332
+ title: decodeHtmlEntities(ogTitle?.[1] || titleTag?.[1] || ''),
333
+ description: decodeHtmlEntities(ogDesc?.[1] || metaDesc?.[1] || ''),
334
+ imageUrl: decodeHtmlEntities(ogImg?.[1] || '')
335
+ };
336
+ } catch {}
337
+ }
338
+ return { title: '', description: '', imageUrl: '' };
339
+ };
340
+ export const resolvePreviewImage = async (imageUrl) => {
341
+ if (!imageUrl) return null;
342
+ for (const ua of PREVIEW_USER_AGENTS) {
343
+ try {
344
+ const res = await fetch(imageUrl, { headers: { 'User-Agent': ua } });
345
+ if (res.ok) return Buffer.from(await res.arrayBuffer());
346
+ } catch {}
347
+ }
348
+ return null;
349
+ };
350
+ export const resolvePreviewMeta = async (url, sock, customTitle, customDesc) => {
351
+ if (customTitle && customDesc) return { title: customTitle, description: customDesc };
352
+ if (url.includes('chat.whatsapp.com')) {
353
+ try {
354
+ const code = url.split('chat.whatsapp.com/')[1]?.split('?')[0];
355
+ const info = await sock?.groupGetInviteInfo(code);
356
+ return {
357
+ title: customTitle || info?.subject || '',
358
+ description: customDesc || `${info?.size || 0} members · WhatsApp Group`
359
+ };
360
+ } catch {}
361
+ return { title: customTitle || '', description: customDesc || 'WhatsApp Group Invite' };
362
+ }
363
+ const meta = await fetchLinkMeta(url);
364
+ return { title: customTitle || meta.title, description: customDesc || meta.description };
365
+ };
366
+ export const buildLinkPreview = async (url, sock, options = {}) => {
367
+ const { customTitle = '', customDesc = '', customImage } = options;
368
+ const { title, description } = await resolvePreviewMeta(url, sock, customTitle, customDesc);
369
+ let imageBuffer = Buffer.isBuffer(customImage) ? customImage : null;
370
+ if (!imageBuffer) {
371
+ if (url.includes('chat.whatsapp.com')) {
372
+ try {
373
+ const code = url.split('chat.whatsapp.com/')[1]?.split('?')[0];
374
+ const info = await sock?.groupGetInviteInfo(code);
375
+ if (info?.id) {
376
+ const ppUrl = await sock?.profilePictureUrl(info.id, 'image');
377
+ imageBuffer = await resolvePreviewImage(ppUrl);
378
+ }
379
+ } catch {}
380
+ } else {
381
+ const meta = await fetchLinkMeta(url);
382
+ if (meta.imageUrl) imageBuffer = await resolvePreviewImage(meta.imageUrl);
383
+ }
384
+ }
385
+ return { url, title, description, imageBuffer };
386
+ };
297
387
  /** gets the SHA256 of the given media message */
298
388
  export const mediaMessageSHA256B64 = (message) => {
299
389
  const media = Object.values(message)[0];
@@ -220,6 +220,80 @@ export const generateProfilePicture = async (mediaUpload, dimensions) => {
220
220
  img: await img
221
221
  };
222
222
  };
223
+ // crysnovax@HD-Profile --- Like generateProfilePicture but skips resize/crop.
224
+ // Re-encodes to JPEG so WA always gets valid image bytes.
225
+ // crysnovax@HD-Profile --- HD mode: preserves original aspect ratio, no crop.
226
+ // WhatsApp server accepts max 720px on any side. Images under 720px pass through
227
+ // unchanged. Larger images are scaled down proportionally to fit within 720px.
228
+ export const generateProfilePictureHD = async (mediaUpload) => {
229
+ let buffer;
230
+ if (Buffer.isBuffer(mediaUpload)) {
231
+ buffer = mediaUpload;
232
+ }
233
+ else {
234
+ const { stream } = await getStream(mediaUpload);
235
+ buffer = await toBuffer(stream);
236
+ }
237
+
238
+ const lib = await getImageProcessingLibrary();
239
+ let img;
240
+
241
+ if ('sharp' in lib && lib.sharp?.default) {
242
+ const metadata = await lib.sharp.default(buffer).metadata();
243
+ const origWidth = metadata.width || 720;
244
+ const origHeight = metadata.height || 720;
245
+ const maxDim = Math.max(origWidth, origHeight);
246
+
247
+ // WhatsApp server limit: 720px max dimension
248
+ // If already under 720, send as-is. If larger, scale down proportionally.
249
+ const needsResize = maxDim > 720;
250
+
251
+ let pipeline = lib.sharp.default(buffer);
252
+
253
+ if (needsResize) {
254
+ pipeline = pipeline.resize(720, 720, {
255
+ fit: 'inside',
256
+ withoutEnlargement: true
257
+ });
258
+ }
259
+
260
+ img = pipeline
261
+ .jpeg({ quality: 85, mozjpeg: true })
262
+ .toBuffer();
263
+ }
264
+ else if ('image' in lib && lib.image?.Transformer) {
265
+ // @napi-rs/image — cap at 720 via resize
266
+ img = new lib.image
267
+ .Transformer(buffer)
268
+ .resize(720, 720)
269
+ .jpeg(85);
270
+ }
271
+ else if ('jimp' in lib && lib.jimp?.Jimp) {
272
+ const jimp = await lib.jimp.Jimp.read(buffer);
273
+ const origWidth = jimp.width;
274
+ const origHeight = jimp.height;
275
+ const maxDim = Math.max(origWidth, origHeight);
276
+
277
+ let processed = jimp;
278
+ if (maxDim > 720) {
279
+ const scale = 720 / maxDim;
280
+ processed = jimp.resize({
281
+ w: Math.round(origWidth * scale),
282
+ h: Math.round(origHeight * scale),
283
+ mode: lib.jimp.ResizeStrategy.BILINEAR
284
+ });
285
+ }
286
+
287
+ img = processed.getBuffer('image/jpeg', { quality: 85 });
288
+ }
289
+ else {
290
+ throw new Boom('No image processing library available');
291
+ }
292
+
293
+ const result = await img;
294
+ console.log(`[HD-Profile] Output: ${result.length} bytes, dims preserved (max 720px)`);
295
+ return { img: result };
296
+ };
223
297
  /** gets the SHA256 of the given media message */
224
298
  export const mediaMessageSHA256B64 = (message) => {
225
299
  const media = Object.values(message)[0];