Files
sgse-app/packages/backend/convex/pushNotifications.ts
deyvisonwanderley aa3e3470cd feat: enhance push notification management and error handling
- Implemented error handling for unhandled promise rejections related to message channels, improving stability during push notification operations.
- Updated the PushNotificationManager component to manage push subscription registration with timeouts, preventing application hangs.
- Enhanced the sidebar and chat components to display user avatars, improving user experience and visual consistency.
- Refactored email processing logic to support scheduled email sending, integrating new backend functionalities for better email management.
- Improved overall error handling and logging across components to reduce console spam and enhance debugging capabilities.
2025-11-05 06:14:52 -03:00

279 lines
7.5 KiB
TypeScript

import { v } from "convex/values";
import { mutation, query, internalMutation, internalQuery } from "./_generated/server";
import { Id } from "./_generated/dataModel";
import { internal, api } from "./_generated/api";
/**
* Registrar subscription de push notification
*/
export const registrarPushSubscription = mutation({
args: {
endpoint: v.string(),
keys: v.object({
p256dh: v.string(),
auth: v.string(),
}),
userAgent: v.optional(v.string()),
},
returns: v.object({ sucesso: v.boolean(), erro: v.optional(v.string()) }),
handler: async (ctx, args) => {
// Obter usuário autenticado
const identity = await ctx.auth.getUserIdentity();
if (!identity?.email) {
return { sucesso: false, erro: "Usuário não autenticado" };
}
const usuario = await ctx.db
.query("usuarios")
.withIndex("by_email", (q) => q.eq("email", identity.email!))
.first();
if (!usuario) {
return { sucesso: false, erro: "Usuário não encontrado" };
}
// Verificar se já existe subscription com este endpoint
const existente = await ctx.db
.query("pushSubscriptions")
.withIndex("by_endpoint", (q) => q.eq("endpoint", args.endpoint))
.first();
if (existente) {
// Atualizar subscription existente
await ctx.db.patch(existente._id, {
usuarioId: usuario._id,
keys: args.keys,
userAgent: args.userAgent,
ultimaAtividade: Date.now(),
ativo: true,
});
} else {
// Criar nova subscription
await ctx.db.insert("pushSubscriptions", {
usuarioId: usuario._id,
endpoint: args.endpoint,
keys: args.keys,
userAgent: args.userAgent,
criadoEm: Date.now(),
ultimaAtividade: Date.now(),
ativo: true,
});
}
return { sucesso: true };
},
});
/**
* Remover subscription de push notification
*/
export const removerPushSubscription = mutation({
args: {
endpoint: v.string(),
},
returns: v.object({ sucesso: v.boolean() }),
handler: async (ctx, args) => {
const subscription = await ctx.db
.query("pushSubscriptions")
.withIndex("by_endpoint", (q) => q.eq("endpoint", args.endpoint))
.first();
if (subscription) {
await ctx.db.patch(subscription._id, { ativo: false });
}
return { sucesso: true };
},
});
/**
* Obter subscriptions ativas de um usuário
*/
export const obterPushSubscriptions = internalQuery({
args: {
usuarioId: v.id("usuarios"),
},
returns: v.array(
v.object({
_id: v.id("pushSubscriptions"),
endpoint: v.string(),
keys: v.object({
p256dh: v.string(),
auth: v.string(),
}),
})
),
handler: async (ctx, args) => {
const subscriptions = await ctx.db
.query("pushSubscriptions")
.withIndex("by_usuario", (q) => q.eq("usuarioId", args.usuarioId).eq("ativo", true))
.collect();
return subscriptions.map((sub) => ({
_id: sub._id,
endpoint: sub.endpoint,
keys: sub.keys,
}));
},
});
/**
* Enviar push notification para um usuário
* Esta função será chamada quando uma nova mensagem chegar
*/
export const enviarPushNotification = internalMutation({
args: {
usuarioId: v.id("usuarios"),
titulo: v.string(),
corpo: v.string(),
data: v.optional(
v.object({
conversaId: v.optional(v.id("conversas")),
mensagemId: v.optional(v.id("mensagens")),
tipo: v.optional(v.string()),
})
),
},
returns: v.object({ enviados: v.number(), falhas: v.number() }),
handler: async (ctx, args) => {
// Buscar subscriptions ativas do usuário
const subscriptions = await ctx.runQuery(internal.pushNotifications.obterPushSubscriptions, {
usuarioId: args.usuarioId,
});
if (subscriptions.length === 0) {
return { enviados: 0, falhas: 0 };
}
// Verificar preferências do usuário
const usuario = await ctx.db.get(args.usuarioId);
if (!usuario || usuario.notificacoesAtivadas === false) {
return { enviados: 0, falhas: 0 };
}
// Se há conversaId, verificar preferências específicas da conversa
const conversaId = args.data?.conversaId;
if (conversaId) {
const preferencias = await ctx.db
.query("preferenciasNotificacaoConversa")
.withIndex("by_usuario_conversa", (q) =>
q.eq("usuarioId", args.usuarioId).eq("conversaId", conversaId)
)
.first();
if (preferencias) {
// Se silenciado ou push desativado, não enviar
if (preferencias.silenciado || !preferencias.pushAtivado) {
return { enviados: 0, falhas: 0 };
}
// Se apenas menções e não é menção, não enviar
if (preferencias.apenasMencoes && args.data?.tipo !== "mencao") {
return { enviados: 0, falhas: 0 };
}
}
}
// Agendar envio de push via action (que roda em Node.js)
let enviados = 0;
let falhas = 0;
// Converter IDs para strings ao passar para a action
// A action espera strings, mas recebemos Ids do Convex
const dataParaAction = args.data
? {
conversaId: args.data.conversaId ? String(args.data.conversaId) : undefined,
mensagemId: args.data.mensagemId ? String(args.data.mensagemId) : undefined,
tipo: args.data.tipo,
}
: undefined;
for (const subscription of subscriptions) {
try {
await ctx.scheduler.runAfter(0, api.actions.pushNotifications.enviarPush, {
subscriptionId: subscription._id,
titulo: args.titulo,
corpo: args.corpo,
data: dataParaAction,
});
enviados++;
} catch (error: unknown) {
const errorMessage = error instanceof Error ? error.message : String(error);
console.error(`Erro ao agendar push para subscription ${subscription._id}:`, errorMessage);
falhas++;
}
}
return { enviados, falhas };
},
});
/**
* Obter subscription por ID (para actions)
*/
export const getSubscriptionById = internalQuery({
args: {
subscriptionId: v.id("pushSubscriptions"),
},
returns: v.union(
v.object({
_id: v.id("pushSubscriptions"),
endpoint: v.string(),
keys: v.object({
p256dh: v.string(),
auth: v.string(),
}),
ativo: v.boolean(),
}),
v.null()
),
handler: async (ctx, args) => {
const subscription = await ctx.db.get(args.subscriptionId);
if (!subscription) {
return null;
}
return {
_id: subscription._id,
endpoint: subscription.endpoint,
keys: subscription.keys,
ativo: subscription.ativo,
};
},
});
/**
* Marcar subscription como inativa
*/
export const marcarSubscriptionInativa = internalMutation({
args: {
subscriptionId: v.id("pushSubscriptions"),
},
returns: v.null(),
handler: async (ctx, args) => {
await ctx.db.patch(args.subscriptionId, { ativo: false });
return null;
},
});
/**
* Verificar se usuário está online (última atividade recente)
*/
export const verificarUsuarioOnline = internalQuery({
args: {
usuarioId: v.id("usuarios"),
},
returns: v.boolean(),
handler: async (ctx, args) => {
const usuario = await ctx.db.get(args.usuarioId);
if (!usuario || !usuario.ultimaAtividade) {
return false;
}
// Considerar online se última atividade foi há menos de 5 minutos
const cincoMinutosAtras = Date.now() - 5 * 60 * 1000;
return usuario.ultimaAtividade >= cincoMinutosAtras;
},
});