diff --git a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts index 4db8146cc..2068096f9 100644 --- a/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts +++ b/src/api/integrations/channel/whatsapp/whatsapp.baileys.service.ts @@ -139,6 +139,7 @@ import { createHash } from 'crypto'; import EventEmitter2 from 'eventemitter2'; import ffmpeg from 'fluent-ffmpeg'; import FormData from 'form-data'; +import { getLinkPreview } from 'link-preview-js'; import Long from 'long'; import mimeTypes from 'mime-types'; import NodeCache from 'node-cache'; @@ -436,7 +437,7 @@ export class BaileysStartupService extends ChannelStartupService { qrcodeTerminal.generate(qr, { small: true }, (qrcode) => this.logger.log( `\n{ instance: ${this.instance.name} pairingCode: ${this.instance.qrcode.pairingCode}, qrcodeCount: ${this.instance.qrcode.count} }\n` + - qrcode, + qrcode, ), ); @@ -1049,16 +1050,16 @@ export class BaileysStartupService extends ChannelStartupService { const messagesRepository: Set = new Set( chatwootImport.getRepositoryMessagesCache(instance) ?? - ( - await this.prismaRepository.message.findMany({ - select: { key: true }, - where: { instanceId: this.instanceId }, - }) - ).map((message) => { - const key = message.key as { id: string }; + ( + await this.prismaRepository.message.findMany({ + select: { key: true }, + where: { instanceId: this.instanceId }, + }) + ).map((message) => { + const key = message.key as { id: string }; - return key.id; - }), + return key.id; + }), ); if (chatwootImport.getRepositoryMessagesCache(instance) === null) { @@ -2221,6 +2222,46 @@ export class BaileysStartupService extends ChannelStartupService { } } + private async generateLinkPreview(text: string) { + try { + const linkRegex = /https?:\/\/[^\s]+/; + const match = text.match(linkRegex); + + if (!match) return undefined; + + // Trim common trailing punctuation that may follow URLs in natural text + const url = match[0].replace(/[.,);\]]+$/u, ''); + if (!url) return undefined; + + const previewData = await getLinkPreview(url, { + imagesPropertyType: 'og', // fetches only open-graph images + headers: { + 'user-agent': 'googlebot', // fetches with googlebot to prevent login pages + }, + }) as any; + + if (!previewData || !previewData.title) return undefined; + + const image = previewData.images && previewData.images.length > 0 ? previewData.images[0] : undefined; + + return { + externalAdReply: { + title: previewData.title, + body: previewData.description, + mediaType: 2, // 2 for video/image preview, though usually 1 is for thumbnail + thumbnailUrl: image, + sourceUrl: url, + mediaUrl: url, + renderLargerThumbnail: true + // showAdAttribution: true // Removed to prevent "Sent via ad" label + } + }; + } catch (error) { + this.logger.error(`Error generating link preview: ${error}`); + return undefined; + } + } + private async sendMessage( sender: string, message: any, @@ -2432,7 +2473,12 @@ export class BaileysStartupService extends ChannelStartupService { } } - const linkPreview = options?.linkPreview != false ? undefined : false; + const linkPreview = options?.linkPreview === false ? false : undefined; + + let previewContext: any = undefined; + if (linkPreview !== false && (message as any)?.conversation) { + previewContext = await this.generateLinkPreview((message as any).conversation); + } let quoted: WAMessage; @@ -2486,6 +2532,7 @@ export class BaileysStartupService extends ChannelStartupService { quoted, null, group?.ephemeralDuration, + previewContext, // group?.participants, ); } else { @@ -2499,6 +2546,7 @@ export class BaileysStartupService extends ChannelStartupService { unsigned: false, }, disappearingMode: { initiator: 0 }, + ...previewContext, }; messageSent = await this.sendMessage( sender, diff --git a/src/api/integrations/chatbot/typebot/services/typebot.service.ts b/src/api/integrations/chatbot/typebot/services/typebot.service.ts index 03712bfdb..79f3180b8 100644 --- a/src/api/integrations/chatbot/typebot/services/typebot.service.ts +++ b/src/api/integrations/chatbot/typebot/services/typebot.service.ts @@ -368,6 +368,60 @@ export class TypebotService extends BaseChatbotService { sendTelemetry('/message/sendWhatsAppAudio'); } + if (message.type === 'file' || message.type === 'embed') { + const content = message.content as { url?: string; name?: string } | undefined; + if (!content?.url) { + sendTelemetry('/message/sendMediaMissingUrl'); + return; + } + + const mediaUrl = content.url; + const mediaType = this.getMediaType(mediaUrl); + + let fileName = content.name; + if (!fileName) { + try { + const urlObj = new URL(mediaUrl); + const path = urlObj.pathname || ''; + const candidate = path.split('/').pop() || ''; + if (candidate && candidate.includes('.')) { + fileName = candidate; + } + } catch { + // Ignore URL parsing failures + } + + if (!fileName) { + fileName = mediaType && mediaType !== 'document' ? `media.${mediaType}` : 'attachment'; + } + } + + if (mediaType === 'audio') { + await instance.audioWhatsapp( + { + number: session.remoteJid, + delay: settings?.delayMessage || 1000, + encoding: true, + audio: mediaUrl, + }, + false, + ); + } else { + await instance.mediaMessage( + { + number: session.remoteJid, + delay: settings?.delayMessage || 1000, + mediatype: mediaType || 'document', + media: mediaUrl, + fileName, + }, + null, + false, + ); + } + sendTelemetry('/message/sendMedia'); + } + const wait = findItemAndGetSecondsToWait(clientSideActions, message.id); if (wait) {