190 lines
6.4 KiB
TypeScript
190 lines
6.4 KiB
TypeScript
import { defineTable } from 'convex/server';
|
|
import { v } from 'convex/values';
|
|
|
|
export const chatTables = {
|
|
// Sistema de Chat
|
|
conversas: defineTable({
|
|
tipo: v.union(v.literal('individual'), v.literal('grupo'), v.literal('sala_reuniao')),
|
|
nome: v.optional(v.string()), // nome do grupo/sala
|
|
|
|
participantes: v.array(v.id('usuarios')), // IDs dos participantes
|
|
administradores: v.optional(v.array(v.id('usuarios'))), // IDs dos administradores (apenas para sala_reuniao)
|
|
ultimaMensagem: v.optional(v.string()),
|
|
ultimaMensagemTimestamp: v.optional(v.number()),
|
|
ultimaMensagemRemetenteId: v.optional(v.id('usuarios')), // ID do remetente da última mensagem
|
|
criadoPor: v.id('usuarios'),
|
|
criadoEm: v.number()
|
|
})
|
|
.index('by_criado_por', ['criadoPor'])
|
|
.index('by_tipo', ['tipo'])
|
|
.index('by_ultima_mensagem', ['ultimaMensagemTimestamp']),
|
|
|
|
mensagens: defineTable({
|
|
conversaId: v.id('conversas'),
|
|
remetenteId: v.id('usuarios'),
|
|
tipo: v.union(v.literal('texto'), v.literal('arquivo'), v.literal('imagem')),
|
|
conteudo: v.string(), // texto ou nome do arquivo (pode ser criptografado)
|
|
conteudoBusca: v.optional(v.string()), // versão normalizada para busca (não criptografada se mensagem for E2E)
|
|
arquivoId: v.optional(v.id('_storage')),
|
|
arquivoNome: v.optional(v.string()),
|
|
arquivoTamanho: v.optional(v.number()),
|
|
arquivoTipo: v.optional(v.string()),
|
|
// Campos para criptografia E2E
|
|
criptografado: v.optional(v.boolean()), // Indica se a mensagem está criptografada
|
|
iv: v.optional(v.string()), // Initialization Vector (base64) para descriptografia
|
|
keyId: v.optional(v.string()), // Identificador da chave usada para criptografar
|
|
linkPreview: v.optional(
|
|
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())
|
|
})
|
|
),
|
|
reagiuPor: v.optional(
|
|
v.array(
|
|
v.object({
|
|
usuarioId: v.id('usuarios'),
|
|
emoji: v.string()
|
|
})
|
|
)
|
|
),
|
|
mencoes: v.optional(v.array(v.id('usuarios'))),
|
|
respostaPara: v.optional(v.id('mensagens')), // ID da mensagem que está respondendo
|
|
agendadaPara: v.optional(v.number()), // timestamp
|
|
enviadaEm: v.number(),
|
|
editadaEm: v.optional(v.number()),
|
|
deletada: v.optional(v.boolean()),
|
|
lidaPor: v.optional(v.array(v.id('usuarios'))) // IDs dos usuários que leram a mensagem
|
|
})
|
|
.index('by_conversa', ['conversaId', 'enviadaEm'])
|
|
.index('by_remetente', ['remetenteId'])
|
|
.index('by_agendamento', ['agendadaPara'])
|
|
.index('by_resposta', ['respostaPara']),
|
|
|
|
leituras: defineTable({
|
|
conversaId: v.id('conversas'),
|
|
usuarioId: v.id('usuarios'),
|
|
ultimaMensagemLida: v.id('mensagens'),
|
|
lidaEm: v.number()
|
|
})
|
|
.index('by_conversa_usuario', ['conversaId', 'usuarioId'])
|
|
.index('by_usuario', ['usuarioId']),
|
|
|
|
// Sistema de Chamadas de Áudio/Vídeo
|
|
chamadas: defineTable({
|
|
conversaId: v.id('conversas'),
|
|
tipo: v.union(v.literal('audio'), v.literal('video')),
|
|
roomName: v.string(), // Nome único da sala Jitsi
|
|
criadoPor: v.id('usuarios'), // Anfitrião/criador
|
|
participantes: v.array(v.id('usuarios')),
|
|
status: v.union(
|
|
v.literal('aguardando'),
|
|
v.literal('em_andamento'),
|
|
v.literal('finalizada'),
|
|
v.literal('cancelada')
|
|
),
|
|
iniciadaEm: v.optional(v.number()),
|
|
finalizadaEm: v.optional(v.number()),
|
|
duracaoSegundos: v.optional(v.number()),
|
|
gravando: v.boolean(),
|
|
gravacaoIniciadaPor: v.optional(v.id('usuarios')),
|
|
gravacaoIniciadaEm: v.optional(v.number()),
|
|
gravacaoFinalizadaEm: v.optional(v.number()),
|
|
configuracoes: v.optional(
|
|
v.object({
|
|
audioHabilitado: v.boolean(),
|
|
videoHabilitado: v.boolean(),
|
|
participantesConfig: v.optional(
|
|
v.array(
|
|
v.object({
|
|
usuarioId: v.id('usuarios'),
|
|
audioHabilitado: v.boolean(),
|
|
videoHabilitado: v.boolean(),
|
|
forcadoPeloAnfitriao: v.optional(v.boolean()) // Se foi forçado pelo anfitrião
|
|
})
|
|
)
|
|
)
|
|
})
|
|
),
|
|
criadoEm: v.number()
|
|
})
|
|
.index('by_conversa', ['conversaId', 'status'])
|
|
.index('by_criado_por', ['criadoPor'])
|
|
.index('by_status', ['status'])
|
|
.index('by_room_name', ['roomName']),
|
|
|
|
notificacoes: defineTable({
|
|
usuarioId: v.id('usuarios'),
|
|
tipo: v.union(
|
|
v.literal('nova_mensagem'),
|
|
v.literal('mencao'),
|
|
v.literal('grupo_criado'),
|
|
v.literal('adicionado_grupo'),
|
|
v.literal('alerta_seguranca'),
|
|
v.literal('etapa_fluxo_concluida')
|
|
),
|
|
conversaId: v.optional(v.id('conversas')),
|
|
mensagemId: v.optional(v.id('mensagens')),
|
|
remetenteId: v.optional(v.id('usuarios')),
|
|
titulo: v.string(),
|
|
descricao: v.string(),
|
|
lida: v.boolean(),
|
|
criadaEm: v.number()
|
|
})
|
|
.index('by_usuario', ['usuarioId', 'lida', 'criadaEm'])
|
|
.index('by_usuario_lida', ['usuarioId', 'lida']),
|
|
|
|
digitando: defineTable({
|
|
conversaId: v.id('conversas'),
|
|
usuarioId: v.id('usuarios'),
|
|
iniciouEm: v.number()
|
|
})
|
|
.index('by_conversa', ['conversaId', 'iniciouEm'])
|
|
.index('by_usuario', ['usuarioId']),
|
|
|
|
// Push Notifications
|
|
pushSubscriptions: defineTable({
|
|
usuarioId: v.id('usuarios'),
|
|
endpoint: v.string(), // URL do serviço de push
|
|
keys: v.object({
|
|
p256dh: v.string(), // Chave pública
|
|
auth: v.string() // Chave de autenticação
|
|
}),
|
|
userAgent: v.optional(v.string()),
|
|
criadoEm: v.number(),
|
|
ultimaAtividade: v.number(),
|
|
ativo: v.boolean()
|
|
})
|
|
.index('by_usuario', ['usuarioId', 'ativo'])
|
|
.index('by_endpoint', ['endpoint']),
|
|
|
|
// Preferências de Notificação por Conversa
|
|
preferenciasNotificacaoConversa: defineTable({
|
|
usuarioId: v.id('usuarios'),
|
|
conversaId: v.id('conversas'),
|
|
pushAtivado: v.boolean(), // Receber push notifications
|
|
emailAtivado: v.boolean(), // Receber emails quando offline
|
|
somAtivado: v.boolean(), // Tocar som
|
|
silenciado: v.boolean(), // Silenciar completamente
|
|
apenasMencoes: v.boolean(), // Notificar apenas quando mencionado
|
|
criadoEm: v.number(),
|
|
atualizadoEm: v.number()
|
|
})
|
|
.index('by_usuario_conversa', ['usuarioId', 'conversaId'])
|
|
.index('by_conversa', ['conversaId']),
|
|
|
|
// Chaves de Criptografia E2E por Conversa
|
|
chavesCriptografia: defineTable({
|
|
conversaId: v.id('conversas'),
|
|
chaveCompartilhada: v.string(), // Chave criptografada compartilhada (base64)
|
|
keyId: v.string(), // Identificador único da chave
|
|
criadoPor: v.id('usuarios'), // Usuário que criou/compartilhou a chave
|
|
criadoEm: v.number(),
|
|
ativo: v.boolean() // Se a chave está ativa (permite rotação de chaves)
|
|
})
|
|
.index('by_conversa', ['conversaId', 'ativo'])
|
|
.index('by_key_id', ['keyId'])
|
|
};
|