Feat: Agregar soporte para envío de Contacts, Polls y Events
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m8s

- Backend: Nuevo soporte en endpoint /send para tipos contact, poll, event
- UI: Modales para crear y enviar contactos, encuestas y eventos
- Visualización: Componentes MessagePoll y MessageEvent para mostrar mensajes recibidos
- Tipos: Agregar PollInfo, EventInfo y tipo 'event' a MessageType
This commit is contained in:
2025-12-04 12:06:35 -06:00
parent 48f23c512b
commit cb846d0c56
10 changed files with 1443 additions and 46 deletions

View File

@@ -33,6 +33,43 @@ interface TextMessageBody {
quotedMessageId?: string
}
interface ContactInfo {
displayName: string
phoneNumber: string
organization?: string
}
interface ContactMessageBody {
type: 'contact'
contacts: ContactInfo[]
quotedMessageId?: string
}
interface PollMessageBody {
type: 'poll'
name: string
options: string[]
selectableCount?: number
quotedMessageId?: string
}
interface EventMessageBody {
type: 'event'
name: string
startDate: string // ISO date string
endDate?: string
description?: string
location?: {
name?: string
address?: string
latitude?: number
longitude?: number
}
quotedMessageId?: string
}
type JsonMessageBody = TextMessageBody | ContactMessageBody | PollMessageBody | EventMessageBody
export default defineEventHandler(async (event) => {
const username = getHeader(event, 'x-authentik-username')
if (!username) {
@@ -71,80 +108,107 @@ export default defineEventHandler(async (event) => {
// ==================== MEDIA / STICKER ====================
return await handleMediaMessage(event, instanceId, chatId, jid, socket)
} else {
// ==================== TEXT ====================
return await handleTextMessage(event, instanceId, chatId, jid)
// ==================== JSON MESSAGES ====================
return await handleJsonMessage(event, instanceId, chatId, jid, socket)
}
})
/**
* Handle text message sending
* Handle JSON message sending (text, contacts, polls, events)
*/
async function handleTextMessage(
async function handleJsonMessage(
event: any,
instanceId: string,
chatId: string,
jid: string
jid: string,
socket: any
) {
const body = await readBody<TextMessageBody>(event)
const messageText = body.content || body.message
const body = await readBody<JsonMessageBody>(event)
if (!messageText?.trim()) {
throw createError({ statusCode: 400, message: 'Message content is required' })
}
// Determine message type
const messageType = (body as any).type || 'text'
// Get quoted message if provided
let quotedMessage = null
if (body.quotedMessageId) {
const quotedMessageId = (body as any).quotedMessageId
if (quotedMessageId) {
const quotedResult = await query(
'SELECT raw_message FROM messages WHERE message_id = $1 AND instance_id = $2',
[body.quotedMessageId, instanceId]
[quotedMessageId, instanceId]
)
if (quotedResult.rows.length > 0) {
quotedMessage = quotedResult.rows[0].raw_message
}
}
const options: any = {}
if (quotedMessage) {
options.quoted = quotedMessage
}
try {
const content = { text: messageText }
const options: any = {}
if (quotedMessage) {
options.quoted = quotedMessage
}
let content: any
let dbMessageType: string
let dbContent: string
const result = await baileysManager.sendMessage(instanceId, jid, content, options)
switch (messageType) {
case 'contact':
return await handleContactMessage(body as ContactMessageBody, instanceId, chatId, jid, socket, options)
// Save to database
await query(
`INSERT INTO messages (
instance_id, chat_id, message_id, from_jid, from_me,
message_type, content, timestamp, status, raw_message, quoted_message_id
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8, $9, $10)
ON CONFLICT (instance_id, message_id) DO NOTHING`,
[
instanceId,
chatId,
result.key.id,
'me',
true,
'text',
messageText,
'sent',
JSON.stringify(result),
body.quotedMessageId || null
]
)
case 'poll':
return await handlePollMessage(body as PollMessageBody, instanceId, chatId, jid, socket, options)
await query(
`UPDATE chats SET last_message_at = NOW(), last_message_type = 'text' WHERE id = $1`,
[chatId]
)
case 'event':
return await handleEventMessage(body as EventMessageBody, instanceId, chatId, jid, socket, options)
return {
success: true,
messages: [{ messageId: result.key.id, type: 'text' }]
default:
// Text message
const textBody = body as TextMessageBody
const messageText = textBody.content || textBody.message
if (!messageText?.trim()) {
throw createError({ statusCode: 400, message: 'Message content is required' })
}
content = { text: messageText }
dbMessageType = 'text'
dbContent = messageText
const result = await socket.sendMessage(jid, content, options)
// Save to database
await query(
`INSERT INTO messages (
instance_id, chat_id, message_id, from_jid, from_me,
message_type, content, timestamp, status, raw_message, quoted_message_id
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8, $9, $10)
ON CONFLICT (instance_id, message_id) DO NOTHING`,
[
instanceId,
chatId,
result.key.id,
'me',
true,
dbMessageType,
dbContent,
'sent',
JSON.stringify(result),
quotedMessageId || null
]
)
await query(
`UPDATE chats SET last_message_at = NOW(), last_message_type = $1 WHERE id = $2`,
[dbMessageType, chatId]
)
return {
success: true,
messages: [{ messageId: result.key.id, type: dbMessageType }]
}
}
} catch (error) {
console.error('[Send] Error sending text:', error)
console.error('[Send] Error sending message:', error)
throw createError({
statusCode: 500,
message: `Failed to send message: ${(error as Error).message}`
@@ -152,6 +216,231 @@ async function handleTextMessage(
}
}
/**
* Handle contact message sending
*/
async function handleContactMessage(
body: ContactMessageBody,
instanceId: string,
chatId: string,
jid: string,
socket: any,
options: any
) {
if (!body.contacts || body.contacts.length === 0) {
throw createError({ statusCode: 400, message: 'At least one contact is required' })
}
// Build vCards for each contact
const vcards = body.contacts.map(contact => {
const lines = [
'BEGIN:VCARD',
'VERSION:3.0',
`FN:${contact.displayName}`,
`TEL;type=CELL;waid=${contact.phoneNumber.replace(/\D/g, '')}:${contact.phoneNumber}`
]
if (contact.organization) {
lines.push(`ORG:${contact.organization}`)
}
lines.push('END:VCARD')
return lines.join('\n')
})
const content = {
contacts: {
displayName: body.contacts.length === 1
? body.contacts[0].displayName
: `${body.contacts.length} contactos`,
contacts: vcards.map(vcard => ({ vcard }))
}
}
const result = await socket.sendMessage(jid, content, options)
// Save to database
await query(
`INSERT INTO messages (
instance_id, chat_id, message_id, from_jid, from_me,
message_type, content, timestamp, status, raw_message, quoted_message_id
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8, $9, $10)
ON CONFLICT (instance_id, message_id) DO NOTHING`,
[
instanceId,
chatId,
result.key.id,
'me',
true,
'contact',
JSON.stringify(body.contacts),
'sent',
JSON.stringify(result),
body.quotedMessageId || null
]
)
await query(
`UPDATE chats SET last_message_at = NOW(), last_message_type = 'contact' WHERE id = $1`,
[chatId]
)
return {
success: true,
messages: [{ messageId: result.key.id, type: 'contact' }]
}
}
/**
* Handle poll message sending
*/
async function handlePollMessage(
body: PollMessageBody,
instanceId: string,
chatId: string,
jid: string,
socket: any,
options: any
) {
if (!body.name?.trim()) {
throw createError({ statusCode: 400, message: 'Poll name is required' })
}
if (!body.options || body.options.length < 2) {
throw createError({ statusCode: 400, message: 'At least 2 poll options are required' })
}
if (body.options.length > 12) {
throw createError({ statusCode: 400, message: 'Maximum 12 poll options allowed' })
}
const content = {
poll: {
name: body.name,
values: body.options,
selectableCount: body.selectableCount || 1
}
}
const result = await socket.sendMessage(jid, content, options)
// Save to database
await query(
`INSERT INTO messages (
instance_id, chat_id, message_id, from_jid, from_me,
message_type, content, timestamp, status, raw_message, quoted_message_id
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8, $9, $10)
ON CONFLICT (instance_id, message_id) DO NOTHING`,
[
instanceId,
chatId,
result.key.id,
'me',
true,
'poll',
JSON.stringify({ name: body.name, options: body.options, selectableCount: body.selectableCount || 1 }),
'sent',
JSON.stringify(result),
body.quotedMessageId || null
]
)
await query(
`UPDATE chats SET last_message_at = NOW(), last_message_type = 'poll' WHERE id = $1`,
[chatId]
)
return {
success: true,
messages: [{ messageId: result.key.id, type: 'poll' }]
}
}
/**
* Handle event message sending
*/
async function handleEventMessage(
body: EventMessageBody,
instanceId: string,
chatId: string,
jid: string,
socket: any,
options: any
) {
if (!body.name?.trim()) {
throw createError({ statusCode: 400, message: 'Event name is required' })
}
if (!body.startDate) {
throw createError({ statusCode: 400, message: 'Event start date is required' })
}
const eventContent: any = {
name: body.name,
startDate: new Date(body.startDate)
}
if (body.endDate) {
eventContent.endDate = new Date(body.endDate)
}
if (body.description) {
eventContent.description = body.description
}
if (body.location) {
if (body.location.latitude && body.location.longitude) {
eventContent.location = {
degreesLatitude: body.location.latitude,
degreesLongitude: body.location.longitude,
name: body.location.name,
address: body.location.address
}
}
}
const content = { event: eventContent }
const result = await socket.sendMessage(jid, content, options)
// Save to database
await query(
`INSERT INTO messages (
instance_id, chat_id, message_id, from_jid, from_me,
message_type, content, timestamp, status, raw_message, quoted_message_id
) VALUES ($1, $2, $3, $4, $5, $6, $7, NOW(), $8, $9, $10)
ON CONFLICT (instance_id, message_id) DO NOTHING`,
[
instanceId,
chatId,
result.key.id,
'me',
true,
'event',
JSON.stringify({
name: body.name,
startDate: body.startDate,
endDate: body.endDate,
description: body.description,
location: body.location
}),
'sent',
JSON.stringify(result),
body.quotedMessageId || null
]
)
await query(
`UPDATE chats SET last_message_at = NOW(), last_message_type = 'event' WHERE id = $1`,
[chatId]
)
return {
success: true,
messages: [{ messageId: result.key.id, type: 'event' }]
}
}
/**
* Handle media/sticker message sending
*/