diff --git a/app/pages/messages/index.vue b/app/pages/messages/index.vue index 423ffd3..8a0c085 100644 --- a/app/pages/messages/index.vue +++ b/app/pages/messages/index.vue @@ -70,6 +70,14 @@ icon="i-lucide-search" class="flex-1" /> + + + + + + + + + + Nueva conversación + + + + + + Número de teléfono + + + + Ingresa el número con código de país, sin espacios ni guiones + + + + + {{ newChatError }} + + + + + Cancelar + + + Iniciar conversación + + + + + + @@ -279,6 +338,12 @@ const showDebugPanel = ref(false) const showChatsDebug = ref(false) const showSelectedChatDebug = ref(false) +// New chat modal state +const showNewChatModal = ref(false) +const newChatPhoneNumber = ref('') +const newChatLoading = ref(false) +const newChatError = ref('') + // Instance options for selector const instanceOptions = computed(() => instances.value @@ -658,6 +723,54 @@ const handleReact = async (message: any, emoji: string) => { } } +// New chat modal functions +const closeNewChatModal = () => { + showNewChatModal.value = false + newChatPhoneNumber.value = '' + newChatError.value = '' +} + +const startNewChat = async () => { + if (!selectedInstance.value?.value || !newChatPhoneNumber.value.trim()) return + + newChatLoading.value = true + newChatError.value = '' + + try { + const result = await $fetch(`/api/messages/${selectedInstance.value.value}/new-chat`, { + method: 'POST', + body: { + phoneNumber: newChatPhoneNumber.value.trim() + } + }) + + if (result.success && result.chat) { + // If it's a new chat, add it to the list + if (result.isNew) { + chats.value.unshift(result.chat) + } + + // Select the chat + selectedChat.value = result.chat + + // Close the modal + closeNewChatModal() + + toast.add({ + title: result.isNew ? 'Conversación creada' : 'Conversación encontrada', + icon: 'i-lucide-check', + color: 'success', + timeout: 2000 + }) + } + } catch (e: any) { + console.error('Error creating new chat:', e) + newChatError.value = e?.data?.message || e?.message || 'Error al crear la conversación' + } finally { + newChatLoading.value = false + } +} + // Handle typing event from input const handleTyping = () => { if (!selectedChat.value?.jid || selectedChat.value?.isGroup) return diff --git a/server/api/messages/[instanceId]/new-chat.post.ts b/server/api/messages/[instanceId]/new-chat.post.ts new file mode 100644 index 0000000..bc74933 --- /dev/null +++ b/server/api/messages/[instanceId]/new-chat.post.ts @@ -0,0 +1,138 @@ +/** + * POST /api/messages/:instanceId/new-chat + * Create or get a chat by phone number to start a new conversation + */ +import { query } from '../../../utils/database' +import { baileysManager } from '../../../services/baileys/manager' +import { randomUUID } from 'crypto' + +interface NewChatBody { + phoneNumber: string +} + +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') + if (!instanceId) { + throw createError({ statusCode: 400, message: 'Missing instanceId' }) + } + + // Verify instance exists and is connected + const socket = baileysManager.getSocket(instanceId) + if (!socket) { + throw createError({ statusCode: 400, message: 'Instance not connected' }) + } + + const body = await readBody(event) + + if (!body.phoneNumber?.trim()) { + throw createError({ statusCode: 400, message: 'Phone number is required' }) + } + + // Clean phone number: remove spaces, dashes, parentheses, and + prefix + const cleanNumber = body.phoneNumber.replace(/[\s\-\(\)\+]/g, '') + + // Validate it's a numeric string + if (!/^\d+$/.test(cleanNumber)) { + throw createError({ statusCode: 400, message: 'Invalid phone number format' }) + } + + // Build WhatsApp JID (individual chat) + const jid = `${cleanNumber}@s.whatsapp.net` + + try { + // Check if chat already exists + const existingChat = await query( + `SELECT id, jid, name, is_group, unread_count, last_message_at, last_message_type, + (SELECT COALESCE(content, caption) FROM messages m + WHERE m.chat_id = chats.id ORDER BY timestamp DESC LIMIT 1) as last_message + FROM chats + WHERE instance_id = $1 AND jid = $2`, + [instanceId, jid] + ) + + if (existingChat.rows.length > 0) { + // Chat exists, return it + const chat = existingChat.rows[0] + return { + success: true, + chat: { + id: chat.id, + jid: chat.jid, + name: chat.name || cleanNumber, + isGroup: chat.is_group, + unreadCount: chat.unread_count || 0, + lastMessage: chat.last_message, + lastMessageAt: chat.last_message_at, + lastMessageType: chat.last_message_type + }, + isNew: false + } + } + + // Chat doesn't exist, create it + const chatId = randomUUID() + + // Try to get contact name from WhatsApp (optional, may fail) + let contactName = cleanNumber + try { + // Check if number exists on WhatsApp using onWhatsApp + const [exists] = await socket.onWhatsApp(jid) + if (!exists?.exists) { + throw createError({ + statusCode: 400, + message: 'Este número no está registrado en WhatsApp' + }) + } + + // Try to get the push name or contact name + if (exists.notify) { + contactName = exists.notify + } + } catch (e: any) { + // If it's our validation error, rethrow + if (e.statusCode === 400) { + throw e + } + // Otherwise just use the number as name + console.log('[NewChat] Could not verify WhatsApp status:', e.message) + } + + // Insert new chat + await query( + `INSERT INTO chats (id, instance_id, jid, name, is_group, unread_count, created_at, updated_at) + VALUES ($1, $2, $3, $4, false, 0, NOW(), NOW())`, + [chatId, instanceId, jid, contactName] + ) + + return { + success: true, + chat: { + id: chatId, + jid: jid, + name: contactName, + isGroup: false, + unreadCount: 0, + lastMessage: null, + lastMessageAt: null, + lastMessageType: null + }, + isNew: true + } + } catch (e: any) { + // Rethrow HTTP errors + if (e.statusCode) { + throw e + } + + console.error('[NewChat] Error creating chat:', e) + throw createError({ + statusCode: 500, + message: `Error creating chat: ${e.message}` + }) + } +})
+ Ingresa el número con código de país, sin espacios ni guiones +