Files
whatsappNucleo/server/api/messages/[instanceId]/[chatId]/index.get.ts
josedario87 b0101c791a
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m8s
Fix: Agregar soporte para recepción de Polls y Events
- manager.ts: Detectar pollCreationMessage y eventMessage en getMessageType
- manager.ts: Extraer y guardar datos de poll/event como JSON en content
- index.get.ts: Parsear poll/event desde raw_message y content
- index.get.ts: Incluir poll y event en respuesta de la API
2025-12-04 12:14:45 -06:00

342 lines
10 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
}
}
// Poll message
if (messageType === 'poll') {
const pollMsg = msg.pollCreationMessage || msg.pollCreationMessageV3
if (pollMsg) {
result.poll = {
name: pollMsg.name,
options: pollMsg.options?.map((o: any) => o.optionName) || [],
selectableCount: pollMsg.selectableOptionsCount || 1
}
}
}
// Event message
if (messageType === 'event' && msg.eventMessage) {
result.event = {
name: msg.eventMessage.name,
startDate: msg.eventMessage.startTime
? new Date(msg.eventMessage.startTime * 1000).toISOString()
: null,
endDate: msg.eventMessage.endTime
? new Date(msg.eventMessage.endTime * 1000).toISOString()
: null,
description: msg.eventMessage.description,
location: msg.eventMessage.location ? {
name: msg.eventMessage.location.name,
address: msg.eventMessage.location.address,
latitude: msg.eventMessage.location.degreesLatitude,
longitude: msg.eventMessage.location.degreesLongitude
} : undefined
}
}
// 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 || msg.contactsArrayMessage) return 'contact'
if (msg.locationMessage) return 'location'
if (msg.pollCreationMessage || msg.pollCreationMessageV3) return 'poll'
if (msg.eventMessage) return 'event'
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)
// For poll/event, try to parse content as JSON if it's stored that way
let poll = parsedData.poll
let event = parsedData.event
if (row.message_type === 'poll' && row.content && !poll) {
try {
poll = JSON.parse(row.content)
} catch {}
}
if (row.message_type === 'event' && row.content && !event) {
try {
event = JSON.parse(row.content)
} catch {}
}
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,
poll: poll,
event: event,
quoted: parsedData.quoted,
timestamp: row.timestamp,
status: row.status || 'sent',
participant: row.participant_jid || parsedData.participant,
pushName: row.push_name || parsedData.pushName,
isGroup: isGroup
}
})
})