Files
whatsappNucleo/server/api/messages/[instanceId]/[chatId]/index.get.ts
josedario87 80d0042c7e
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m4s
Feature: Agregar botón para crear webhook de debug automáticamente
- 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
2025-12-02 21:21:33 -06:00

292 lines
8.8 KiB
TypeScript

/**
* GET /api/messages/:instanceId/:chatId
* Get messages for a chat with full parsed data from raw_message
*/
import { query } from '../../../../utils/database'
interface MessageRow {
id: string
message_id: string
from_jid: string
from_me: boolean
message_type: string
content: string | null
caption: string | null
media_url: string | null
timestamp: Date
status: string
raw_message: any
participant_jid: string | null
push_name: string | null
quoted_message_id: string | null
}
interface ChatRow {
is_group: boolean
}
// Parse different message types from raw Baileys message
function parseRawMessage(raw: any, messageType: string) {
if (!raw?.message) return {}
const msg = raw.message
const result: any = {}
// Extract media info based on type
if (messageType === 'image' && msg.imageMessage) {
result.media = {
mimetype: msg.imageMessage.mimetype,
filesize: msg.imageMessage.fileLength ? Number(msg.imageMessage.fileLength) : undefined,
width: msg.imageMessage.width,
height: msg.imageMessage.height,
thumbnail: msg.imageMessage.jpegThumbnail
? Buffer.from(msg.imageMessage.jpegThumbnail).toString('base64')
: undefined,
isViewOnce: !!msg.imageMessage.viewOnce
}
result.caption = msg.imageMessage.caption
}
if (messageType === 'video' && msg.videoMessage) {
result.media = {
mimetype: msg.videoMessage.mimetype,
filesize: msg.videoMessage.fileLength ? Number(msg.videoMessage.fileLength) : undefined,
width: msg.videoMessage.width,
height: msg.videoMessage.height,
duration: msg.videoMessage.seconds,
thumbnail: msg.videoMessage.jpegThumbnail
? Buffer.from(msg.videoMessage.jpegThumbnail).toString('base64')
: undefined,
isViewOnce: !!msg.videoMessage.viewOnce
}
result.caption = msg.videoMessage.caption
}
if (messageType === 'audio' && msg.audioMessage) {
result.media = {
mimetype: msg.audioMessage.mimetype,
filesize: msg.audioMessage.fileLength ? Number(msg.audioMessage.fileLength) : undefined,
duration: msg.audioMessage.seconds,
isPtt: !!msg.audioMessage.ptt,
waveform: msg.audioMessage.waveform
? Array.from(msg.audioMessage.waveform)
: undefined
}
}
if (messageType === 'document' && msg.documentMessage) {
result.media = {
mimetype: msg.documentMessage.mimetype,
filename: msg.documentMessage.fileName,
filesize: msg.documentMessage.fileLength ? Number(msg.documentMessage.fileLength) : undefined,
thumbnail: msg.documentMessage.jpegThumbnail
? Buffer.from(msg.documentMessage.jpegThumbnail).toString('base64')
: undefined
}
result.caption = msg.documentMessage.caption
}
if (messageType === 'sticker' && msg.stickerMessage) {
result.media = {
mimetype: msg.stickerMessage.mimetype,
width: msg.stickerMessage.width,
height: msg.stickerMessage.height,
filesize: msg.stickerMessage.fileLength ? Number(msg.stickerMessage.fileLength) : undefined
}
}
if (messageType === 'contact' && msg.contactMessage) {
const vcard = msg.contactMessage.vcard || ''
const phones = extractPhonesFromVCard(vcard)
result.contact = {
displayName: msg.contactMessage.displayName,
vcard: vcard,
phones: phones
}
}
if (messageType === 'location' && msg.locationMessage) {
result.location = {
latitude: msg.locationMessage.degreesLatitude,
longitude: msg.locationMessage.degreesLongitude,
name: msg.locationMessage.name,
address: msg.locationMessage.address,
url: msg.locationMessage.url
}
}
// Extract quoted message if exists
const contextInfo = getContextInfo(msg)
if (contextInfo?.quotedMessage) {
const quotedType = getMessageType(contextInfo.quotedMessage)
result.quoted = {
id: contextInfo.stanzaId,
content: extractTextContent(contextInfo.quotedMessage),
type: quotedType,
fromMe: contextInfo.participant === raw.key?.participant || false,
participant: contextInfo.participant,
participantName: null // Will be filled from contacts if needed
}
// Add media thumbnail for quoted media messages
if (['image', 'video', 'sticker'].includes(quotedType)) {
const quotedMedia = contextInfo.quotedMessage[`${quotedType}Message`]
if (quotedMedia?.jpegThumbnail) {
result.quoted.media = {
thumbnail: Buffer.from(quotedMedia.jpegThumbnail).toString('base64')
}
}
}
}
// Extract participant for group messages
if (raw.key?.participant) {
result.participant = raw.key.participant
}
// Extract pushName
if (raw.pushName) {
result.pushName = raw.pushName
}
return result
}
// Get context info from any message type
function getContextInfo(msg: any): any {
const messageTypes = [
'extendedTextMessage',
'imageMessage',
'videoMessage',
'audioMessage',
'documentMessage',
'stickerMessage',
'contactMessage',
'locationMessage'
]
for (const type of messageTypes) {
if (msg[type]?.contextInfo) {
return msg[type].contextInfo
}
}
return null
}
// Get message type from message content
function getMessageType(msg: any): string {
if (msg.conversation || msg.extendedTextMessage) return 'text'
if (msg.imageMessage) return 'image'
if (msg.videoMessage) return 'video'
if (msg.audioMessage) return 'audio'
if (msg.documentMessage) return 'document'
if (msg.stickerMessage) return 'sticker'
if (msg.contactMessage) return 'contact'
if (msg.locationMessage) return 'location'
return 'unknown'
}
// Extract text content from any message type
function extractTextContent(msg: any): string | null {
if (msg.conversation) return msg.conversation
if (msg.extendedTextMessage?.text) return msg.extendedTextMessage.text
if (msg.imageMessage?.caption) return msg.imageMessage.caption
if (msg.videoMessage?.caption) return msg.videoMessage.caption
if (msg.documentMessage?.caption) return msg.documentMessage.caption
return null
}
// Extract phone numbers from vCard
function extractPhonesFromVCard(vcard: string): string[] {
const phones: string[] = []
const telRegex = /TEL[^:]*:([^\n\r]+)/gi
let match
while ((match = telRegex.exec(vcard)) !== null) {
const phone = match[1].trim().replace(/[^\d+]/g, '')
if (phone) phones.push(phone)
}
return phones
}
export default defineEventHandler(async (event) => {
const username = getHeader(event, 'x-authentik-username')
if (!username) {
throw createError({ statusCode: 401, message: 'Unauthorized' })
}
const instanceId = getRouterParam(event, 'instanceId')
const chatId = getRouterParam(event, 'chatId')
// Get query params for pagination
const queryParams = getQuery(event)
const limit = Math.min(parseInt(queryParams.limit as string) || 50, 100)
const offset = parseInt(queryParams.offset as string) || 0
const before = queryParams.before as string // For infinite scroll
// Verify chat exists and get info
const chatCheck = await query<ChatRow>(
'SELECT id, is_group FROM chats WHERE id = $1 AND instance_id = $2',
[chatId, instanceId]
)
if (chatCheck.rows.length === 0) {
throw createError({ statusCode: 404, message: 'Chat not found' })
}
const isGroup = chatCheck.rows[0].is_group
// Build query with optional before parameter for infinite scroll
let messagesQuery = `
SELECT id, message_id, from_jid, from_me, message_type,
content, caption, media_url, timestamp, status,
raw_message, participant_jid, push_name, quoted_message_id
FROM messages
WHERE chat_id = $1
`
const params: any[] = [chatId]
if (before) {
messagesQuery += ` AND timestamp < $${params.length + 1}`
params.push(before)
}
messagesQuery += ` ORDER BY timestamp DESC LIMIT $${params.length + 1} OFFSET $${params.length + 2}`
params.push(limit, offset)
const result = await query<MessageRow>(messagesQuery, params)
// Mark as read
await query(
'UPDATE chats SET unread_count = 0 WHERE id = $1',
[chatId]
)
// Parse and return messages
return result.rows.map(row => {
const parsedData = parseRawMessage(row.raw_message, row.message_type)
return {
id: row.id,
messageId: row.message_id,
chatId: chatId,
fromJid: row.from_jid,
fromMe: row.from_me,
type: row.message_type || 'unknown',
content: row.content,
caption: row.caption || parsedData.caption,
media: parsedData.media || (row.media_url ? { url: row.media_url } : undefined),
location: parsedData.location,
contact: parsedData.contact,
quoted: parsedData.quoted,
timestamp: row.timestamp,
status: row.status || 'sent',
participant: row.participant_jid || parsedData.participant,
pushName: row.push_name || parsedData.pushName,
isGroup: isGroup
}
})
})