Files
sgse-app/packages/backend/convex/actions/linkPreview.ts

156 lines
3.8 KiB
TypeScript

'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 <title>)
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);
}
});