'use node'; import { v } from 'convex/values'; import { internal } from '../_generated/api'; import { action } from '../_generated/server'; /** * Extrair preview de link (metadados Open Graph) - função auxiliar */ async function extrairPreviewLinkHelper(url: string) { try { // Validar URL let urlObj: URL; try { urlObj = new URL(url); } catch { return null; } // Buscar HTML da página const response = await fetch(url, { headers: { 'User-Agent': 'Mozilla/5.0 (compatible; SGSE-Bot/1.0)' }, signal: AbortSignal.timeout(5000) // Timeout de 5 segundos }); if (!response.ok) { return null; } const html = await response.text(); // Extrair metadados Open Graph e Twitter Cards const metadata: { titulo?: string; descricao?: string; imagem?: string; site?: string; } = {}; // Título (og:title ou twitter:title ou ) const ogTitleMatch = html.match( /<meta\s+property=["']og:title["']\s+content=["']([^"']+)["']/i ); const twitterTitleMatch = html.match( /<meta\s+name=["']twitter:title["']\s+content=["']([^"']+)["']/i ); const titleMatch = html.match(/<title>([^<]+)<\/title>/i); metadata.titulo = ogTitleMatch?.[1] || twitterTitleMatch?.[1] || titleMatch?.[1] || undefined; if (metadata.titulo) { metadata.titulo = metadata.titulo.trim().substring(0, 200); } // Descrição (og:description ou twitter:description ou meta description) const ogDescMatch = html.match( /<meta\s+property=["']og:description["']\s+content=["']([^"']+)["']/i ); const twitterDescMatch = html.match( /<meta\s+name=["']twitter:description["']\s+content=["']([^"']+)["']/i ); const metaDescMatch = html.match( /<meta\s+name=["']description["']\s+content=["']([^"']+)["']/i ); metadata.descricao = ogDescMatch?.[1] || twitterDescMatch?.[1] || metaDescMatch?.[1] || undefined; if (metadata.descricao) { metadata.descricao = metadata.descricao.trim().substring(0, 300); } // Imagem (og:image ou twitter:image) const ogImageMatch = html.match( /<meta\s+property=["']og:image["']\s+content=["']([^"']+)["']/i ); const twitterImageMatch = html.match( /<meta\s+name=["']twitter:image["']\s+content=["']([^"']+)["']/i ); const imageUrl = ogImageMatch?.[1] || twitterImageMatch?.[1]; if (imageUrl) { // Resolver URL relativa try { metadata.imagem = new URL(imageUrl, url).href; } catch { metadata.imagem = imageUrl; } } // Site (og:site_name ou domínio) const ogSiteMatch = html.match( /<meta\s+property=["']og:site_name["']\s+content=["']([^"']+)["']/i ); metadata.site = ogSiteMatch?.[1] || urlObj.hostname.replace(/^www\./, ''); return { url, titulo: metadata.titulo, descricao: metadata.descricao, imagem: metadata.imagem, site: metadata.site }; } catch (error) { console.error('Erro ao extrair preview de link:', error); return null; } } /** * Processar preview de link e atualizar mensagem */ export const processarPreviewLink = action({ args: { mensagemId: v.id('mensagens'), url: v.string() }, returns: v.null(), handler: async (ctx, args) => { // Extrair preview const preview = await extrairPreviewLinkHelper(args.url); if (preview) { // Atualizar mensagem com preview await ctx.runMutation(internal.chat.atualizarLinkPreview, { mensagemId: args.mensagemId, linkPreview: preview }); } return null; } }); /** * Extrair preview de link (metadados Open Graph) - versão pública */ export const extrairPreviewLink = action({ args: { url: v.string() }, returns: v.union( v.object({ url: v.string(), titulo: v.optional(v.string()), descricao: v.optional(v.string()), imagem: v.optional(v.string()), site: v.optional(v.string()) }), v.null() ), handler: async (ctx, args) => { return await extrairPreviewLinkHelper(args.url); } });