All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m8s
- 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
342 lines
10 KiB
TypeScript
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
|
|
}
|
|
})
|
|
})
|