Feature: Agregar botón para crear webhook de debug automáticamente
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m4s

- Agregar botón "Crear Webhook de Debug" en WebhookReceiverSection
- Detectar si ya existe un webhook apuntando al receptor de debug
- Permitir eliminar el webhook de debug
- Incluir todos los eventos disponibles al crear el webhook
- También incluye mejoras previas de manejo de media y mensajes
This commit is contained in:
2025-12-02 21:21:33 -06:00
parent 71593b25e9
commit 80d0042c7e
21 changed files with 3722 additions and 112 deletions

373
app/types/message.ts Normal file
View File

@@ -0,0 +1,373 @@
/**
* Tipos centralizados para mensajes de WhatsApp
* Basado en la API de Baileys (@whiskeysockets/baileys)
*/
// Tipos de mensaje soportados por Baileys
export type MessageType =
| 'text'
| 'image'
| 'video'
| 'audio'
| 'document'
| 'sticker'
| 'contact'
| 'location'
| 'reaction'
| 'poll'
| 'unknown'
// Estados de mensaje
export type MessageStatus = 'pending' | 'sent' | 'delivered' | 'read' | 'failed'
// Estados de presencia
export type PresenceStatus = 'available' | 'unavailable' | 'composing' | 'recording' | 'paused'
/**
* Información de media (imagen, video, audio, documento, sticker)
*/
export interface MediaInfo {
/** URL del media (puede ser endpoint de API o URL directa) */
url?: string
/** Tipo MIME del archivo */
mimetype?: string
/** Nombre del archivo */
filename?: string
/** Tamaño en bytes */
filesize?: number
/** Ancho en píxeles (para imagen/video/sticker) */
width?: number
/** Alto en píxeles (para imagen/video/sticker) */
height?: number
/** Duración en segundos (para audio/video) */
duration?: number
/** Thumbnail en base64 */
thumbnail?: string
/** Si es mensaje de vista única */
isViewOnce?: boolean
/** Si es audio de voz (PTT) */
isPtt?: boolean
/** Waveform del audio (array de bytes) */
waveform?: number[]
}
/**
* Información de ubicación
*/
export interface LocationInfo {
/** Latitud */
latitude: number
/** Longitud */
longitude: number
/** Nombre del lugar */
name?: string
/** Dirección */
address?: string
/** URL del mapa */
url?: string
}
/**
* Información de contacto compartido
*/
export interface ContactInfo {
/** Nombre para mostrar */
displayName: string
/** vCard completo */
vcard: string
/** Números de teléfono extraídos */
phones?: string[]
}
/**
* Información de mensaje citado (quoted/reply)
*/
export interface QuotedMessage {
/** ID del mensaje original */
id: string
/** Contenido del mensaje original */
content: string | null
/** Tipo del mensaje original */
type: MessageType
/** Si fue enviado por mí */
fromMe: boolean
/** JID del autor original (en grupos) */
participant?: string
/** Nombre del autor original */
participantName?: string
/** Media info si era un mensaje con media */
media?: MediaInfo
}
/**
* Información de reacción a un mensaje
*/
export interface ReactionInfo {
/** Emoji de la reacción */
emoji: string
/** JID de quien reaccionó */
reactorJid: string
/** Nombre de quien reaccionó */
reactorName?: string
/** Timestamp de la reacción */
timestamp?: Date
}
/**
* Mensaje completo con toda la información
*/
export interface Message {
/** UUID interno de la BD */
id: string
/** ID del mensaje de WhatsApp */
messageId: string
/** ID del chat */
chatId?: string
/** JID del remitente */
fromJid: string
/** Si fue enviado por mí */
fromMe: boolean
/** Tipo de mensaje */
type: MessageType
/** Contenido de texto (para text/caption) */
content: string | null
/** Caption para media */
caption?: string
/** Información de media */
media?: MediaInfo
/** Información de ubicación */
location?: LocationInfo
/** Información de contacto */
contact?: ContactInfo
/** Mensaje citado */
quoted?: QuotedMessage
/** Reacciones al mensaje */
reactions?: ReactionInfo[]
/** Timestamp del mensaje */
timestamp: Date
/** Estado del mensaje */
status: MessageStatus
/** JID del participante en grupos */
participant?: string
/** Nombre del remitente (pushName) */
pushName?: string
/** Si es un mensaje del sistema (stub) */
isStub?: boolean
/** Tipo de stub (si aplica) */
stubType?: string
/** Parámetros del stub */
stubParams?: string[]
}
/**
* Información de presencia de un usuario
*/
export interface PresenceInfo {
/** Estado actual */
status: PresenceStatus
/** Última vez visto */
lastSeen?: Date
}
/**
* Participante de grupo
*/
export interface GroupParticipant {
/** JID del participante */
jid: string
/** Nombre del participante */
name?: string
/** Si es administrador */
isAdmin: boolean
/** Si es super administrador (creador) */
isSuperAdmin: boolean
}
/**
* Chat/Conversación
*/
export interface Chat {
/** UUID interno */
id: string
/** JID del chat */
jid: string
/** Nombre del chat */
name: string
/** Si es grupo */
isGroup: boolean
/** URL de foto de perfil */
profilePicture?: string
/** Último mensaje (texto o placeholder) */
lastMessage: string
/** Tipo del último mensaje */
lastMessageType?: MessageType
/** Timestamp del último mensaje */
lastMessageAt: Date
/** Contador de mensajes no leídos */
unreadCount: number
/** Presencia actual (si está suscrito) */
presence?: PresenceInfo
/** Participantes (solo para grupos) */
participants?: GroupParticipant[]
/** Si está archivado */
isArchived?: boolean
/** Si está fijado */
isPinned?: boolean
/** Si está silenciado */
isMuted?: boolean
}
/**
* Contenido para enviar un mensaje
*/
export interface SendMessageContent {
/** Texto del mensaje */
text?: string
/** Archivo de imagen */
image?: File | string
/** Archivo de video */
video?: File | string
/** Archivo de audio */
audio?: File | string
/** Archivo de documento */
document?: File | string
/** Caption para media */
caption?: string
/** ID del mensaje a citar */
quotedMessageId?: string
/** JIDs para mencionar */
mentions?: string[]
/** Si es audio PTT (nota de voz) */
isPtt?: boolean
}
/**
* Evento de actualización de presencia
*/
export interface PresenceUpdateEvent {
/** ID de la instancia */
instanceId: string
/** JID del chat */
jid: string
/** Presencias por participante */
presences: Record<string, PresenceInfo>
}
/**
* Evento de actualización de estado de mensaje
*/
export interface MessageStatusEvent {
/** ID de la instancia */
instanceId: string
/** ID del mensaje */
messageId: string
/** Nuevo estado */
status: MessageStatus
}
/**
* Evento de nuevo mensaje
*/
export interface MessageReceivedEvent {
/** ID de la instancia */
instanceId: string
/** Mensaje completo */
message: Message
}
/**
* Evento de reacción a mensaje
*/
export interface MessageReactionEvent {
/** ID de la instancia */
instanceId: string
/** ID del mensaje */
messageId: string
/** Información de la reacción */
reaction: ReactionInfo
}
// Utilidades
/**
* Obtiene el placeholder de texto para un tipo de mensaje
*/
export function getMessageTypePlaceholder(type: MessageType): string {
const placeholders: Record<MessageType, string> = {
text: '',
image: 'Foto',
video: 'Video',
audio: 'Audio',
document: 'Documento',
sticker: 'Sticker',
contact: 'Contacto',
location: 'Ubicación',
reaction: 'Reacción',
poll: 'Encuesta',
unknown: 'Mensaje'
}
return placeholders[type] || 'Mensaje'
}
/**
* Obtiene el ícono para un tipo de mensaje
*/
export function getMessageTypeIcon(type: MessageType): string {
const icons: Record<MessageType, string> = {
text: 'i-lucide-message-square',
image: 'i-lucide-image',
video: 'i-lucide-video',
audio: 'i-lucide-music',
document: 'i-lucide-file',
sticker: 'i-lucide-smile',
contact: 'i-lucide-user',
location: 'i-lucide-map-pin',
reaction: 'i-lucide-heart',
poll: 'i-lucide-bar-chart',
unknown: 'i-lucide-help-circle'
}
return icons[type] || 'i-lucide-message-square'
}
/**
* Genera un color consistente basado en un string (para nombres en grupos)
*/
export function stringToColor(str: string): string {
const colors = [
'#e17076', // rojo
'#faa774', // naranja
'#a695e7', // morado
'#7bc862', // verde
'#6ec9cb', // cyan
'#65aadd', // azul
'#ee7aae', // rosa
'#e5a36b', // marrón
]
let hash = 0
for (let i = 0; i < str.length; i++) {
hash = str.charCodeAt(i) + ((hash << 5) - hash)
}
return colors[Math.abs(hash) % colors.length]
}
/**
* Formatea bytes a tamaño legible
*/
export function formatFileSize(bytes: number): string {
if (bytes === 0) return '0 B'
const k = 1024
const sizes = ['B', 'KB', 'MB', 'GB']
const i = Math.floor(Math.log(bytes) / Math.log(k))
return `${parseFloat((bytes / Math.pow(k, i)).toFixed(1))} ${sizes[i]}`
}
/**
* Formatea duración en segundos a mm:ss
*/
export function formatDuration(seconds: number): string {
const mins = Math.floor(seconds / 60)
const secs = Math.floor(seconds % 60)
return `${mins}:${secs.toString().padStart(2, '0')}`
}