"use node"; import { action } from "../_generated/server"; import { v } from "convex/values"; import { internal } from "../_generated/api"; /** * 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); }, });