mirror of
https://github.com/FranP-code/Baileys.git
synced 2025-10-13 00:32:22 +00:00
feat: generate high quality thumbs on link preview
This commit is contained in:
@@ -118,6 +118,11 @@ type SocketConfig = {
|
|||||||
msgRetryCounterMap?: MessageRetryMap
|
msgRetryCounterMap?: MessageRetryMap
|
||||||
/** width for link preview images */
|
/** width for link preview images */
|
||||||
linkPreviewImageThumbnailWidth: number
|
linkPreviewImageThumbnailWidth: number
|
||||||
|
/**
|
||||||
|
* generate a high quality link preview,
|
||||||
|
* entails uploading the jpegThumbnail to WA
|
||||||
|
* */
|
||||||
|
generateHighQualityLinkPreview: boolean
|
||||||
/** Should Baileys ask the phone for full history, will be received async */
|
/** Should Baileys ask the phone for full history, will be received async */
|
||||||
syncFullHistory: boolean
|
syncFullHistory: boolean
|
||||||
/**
|
/**
|
||||||
|
|||||||
@@ -50,6 +50,7 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = {
|
|||||||
syncFullHistory: false,
|
syncFullHistory: false,
|
||||||
linkPreviewImageThumbnailWidth: 192,
|
linkPreviewImageThumbnailWidth: 192,
|
||||||
transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 3000 },
|
transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 3000 },
|
||||||
|
generateHighQualityLinkPreview: false,
|
||||||
getMessage: async() => undefined
|
getMessage: async() => undefined
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -10,7 +10,11 @@ import { areJidsSameUser, BinaryNode, BinaryNodeAttributes, getBinaryNodeChild,
|
|||||||
import { makeGroupsSocket } from './groups'
|
import { makeGroupsSocket } from './groups'
|
||||||
|
|
||||||
export const makeMessagesSocket = (config: SocketConfig) => {
|
export const makeMessagesSocket = (config: SocketConfig) => {
|
||||||
const { logger, linkPreviewImageThumbnailWidth } = config
|
const {
|
||||||
|
logger,
|
||||||
|
linkPreviewImageThumbnailWidth,
|
||||||
|
generateHighQualityLinkPreview
|
||||||
|
} = config
|
||||||
const sock = makeGroupsSocket(config)
|
const sock = makeGroupsSocket(config)
|
||||||
const {
|
const {
|
||||||
ev,
|
ev,
|
||||||
@@ -607,7 +611,13 @@ export const makeMessagesSocket = (config: SocketConfig) => {
|
|||||||
userJid,
|
userJid,
|
||||||
getUrlInfo: text => getUrlInfo(
|
getUrlInfo: text => getUrlInfo(
|
||||||
text,
|
text,
|
||||||
{ thumbnailWidth: linkPreviewImageThumbnailWidth, timeoutMs: 3_000 },
|
{
|
||||||
|
thumbnailWidth: linkPreviewImageThumbnailWidth,
|
||||||
|
timeoutMs: 3_000,
|
||||||
|
uploadImage: generateHighQualityLinkPreview
|
||||||
|
? waUploadToServer
|
||||||
|
: undefined
|
||||||
|
},
|
||||||
logger
|
logger
|
||||||
),
|
),
|
||||||
upload: waUploadToServer,
|
upload: waUploadToServer,
|
||||||
|
|||||||
@@ -42,6 +42,7 @@ export interface WAUrlInfo {
|
|||||||
title: string
|
title: string
|
||||||
description?: string
|
description?: string
|
||||||
jpegThumbnail?: Buffer
|
jpegThumbnail?: Buffer
|
||||||
|
highQualityThumbnail?: proto.Message.IImageMessage
|
||||||
}
|
}
|
||||||
|
|
||||||
// types to generate WA messages
|
// types to generate WA messages
|
||||||
@@ -192,6 +193,7 @@ export type WAMediaUploadFunction = (readStream: Readable, opts: { fileEncSha256
|
|||||||
|
|
||||||
export type MediaGenerationOptions = {
|
export type MediaGenerationOptions = {
|
||||||
logger?: Logger
|
logger?: Logger
|
||||||
|
mediaTypeOverride?: MediaType
|
||||||
upload: WAMediaUploadFunction
|
upload: WAMediaUploadFunction
|
||||||
/** cache media so it does not have to be uploaded again */
|
/** cache media so it does not have to be uploaded again */
|
||||||
mediaCache?: NodeCache
|
mediaCache?: NodeCache
|
||||||
|
|||||||
@@ -38,6 +38,11 @@ export type SocketConfig = CommonSocketConfig & {
|
|||||||
syncFullHistory: boolean
|
syncFullHistory: boolean
|
||||||
/** Should baileys fire init queries automatically, default true */
|
/** Should baileys fire init queries automatically, default true */
|
||||||
fireInitQueries: boolean
|
fireInitQueries: boolean
|
||||||
|
/**
|
||||||
|
* generate a high quality link preview,
|
||||||
|
* entails uploading the jpegThumbnail to WA
|
||||||
|
* */
|
||||||
|
generateHighQualityLinkPreview: boolean
|
||||||
/**
|
/**
|
||||||
* fetch a message from your store
|
* fetch a message from your store
|
||||||
* implement this so that messages failed to send (solves the "this message can take a while" issue) can be retried
|
* implement this so that messages failed to send (solves the "this message can take a while" issue) can be retried
|
||||||
|
|||||||
@@ -1,5 +1,6 @@
|
|||||||
import { Logger } from 'pino'
|
import { Logger } from 'pino'
|
||||||
import { WAUrlInfo } from '../Types'
|
import { WAMediaUploadFunction, WAUrlInfo } from '../Types'
|
||||||
|
import { prepareWAMessageMedia } from './messages'
|
||||||
import { extractImageThumb, getHttpStream } from './messages-media'
|
import { extractImageThumb, getHttpStream } from './messages-media'
|
||||||
|
|
||||||
const THUMBNAIL_WIDTH_PX = 192
|
const THUMBNAIL_WIDTH_PX = 192
|
||||||
@@ -14,6 +15,7 @@ const getCompressedJpegThumbnail = async(url: string, { thumbnailWidth, timeoutM
|
|||||||
export type URLGenerationOptions = {
|
export type URLGenerationOptions = {
|
||||||
thumbnailWidth: number
|
thumbnailWidth: number
|
||||||
timeoutMs: number
|
timeoutMs: number
|
||||||
|
uploadImage?: WAMediaUploadFunction
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -38,25 +40,37 @@ export const getUrlInfo = async(
|
|||||||
if(info && 'title' in info) {
|
if(info && 'title' in info) {
|
||||||
const [image] = info.images
|
const [image] = info.images
|
||||||
|
|
||||||
let jpegThumbnail: Buffer | undefined = undefined
|
const urlInfo: WAUrlInfo = {
|
||||||
try {
|
|
||||||
jpegThumbnail = image
|
|
||||||
? (await getCompressedJpegThumbnail(image, opts)).buffer
|
|
||||||
: undefined
|
|
||||||
} catch(error) {
|
|
||||||
logger?.debug(
|
|
||||||
{ err: error.stack, url: previewLink },
|
|
||||||
'error in generating thumbnail'
|
|
||||||
)
|
|
||||||
}
|
|
||||||
|
|
||||||
return {
|
|
||||||
'canonical-url': info.url,
|
'canonical-url': info.url,
|
||||||
'matched-text': text,
|
'matched-text': text,
|
||||||
title: info.title,
|
title: info.title,
|
||||||
description: info.description,
|
description: info.description,
|
||||||
jpegThumbnail
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
if(opts.uploadImage) {
|
||||||
|
const { imageMessage } = await prepareWAMessageMedia(
|
||||||
|
{ image: { url: image } },
|
||||||
|
{ upload: opts.uploadImage, mediaTypeOverride: 'thumbnail-link' }
|
||||||
|
)
|
||||||
|
urlInfo.jpegThumbnail = imageMessage?.jpegThumbnail
|
||||||
|
? Buffer.from(imageMessage.jpegThumbnail)
|
||||||
|
: undefined
|
||||||
|
urlInfo.highQualityThumbnail = imageMessage || undefined
|
||||||
|
console.log(urlInfo)
|
||||||
|
} else {
|
||||||
|
try {
|
||||||
|
urlInfo.jpegThumbnail = image
|
||||||
|
? (await getCompressedJpegThumbnail(image, opts)).buffer
|
||||||
|
: undefined
|
||||||
|
} catch(error) {
|
||||||
|
logger?.debug(
|
||||||
|
{ err: error.stack, url: previewLink },
|
||||||
|
'error in generating thumbnail'
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
return urlInfo
|
||||||
}
|
}
|
||||||
} catch(error) {
|
} catch(error) {
|
||||||
if(!error.message.includes('receive a valid')) {
|
if(!error.message.includes('receive a valid')) {
|
||||||
|
|||||||
@@ -238,9 +238,16 @@ export async function generateThumbnail(
|
|||||||
}
|
}
|
||||||
) {
|
) {
|
||||||
let thumbnail: string | undefined
|
let thumbnail: string | undefined
|
||||||
|
let originalImageDimensions: { width: number; height: number } | undefined
|
||||||
if(mediaType === 'image') {
|
if(mediaType === 'image') {
|
||||||
const { buffer } = await extractImageThumb(file)
|
const { buffer, original } = await extractImageThumb(file)
|
||||||
thumbnail = buffer.toString('base64')
|
thumbnail = buffer.toString('base64')
|
||||||
|
if(original.width && original.height) {
|
||||||
|
originalImageDimensions = {
|
||||||
|
width: original.width,
|
||||||
|
height: original.height,
|
||||||
|
}
|
||||||
|
}
|
||||||
} else if(mediaType === 'video') {
|
} else if(mediaType === 'video') {
|
||||||
const imgFilename = join(getTmpFilesDirectory(), generateMessageID() + '.jpg')
|
const imgFilename = join(getTmpFilesDirectory(), generateMessageID() + '.jpg')
|
||||||
try {
|
try {
|
||||||
@@ -254,7 +261,10 @@ export async function generateThumbnail(
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return thumbnail
|
return {
|
||||||
|
thumbnail,
|
||||||
|
originalImageDimensions
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export const getHttpStream = async(url: string | URL, options: AxiosRequestConfig & { isStream?: true } = {}) => {
|
export const getHttpStream = async(url: string | URL, options: AxiosRequestConfig & { isStream?: true } = {}) => {
|
||||||
|
|||||||
@@ -36,6 +36,8 @@ type MediaUploadData = {
|
|||||||
fileName?: string
|
fileName?: string
|
||||||
jpegThumbnail?: string
|
jpegThumbnail?: string
|
||||||
mimetype?: string
|
mimetype?: string
|
||||||
|
width?: number
|
||||||
|
height?: number
|
||||||
}
|
}
|
||||||
|
|
||||||
const MIMETYPE_MAP: { [T in MediaType]?: string } = {
|
const MIMETYPE_MAP: { [T in MediaType]?: string } = {
|
||||||
@@ -144,7 +146,11 @@ export const prepareWAMessageMedia = async(
|
|||||||
fileSha256,
|
fileSha256,
|
||||||
fileLength,
|
fileLength,
|
||||||
didSaveToTmpPath
|
didSaveToTmpPath
|
||||||
} = await encryptedStream(uploadData.media, mediaType, requiresOriginalForSomeProcessing)
|
} = await encryptedStream(
|
||||||
|
uploadData.media,
|
||||||
|
options.mediaTypeOverride || mediaType,
|
||||||
|
requiresOriginalForSomeProcessing
|
||||||
|
)
|
||||||
// url safe Base64 encode the SHA256 hash of the body
|
// url safe Base64 encode the SHA256 hash of the body
|
||||||
const fileEncSha256B64 = fileEncSha256.toString('base64')
|
const fileEncSha256B64 = fileEncSha256.toString('base64')
|
||||||
const [{ mediaUrl, directPath }] = await Promise.all([
|
const [{ mediaUrl, directPath }] = await Promise.all([
|
||||||
@@ -159,7 +165,17 @@ export const prepareWAMessageMedia = async(
|
|||||||
(async() => {
|
(async() => {
|
||||||
try {
|
try {
|
||||||
if(requiresThumbnailComputation) {
|
if(requiresThumbnailComputation) {
|
||||||
uploadData.jpegThumbnail = await generateThumbnail(bodyPath!, mediaType as any, options)
|
const {
|
||||||
|
thumbnail,
|
||||||
|
originalImageDimensions
|
||||||
|
} = await generateThumbnail(bodyPath!, mediaType as any, options)
|
||||||
|
uploadData.jpegThumbnail = thumbnail
|
||||||
|
if(!uploadData.width && originalImageDimensions) {
|
||||||
|
uploadData.width = originalImageDimensions.width
|
||||||
|
uploadData.height = originalImageDimensions.height
|
||||||
|
logger?.debug('set dimensions')
|
||||||
|
}
|
||||||
|
|
||||||
logger?.debug('generated thumbnail')
|
logger?.debug('generated thumbnail')
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -280,6 +296,17 @@ export const generateWAMessageContent = async(
|
|||||||
extContent.description = urlInfo.description
|
extContent.description = urlInfo.description
|
||||||
extContent.title = urlInfo.title
|
extContent.title = urlInfo.title
|
||||||
extContent.previewType = 0
|
extContent.previewType = 0
|
||||||
|
|
||||||
|
const img = urlInfo.highQualityThumbnail
|
||||||
|
if(img) {
|
||||||
|
extContent.thumbnailDirectPath = img.directPath
|
||||||
|
extContent.mediaKey = img.mediaKey
|
||||||
|
extContent.mediaKeyTimestamp = img.mediaKeyTimestamp
|
||||||
|
extContent.thumbnailWidth = img.width
|
||||||
|
extContent.thumbnailHeight = img.height
|
||||||
|
extContent.thumbnailSha256 = img.fileSha256
|
||||||
|
extContent.thumbnailEncSha256 = img.fileEncSha256
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
m.extendedTextMessage = extContent
|
m.extendedTextMessage = extContent
|
||||||
|
|||||||
Reference in New Issue
Block a user