From f0bdb12e56cea8b0bfbb0dff37c01690274e3e31 Mon Sep 17 00:00:00 2001 From: Adhiraj Singh Date: Thu, 15 Sep 2022 18:40:22 +0530 Subject: [PATCH] feat: generate high quality thumbs on link preview --- README.md | 5 +++++ src/Defaults/index.ts | 1 + src/Socket/messages-send.ts | 14 ++++++++++-- src/Types/Message.ts | 2 ++ src/Types/index.ts | 5 +++++ src/Utils/link-preview.ts | 44 ++++++++++++++++++++++++------------- src/Utils/messages-media.ts | 14 ++++++++++-- src/Utils/messages.ts | 31 ++++++++++++++++++++++++-- 8 files changed, 95 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index f56a99d..933a0cb 100644 --- a/README.md +++ b/README.md @@ -118,6 +118,11 @@ type SocketConfig = { msgRetryCounterMap?: MessageRetryMap /** width for link preview images */ 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 */ syncFullHistory: boolean /** diff --git a/src/Defaults/index.ts b/src/Defaults/index.ts index ecec445..6852b55 100644 --- a/src/Defaults/index.ts +++ b/src/Defaults/index.ts @@ -50,6 +50,7 @@ export const DEFAULT_CONNECTION_CONFIG: SocketConfig = { syncFullHistory: false, linkPreviewImageThumbnailWidth: 192, transactionOpts: { maxCommitRetries: 10, delayBetweenTriesMs: 3000 }, + generateHighQualityLinkPreview: false, getMessage: async() => undefined } diff --git a/src/Socket/messages-send.ts b/src/Socket/messages-send.ts index 21f955a..c5be931 100644 --- a/src/Socket/messages-send.ts +++ b/src/Socket/messages-send.ts @@ -10,7 +10,11 @@ import { areJidsSameUser, BinaryNode, BinaryNodeAttributes, getBinaryNodeChild, import { makeGroupsSocket } from './groups' export const makeMessagesSocket = (config: SocketConfig) => { - const { logger, linkPreviewImageThumbnailWidth } = config + const { + logger, + linkPreviewImageThumbnailWidth, + generateHighQualityLinkPreview + } = config const sock = makeGroupsSocket(config) const { ev, @@ -607,7 +611,13 @@ export const makeMessagesSocket = (config: SocketConfig) => { userJid, getUrlInfo: text => getUrlInfo( text, - { thumbnailWidth: linkPreviewImageThumbnailWidth, timeoutMs: 3_000 }, + { + thumbnailWidth: linkPreviewImageThumbnailWidth, + timeoutMs: 3_000, + uploadImage: generateHighQualityLinkPreview + ? waUploadToServer + : undefined + }, logger ), upload: waUploadToServer, diff --git a/src/Types/Message.ts b/src/Types/Message.ts index 5fc0f97..c1923cc 100644 --- a/src/Types/Message.ts +++ b/src/Types/Message.ts @@ -42,6 +42,7 @@ export interface WAUrlInfo { title: string description?: string jpegThumbnail?: Buffer + highQualityThumbnail?: proto.Message.IImageMessage } // types to generate WA messages @@ -192,6 +193,7 @@ export type WAMediaUploadFunction = (readStream: Readable, opts: { fileEncSha256 export type MediaGenerationOptions = { logger?: Logger + mediaTypeOverride?: MediaType upload: WAMediaUploadFunction /** cache media so it does not have to be uploaded again */ mediaCache?: NodeCache diff --git a/src/Types/index.ts b/src/Types/index.ts index e35c67c..d1fb641 100644 --- a/src/Types/index.ts +++ b/src/Types/index.ts @@ -38,6 +38,11 @@ export type SocketConfig = CommonSocketConfig & { syncFullHistory: boolean /** Should baileys fire init queries automatically, default true */ fireInitQueries: boolean + /** + * generate a high quality link preview, + * entails uploading the jpegThumbnail to WA + * */ + generateHighQualityLinkPreview: boolean /** * 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 diff --git a/src/Utils/link-preview.ts b/src/Utils/link-preview.ts index 0146f0f..b48ee9a 100644 --- a/src/Utils/link-preview.ts +++ b/src/Utils/link-preview.ts @@ -1,5 +1,6 @@ import { Logger } from 'pino' -import { WAUrlInfo } from '../Types' +import { WAMediaUploadFunction, WAUrlInfo } from '../Types' +import { prepareWAMessageMedia } from './messages' import { extractImageThumb, getHttpStream } from './messages-media' const THUMBNAIL_WIDTH_PX = 192 @@ -14,6 +15,7 @@ const getCompressedJpegThumbnail = async(url: string, { thumbnailWidth, timeoutM export type URLGenerationOptions = { thumbnailWidth: number timeoutMs: number + uploadImage?: WAMediaUploadFunction } /** @@ -38,25 +40,37 @@ export const getUrlInfo = async( if(info && 'title' in info) { const [image] = info.images - let jpegThumbnail: Buffer | undefined = undefined - try { - jpegThumbnail = image - ? (await getCompressedJpegThumbnail(image, opts)).buffer - : undefined - } catch(error) { - logger?.debug( - { err: error.stack, url: previewLink }, - 'error in generating thumbnail' - ) - } - - return { + const urlInfo: WAUrlInfo = { 'canonical-url': info.url, 'matched-text': text, title: info.title, 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) { if(!error.message.includes('receive a valid')) { diff --git a/src/Utils/messages-media.ts b/src/Utils/messages-media.ts index 8e209e0..dd7e103 100644 --- a/src/Utils/messages-media.ts +++ b/src/Utils/messages-media.ts @@ -238,9 +238,16 @@ export async function generateThumbnail( } ) { let thumbnail: string | undefined + let originalImageDimensions: { width: number; height: number } | undefined if(mediaType === 'image') { - const { buffer } = await extractImageThumb(file) + const { buffer, original } = await extractImageThumb(file) thumbnail = buffer.toString('base64') + if(original.width && original.height) { + originalImageDimensions = { + width: original.width, + height: original.height, + } + } } else if(mediaType === 'video') { const imgFilename = join(getTmpFilesDirectory(), generateMessageID() + '.jpg') 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 } = {}) => { diff --git a/src/Utils/messages.ts b/src/Utils/messages.ts index bd0f05c..4a34996 100644 --- a/src/Utils/messages.ts +++ b/src/Utils/messages.ts @@ -36,6 +36,8 @@ type MediaUploadData = { fileName?: string jpegThumbnail?: string mimetype?: string + width?: number + height?: number } const MIMETYPE_MAP: { [T in MediaType]?: string } = { @@ -144,7 +146,11 @@ export const prepareWAMessageMedia = async( fileSha256, fileLength, 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 const fileEncSha256B64 = fileEncSha256.toString('base64') const [{ mediaUrl, directPath }] = await Promise.all([ @@ -159,7 +165,17 @@ export const prepareWAMessageMedia = async( (async() => { try { 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') } @@ -280,6 +296,17 @@ export const generateWAMessageContent = async( extContent.description = urlInfo.description extContent.title = urlInfo.title 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