diff --git a/app/components/messages/ContactSendModal.vue b/app/components/messages/ContactSendModal.vue
new file mode 100644
index 0000000..01d772f
--- /dev/null
+++ b/app/components/messages/ContactSendModal.vue
@@ -0,0 +1,150 @@
+
+
+
+
+
+
+
Enviar Contacto
+
+
+
+
+
+
+
+
+
+ Contacto {{ index + 1 }}
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Agregar otro contacto
+
+
+
+
+
+
+ Cancelar
+
+
+ Enviar
+
+
+
+
+
+
+
+
+
diff --git a/app/components/messages/EventSendModal.vue b/app/components/messages/EventSendModal.vue
new file mode 100644
index 0000000..f5ae049
--- /dev/null
+++ b/app/components/messages/EventSendModal.vue
@@ -0,0 +1,257 @@
+
+
+
+
+
+
+
Crear Evento
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Agregar fecha de fin
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Agregar ubicación
+
+
+
+
+
+
+
+
+
+
+ Las coordenadas son opcionales pero permiten mostrar el mapa
+
+
+
+
+
+
+
+ Cancelar
+
+
+ Crear evento
+
+
+
+
+
+
+
+
+
diff --git a/app/components/messages/MessageBubble.vue b/app/components/messages/MessageBubble.vue
index 7ae6eab..a3681f9 100644
--- a/app/components/messages/MessageBubble.vue
+++ b/app/components/messages/MessageBubble.vue
@@ -135,6 +135,20 @@
:from-me="message.fromMe"
/>
+
+
+
+
+
+
Suelta los archivos aquí
+
+
+
+
+
+
+
+
+
@@ -190,9 +208,37 @@ const props = withDefaults(defineProps(), {
replyingTo: null
})
+interface ContactInfo {
+ displayName: string
+ phoneNumber: string
+ organization?: string
+}
+
+interface PollData {
+ name: string
+ options: string[]
+ selectableCount: number
+}
+
+interface EventData {
+ name: string
+ startDate: string
+ endDate?: string
+ description?: string
+ location?: {
+ name?: string
+ address?: string
+ latitude?: number
+ longitude?: number
+ }
+}
+
const emit = defineEmits<{
send: [content: string, files: File[], caption: string, quotedId?: string, stickerModes?: boolean[]]
sendVoice: [audioFile: File]
+ sendContact: [contacts: ContactInfo[], quotedId?: string]
+ sendPoll: [poll: PollData, quotedId?: string]
+ sendEvent: [event: EventData, quotedId?: string]
cancelReply: []
typing: []
recording: [isRecording: boolean]
@@ -212,6 +258,11 @@ const isDragging = ref(false)
const stickerModes = ref([])
const showDebug = ref(false)
+// Modal states
+const showContactModal = ref(false)
+const showPollModal = ref(false)
+const showEventModal = ref(false)
+
// File size limits (in bytes) - should match server
const MAX_SIZES: Record = {
image: 16 * 1024 * 1024, // 16 MB
@@ -317,6 +368,21 @@ const attachmentMenuItems = [
label: 'Documento',
icon: 'i-lucide-file',
onSelect: () => documentInput.value?.click()
+ }],
+ [{
+ label: 'Contacto',
+ icon: 'i-lucide-user',
+ onSelect: () => showContactModal.value = true
+ }],
+ [{
+ label: 'Encuesta',
+ icon: 'i-lucide-bar-chart-2',
+ onSelect: () => showPollModal.value = true
+ }],
+ [{
+ label: 'Evento',
+ icon: 'i-lucide-calendar',
+ onSelect: () => showEventModal.value = true
}]
]
@@ -441,5 +507,20 @@ const formatDuration = (seconds: number): string => {
const getTypePlaceholder = (type: MessageType): string => {
return getMessageTypePlaceholder(type)
}
+
+// Handle contact send from modal
+const handleSendContact = (contacts: ContactInfo[]) => {
+ emit('sendContact', contacts, props.replyingTo?.messageId)
+}
+
+// Handle poll send from modal
+const handleSendPoll = (poll: PollData) => {
+ emit('sendPoll', poll, props.replyingTo?.messageId)
+}
+
+// Handle event send from modal
+const handleSendEvent = (eventData: EventData) => {
+ emit('sendEvent', eventData, props.replyingTo?.messageId)
+}
diff --git a/app/components/messages/PollSendModal.vue b/app/components/messages/PollSendModal.vue
new file mode 100644
index 0000000..d579455
--- /dev/null
+++ b/app/components/messages/PollSendModal.vue
@@ -0,0 +1,201 @@
+
+
+
+
+
+
+
Crear Encuesta
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ {{ index + 1 }}.
+
+
+
+
+
+
+
+ Agregar opción
+
+
+
+
+
+
+
Selección múltiple
+
+ Permitir seleccionar más de una opción
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+
+ Cancelar
+
+
+ Crear encuesta
+
+
+
+
+
+
+
+
+
diff --git a/app/components/messages/content/MessageEvent.vue b/app/components/messages/content/MessageEvent.vue
new file mode 100644
index 0000000..fb290ff
--- /dev/null
+++ b/app/components/messages/content/MessageEvent.vue
@@ -0,0 +1,126 @@
+
+
+
+
+
+ Evento
+
+
+
+
+ {{ event.name }}
+
+
+
+
+
+
+
+ {{ formatDate(event.startDate) }}
+
+
+
+
+
+ {{ formatDate(event.endDate) }}
+
+
+
+
+
+
+ {{ event.description }}
+
+
+
+
+
+
+
+ {{ event.location.name }}
+
+
+ {{ event.location.address }}
+
+
+ Ver en mapa
+
+
+
+
+
+
+
diff --git a/app/components/messages/content/MessagePoll.vue b/app/components/messages/content/MessagePoll.vue
new file mode 100644
index 0000000..229af46
--- /dev/null
+++ b/app/components/messages/content/MessagePoll.vue
@@ -0,0 +1,108 @@
+
+
+
+
+
+ Encuesta
+
+
+
+
+ {{ poll.name }}
+
+
+
+
+
+
+
+
+
+
+ {{ option }}
+
+ {{ poll.votes[index] }}
+
+
+
+
+
+
+
+ {{ totalVotes }} voto{{ totalVotes !== 1 ? 's' : '' }}
+
+ Seleccion multiple ({{ poll.selectableCount }})
+
+
+
+
+
+
diff --git a/app/pages/messages/index.vue b/app/pages/messages/index.vue
index 38262fb..e27b8e9 100644
--- a/app/pages/messages/index.vue
+++ b/app/pages/messages/index.vue
@@ -231,6 +231,9 @@
:replying-to="replyingTo"
@send="handleSendMessage"
@send-voice="handleSendVoice"
+ @send-contact="handleSendContact"
+ @send-poll="handleSendPoll"
+ @send-event="handleSendEvent"
@cancel-reply="replyingTo = null"
@typing="handleTyping"
@recording="handleRecordingPresence"
@@ -647,6 +650,121 @@ const handleRecordingPresence = (isRecording: boolean) => {
}
}
+// Handle contact send
+interface ContactInfo {
+ displayName: string
+ phoneNumber: string
+ organization?: string
+}
+
+const handleSendContact = async (contacts: ContactInfo[], quotedId?: string) => {
+ if (!selectedInstance.value?.value || !selectedChat.value) return
+
+ try {
+ const instanceId = selectedInstance.value.value
+ const chatId = selectedChat.value.id
+
+ await $fetch(`/api/messages/${instanceId}/${chatId}/send`, {
+ method: 'POST',
+ body: {
+ type: 'contact',
+ contacts,
+ quotedMessageId: quotedId || replyingTo.value?.messageId
+ }
+ })
+
+ replyingTo.value = null
+ messages.value = await $fetch(`/api/messages/${instanceId}/${chatId}`)
+ } catch (e: any) {
+ console.error('Error sending contact:', e)
+ toast.add({
+ title: 'Error de envío',
+ description: e?.data?.message || e?.message || 'Error al enviar el contacto',
+ color: 'error',
+ duration: 5000
+ })
+ }
+}
+
+// Handle poll send
+interface PollData {
+ name: string
+ options: string[]
+ selectableCount: number
+}
+
+const handleSendPoll = async (poll: PollData, quotedId?: string) => {
+ if (!selectedInstance.value?.value || !selectedChat.value) return
+
+ try {
+ const instanceId = selectedInstance.value.value
+ const chatId = selectedChat.value.id
+
+ await $fetch(`/api/messages/${instanceId}/${chatId}/send`, {
+ method: 'POST',
+ body: {
+ type: 'poll',
+ ...poll,
+ quotedMessageId: quotedId || replyingTo.value?.messageId
+ }
+ })
+
+ replyingTo.value = null
+ messages.value = await $fetch(`/api/messages/${instanceId}/${chatId}`)
+ } catch (e: any) {
+ console.error('Error sending poll:', e)
+ toast.add({
+ title: 'Error de envío',
+ description: e?.data?.message || e?.message || 'Error al enviar la encuesta',
+ color: 'error',
+ duration: 5000
+ })
+ }
+}
+
+// Handle event send
+interface EventData {
+ name: string
+ startDate: string
+ endDate?: string
+ description?: string
+ location?: {
+ name?: string
+ address?: string
+ latitude?: number
+ longitude?: number
+ }
+}
+
+const handleSendEvent = async (eventData: EventData, quotedId?: string) => {
+ if (!selectedInstance.value?.value || !selectedChat.value) return
+
+ try {
+ const instanceId = selectedInstance.value.value
+ const chatId = selectedChat.value.id
+
+ await $fetch(`/api/messages/${instanceId}/${chatId}/send`, {
+ method: 'POST',
+ body: {
+ type: 'event',
+ ...eventData,
+ quotedMessageId: quotedId || replyingTo.value?.messageId
+ }
+ })
+
+ replyingTo.value = null
+ messages.value = await $fetch(`/api/messages/${instanceId}/${chatId}`)
+ } catch (e: any) {
+ console.error('Error sending event:', e)
+ toast.add({
+ title: 'Error de envío',
+ description: e?.data?.message || e?.message || 'Error al enviar el evento',
+ color: 'error',
+ duration: 5000
+ })
+ }
+}
+
// Reload chats for current instance
const reloadChats = async () => {
if (!selectedInstance.value?.value) return
diff --git a/app/types/message.ts b/app/types/message.ts
index 7958b78..7c268b0 100644
--- a/app/types/message.ts
+++ b/app/types/message.ts
@@ -15,6 +15,7 @@ export type MessageType =
| 'location'
| 'reaction'
| 'poll'
+ | 'event'
| 'unknown'
// Estados de mensaje
@@ -79,6 +80,50 @@ export interface ContactInfo {
phones?: string[]
}
+/**
+ * Información de encuesta
+ */
+export interface PollInfo {
+ /** Nombre/pregunta de la encuesta */
+ name: string
+ /** Opciones de la encuesta */
+ options: string[]
+ /** Votos por opción */
+ votes?: number[]
+ /** Cantidad máxima de selecciones permitidas */
+ selectableCount?: number
+}
+
+/**
+ * Información de ubicación de evento
+ */
+export interface EventLocationInfo {
+ /** Nombre del lugar */
+ name?: string
+ /** Dirección */
+ address?: string
+ /** Latitud */
+ latitude?: number
+ /** Longitud */
+ longitude?: number
+}
+
+/**
+ * Información de evento
+ */
+export interface EventInfo {
+ /** Nombre del evento */
+ name: string
+ /** Fecha y hora de inicio */
+ startDate: string
+ /** Fecha y hora de fin */
+ endDate?: string
+ /** Descripción del evento */
+ description?: string
+ /** Ubicación del evento */
+ location?: EventLocationInfo
+}
+
/**
* Información de mensaje citado (quoted/reply)
*/
@@ -139,6 +184,10 @@ export interface Message {
location?: LocationInfo
/** Información de contacto */
contact?: ContactInfo
+ /** Información de encuesta */
+ poll?: PollInfo
+ /** Información de evento */
+ event?: EventInfo
/** Mensaje citado */
quoted?: QuotedMessage
/** Reacciones al mensaje */
@@ -304,6 +353,7 @@ export function getMessageTypePlaceholder(type: MessageType): string {
location: 'Ubicación',
reaction: 'Reacción',
poll: 'Encuesta',
+ event: 'Evento',
unknown: 'Mensaje'
}
return placeholders[type] || 'Mensaje'
@@ -324,6 +374,7 @@ export function getMessageTypeIcon(type: MessageType): string {
location: 'i-lucide-map-pin',
reaction: 'i-lucide-heart',
poll: 'i-lucide-bar-chart',
+ event: 'i-lucide-calendar',
unknown: 'i-lucide-help-circle'
}
return icons[type] || 'i-lucide-message-square'
diff --git a/server/api/messages/[instanceId]/[chatId]/send.post.ts b/server/api/messages/[instanceId]/[chatId]/send.post.ts
index 999a710..a4de526 100644
--- a/server/api/messages/[instanceId]/[chatId]/send.post.ts
+++ b/server/api/messages/[instanceId]/[chatId]/send.post.ts
@@ -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(event)
- const messageText = body.content || body.message
+ const body = await readBody(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
*/