import { v } from 'convex/values'; import { api, internal } from './_generated/api'; import { internalMutation, internalQuery, mutation } from './_generated/server'; import { getCurrentUserFunction } from './auth'; /** * 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 usuario = await getCurrentUserFunction(ctx); if (!usuario) { return { sucesso: false, erro: 'Usuário não autenticado' }; } // 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; } });