/** * 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( '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(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 } }) })