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; }, });