feat: Add 'atas' (minutes/records) management feature, and implement various improvements across UI, backend logic, and authentication.

This commit is contained in:
2025-12-02 16:37:48 -03:00
parent 05e7f1181d
commit 4bd9e21748
265 changed files with 29156 additions and 26460 deletions

View File

@@ -1,138 +1,155 @@
"use node";
'use node';
import { action } from "../_generated/server";
import { v } from "convex/values";
import { internal } from "../_generated/api";
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;
}
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
});
// 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;
}
if (!response.ok) {
return null;
}
const html = await response.text();
const html = await response.text();
// Extrair metadados Open Graph e Twitter Cards
const metadata: {
titulo?: string;
descricao?: string;
imagem?: string;
site?: string;
} = {};
// 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);
}
// 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);
// 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);
}
metadata.titulo = ogTitleMatch?.[1] || twitterTitleMatch?.[1] || titleMatch?.[1] || undefined;
if (metadata.titulo) {
metadata.titulo = metadata.titulo.trim().substring(0, 200);
}
// 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;
}
}
// 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
);
// 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\./, "");
metadata.descricao =
ogDescMatch?.[1] || twitterDescMatch?.[1] || metaDescMatch?.[1] || undefined;
if (metadata.descricao) {
metadata.descricao = metadata.descricao.trim().substring(0, 300);
}
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;
}
// 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;
},
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);
},
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);
}
});