Feat: Agregar opcion para eliminar chats completamente
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m8s
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m8s
- Crear endpoint DELETE /api/messages/:instanceId/:chatId - Agregar boton de eliminar en ChatItem - Crear modal de confirmacion con advertencia - Elimina el chat y todos los mensajes relacionados (CASCADE) - Muestra cantidad de mensajes eliminados en notificacion
This commit is contained in:
180
README.md
180
README.md
@@ -33,21 +33,181 @@ node .output/server/index.mjs
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
## API Endpoints
|
## API REST
|
||||||
|
|
||||||
|
### Autenticación
|
||||||
|
|
||||||
|
Todos los endpoints requieren autenticación mediante API Key:
|
||||||
|
|
||||||
|
```
|
||||||
|
Authorization: Bearer <NUXT_MASTER_API_KEY>
|
||||||
|
```
|
||||||
|
|
||||||
### Instancias
|
### Instancias
|
||||||
- `GET /api/instances` - Lista instancias
|
|
||||||
- `POST /api/instances` - Crear instancia
|
| Método | Endpoint | Descripción |
|
||||||
- `POST /api/instances/:id/connect` - Conectar instancia
|
|--------|----------|-------------|
|
||||||
- `POST /api/instances/:id/disconnect` - Desconectar
|
| GET | `/api/instances` | Listar todas las instancias |
|
||||||
|
| POST | `/api/instances` | Crear nueva instancia |
|
||||||
|
| GET | `/api/instances/:id` | Obtener detalles de instancia |
|
||||||
|
| GET | `/api/instances/:id/qr` | Obtener código QR |
|
||||||
|
| POST | `/api/instances/:id/connect` | Conectar instancia |
|
||||||
|
| POST | `/api/instances/:id/disconnect` | Desconectar instancia |
|
||||||
|
| DELETE | `/api/instances/:id` | Eliminar instancia |
|
||||||
|
|
||||||
|
**Ejemplo - Listar instancias:**
|
||||||
|
```bash
|
||||||
|
curl -X GET "https://whatsapp.nucleoriofrio.com/api/instances" \
|
||||||
|
-H "Authorization: Bearer <API_KEY>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ejemplo - Crear instancia:**
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://whatsapp.nucleoriofrio.com/api/instances" \
|
||||||
|
-H "Authorization: Bearer <API_KEY>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{"name": "Mi WhatsApp"}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Chats y Contactos
|
||||||
|
|
||||||
|
| Método | Endpoint | Descripción |
|
||||||
|
|--------|----------|-------------|
|
||||||
|
| GET | `/api/messages/:instanceId/chats` | Listar todos los chats |
|
||||||
|
| POST | `/api/messages/:instanceId/new-chat` | Crear nuevo chat |
|
||||||
|
|
||||||
|
**Ejemplo - Listar chats:**
|
||||||
|
```bash
|
||||||
|
curl -X GET "https://whatsapp.nucleoriofrio.com/api/messages/{instanceId}/chats" \
|
||||||
|
-H "Authorization: Bearer <API_KEY>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Respuesta:**
|
||||||
|
```json
|
||||||
|
[
|
||||||
|
{
|
||||||
|
"id": "abc123",
|
||||||
|
"jid": "5491155551234@s.whatsapp.net",
|
||||||
|
"name": "Juan Pérez",
|
||||||
|
"isGroup": false,
|
||||||
|
"unreadCount": 2,
|
||||||
|
"lastMessageAt": "2025-01-15T10:30:00Z"
|
||||||
|
}
|
||||||
|
]
|
||||||
|
```
|
||||||
|
|
||||||
### Mensajes
|
### Mensajes
|
||||||
- `POST /api/messages/:instanceId/:chatId/send` - Enviar mensaje
|
|
||||||
- `GET /api/messages/:instanceId/:chatId` - Obtener mensajes
|
|
||||||
- `POST /api/messages/:instanceId/react` - Reaccionar a mensaje
|
|
||||||
|
|
||||||
### MCP
|
| Método | Endpoint | Descripción |
|
||||||
- `POST /api/mcp` - Endpoint JSON-RPC para agentes IA
|
|--------|----------|-------------|
|
||||||
|
| GET | `/api/messages/:instanceId/:chatId` | Obtener mensajes del chat |
|
||||||
|
| POST | `/api/messages/send` | Enviar mensaje (simple) |
|
||||||
|
| POST | `/api/messages/:instanceId/:chatId/send` | Enviar mensaje (avanzado) |
|
||||||
|
|
||||||
|
**Ejemplo - Obtener mensajes:**
|
||||||
|
```bash
|
||||||
|
curl -X GET "https://whatsapp.nucleoriofrio.com/api/messages/{instanceId}/{chatId}?limit=50" \
|
||||||
|
-H "Authorization: Bearer <API_KEY>"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ejemplo - Enviar mensaje de texto:**
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://whatsapp.nucleoriofrio.com/api/messages/send" \
|
||||||
|
-H "Authorization: Bearer <API_KEY>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"instanceId": "inst_abc123",
|
||||||
|
"to": "5491155551234",
|
||||||
|
"message": "Hola! Este es un mensaje de prueba"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ejemplo - Enviar imagen:**
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://whatsapp.nucleoriofrio.com/api/messages/{instanceId}/{chatId}/send" \
|
||||||
|
-H "Authorization: Bearer <API_KEY>" \
|
||||||
|
-F "file=@imagen.jpg" \
|
||||||
|
-F "caption=Descripción de la imagen"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ejemplo - Enviar documento:**
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://whatsapp.nucleoriofrio.com/api/messages/{instanceId}/{chatId}/send" \
|
||||||
|
-H "Authorization: Bearer <API_KEY>" \
|
||||||
|
-F "file=@documento.pdf" \
|
||||||
|
-F "caption=Reporte mensual"
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ejemplo - Enviar contacto:**
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://whatsapp.nucleoriofrio.com/api/messages/{instanceId}/{chatId}/send" \
|
||||||
|
-H "Authorization: Bearer <API_KEY>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"type": "contact",
|
||||||
|
"contacts": [{
|
||||||
|
"displayName": "Juan Pérez",
|
||||||
|
"phoneNumber": "+5491155551234"
|
||||||
|
}]
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Ejemplo - Enviar encuesta:**
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://whatsapp.nucleoriofrio.com/api/messages/{instanceId}/{chatId}/send" \
|
||||||
|
-H "Authorization: Bearer <API_KEY>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"type": "poll",
|
||||||
|
"name": "¿Qué día prefieres?",
|
||||||
|
"options": ["Lunes", "Martes", "Miércoles"],
|
||||||
|
"selectableCount": 1
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
### Reacciones
|
||||||
|
|
||||||
|
**Ejemplo - Reaccionar a mensaje:**
|
||||||
|
```bash
|
||||||
|
curl -X POST "https://whatsapp.nucleoriofrio.com/api/messages/{instanceId}/react" \
|
||||||
|
-H "Authorization: Bearer <API_KEY>" \
|
||||||
|
-H "Content-Type: application/json" \
|
||||||
|
-d '{
|
||||||
|
"messageId": "3EB0A1B2C3D4E5F6",
|
||||||
|
"emoji": "👍"
|
||||||
|
}'
|
||||||
|
```
|
||||||
|
|
||||||
|
**Quitar reacción:** enviar `emoji` como string vacío `""`
|
||||||
|
|
||||||
|
### Formato de Destinatarios (JID)
|
||||||
|
|
||||||
|
- **Contacto individual:** `5491155551234@s.whatsapp.net` (código país sin +)
|
||||||
|
- **Grupo:** `123456789012345678@g.us`
|
||||||
|
|
||||||
|
### Tipos de Mensaje Soportados
|
||||||
|
|
||||||
|
| Tipo | Descripción |
|
||||||
|
|------|-------------|
|
||||||
|
| `text` | Mensaje de texto |
|
||||||
|
| `image` | Imagen con caption opcional |
|
||||||
|
| `video` | Video con caption opcional |
|
||||||
|
| `audio` | Audio o nota de voz |
|
||||||
|
| `document` | Documento/archivo |
|
||||||
|
| `sticker` | Sticker |
|
||||||
|
| `contact` | Tarjeta de contacto |
|
||||||
|
| `poll` | Encuesta (2-12 opciones) |
|
||||||
|
| `event` | Evento con fecha/ubicación |
|
||||||
|
|
||||||
|
### Límites de Archivos
|
||||||
|
|
||||||
|
| Tipo | Tamaño Máximo |
|
||||||
|
|------|---------------|
|
||||||
|
| Imagen | 16 MB |
|
||||||
|
| Video | 64 MB |
|
||||||
|
| Audio | 16 MB |
|
||||||
|
| Documento | 100 MB |
|
||||||
|
| Sticker | 500 KB |
|
||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
|
|||||||
@@ -32,6 +32,14 @@
|
|||||||
>
|
>
|
||||||
<UIcon name="i-lucide-user-pen" class="w-3 h-3" />
|
<UIcon name="i-lucide-user-pen" class="w-3 h-3" />
|
||||||
</button>
|
</button>
|
||||||
|
<!-- Delete button -->
|
||||||
|
<button
|
||||||
|
@click.stop="$emit('deleteChat', chat)"
|
||||||
|
class="text-xs text-[var(--wa-text-muted)] hover:text-red-500 opacity-50 hover:opacity-100"
|
||||||
|
title="Eliminar chat"
|
||||||
|
>
|
||||||
|
<UIcon name="i-lucide-trash-2" class="w-3 h-3" />
|
||||||
|
</button>
|
||||||
<!-- Debug button -->
|
<!-- Debug button -->
|
||||||
<button
|
<button
|
||||||
@click.stop="showDebug = !showDebug"
|
@click.stop="showDebug = !showDebug"
|
||||||
@@ -101,6 +109,7 @@ const props = defineProps<Props>()
|
|||||||
defineEmits<{
|
defineEmits<{
|
||||||
click: []
|
click: []
|
||||||
editAlias: [chat: Chat]
|
editAlias: [chat: Chat]
|
||||||
|
deleteChat: [chat: Chat]
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const showDebug = ref(false)
|
const showDebug = ref(false)
|
||||||
|
|||||||
@@ -119,6 +119,7 @@
|
|||||||
:active="selectedChat?.id === chat.id"
|
:active="selectedChat?.id === chat.id"
|
||||||
@click="selectedChat = chat"
|
@click="selectedChat = chat"
|
||||||
@edit-alias="openAliasModal"
|
@edit-alias="openAliasModal"
|
||||||
|
@delete-chat="openDeleteModal"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
@@ -260,6 +261,53 @@
|
|||||||
@saved="handleAliasSaved"
|
@saved="handleAliasSaved"
|
||||||
/>
|
/>
|
||||||
|
|
||||||
|
<!-- Delete Confirmation Modal -->
|
||||||
|
<UModal v-model:open="showDeleteModal">
|
||||||
|
<template #content>
|
||||||
|
<UCard>
|
||||||
|
<template #header>
|
||||||
|
<div class="flex items-center gap-3">
|
||||||
|
<div class="p-2 rounded-full bg-red-500/20">
|
||||||
|
<UIcon name="i-lucide-trash-2" class="w-5 h-5 text-red-500" />
|
||||||
|
</div>
|
||||||
|
<h3 class="text-lg font-semibold text-white">Eliminar chat</h3>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
|
||||||
|
<div class="space-y-4">
|
||||||
|
<p class="text-[var(--wa-text-muted)]">
|
||||||
|
Estas a punto de eliminar el chat con
|
||||||
|
<strong class="text-white">{{ chatToDelete?.name || chatToDelete?.jid }}</strong>.
|
||||||
|
</p>
|
||||||
|
<div class="p-3 rounded-lg bg-red-500/10 border border-red-500/30">
|
||||||
|
<p class="text-sm text-red-400">
|
||||||
|
Esta accion eliminara todos los mensajes y datos relacionados.
|
||||||
|
Esta accion no se puede deshacer.
|
||||||
|
</p>
|
||||||
|
</div>
|
||||||
|
</div>
|
||||||
|
|
||||||
|
<template #footer>
|
||||||
|
<div class="flex justify-end gap-2">
|
||||||
|
<UButton
|
||||||
|
variant="ghost"
|
||||||
|
@click="showDeleteModal = false"
|
||||||
|
>
|
||||||
|
Cancelar
|
||||||
|
</UButton>
|
||||||
|
<UButton
|
||||||
|
color="error"
|
||||||
|
:loading="isDeleting"
|
||||||
|
@click="confirmDeleteChat"
|
||||||
|
>
|
||||||
|
Eliminar
|
||||||
|
</UButton>
|
||||||
|
</div>
|
||||||
|
</template>
|
||||||
|
</UCard>
|
||||||
|
</template>
|
||||||
|
</UModal>
|
||||||
|
|
||||||
<!-- New Chat Modal -->
|
<!-- New Chat Modal -->
|
||||||
<UModal v-model:open="showNewChatModal">
|
<UModal v-model:open="showNewChatModal">
|
||||||
<template #content>
|
<template #content>
|
||||||
@@ -357,6 +405,11 @@ const newChatError = ref('')
|
|||||||
const showAliasModal = ref(false)
|
const showAliasModal = ref(false)
|
||||||
const chatToEditAlias = ref<any>(null)
|
const chatToEditAlias = ref<any>(null)
|
||||||
|
|
||||||
|
// Delete confirmation modal state
|
||||||
|
const showDeleteModal = ref(false)
|
||||||
|
const chatToDelete = ref<any>(null)
|
||||||
|
const isDeleting = ref(false)
|
||||||
|
|
||||||
// Instance options for selector
|
// Instance options for selector
|
||||||
const instanceOptions = computed(() =>
|
const instanceOptions = computed(() =>
|
||||||
instances.value
|
instances.value
|
||||||
@@ -766,6 +819,56 @@ const handleAliasSaved = (updatedChat: any) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// Delete modal functions
|
||||||
|
const openDeleteModal = (chat: any) => {
|
||||||
|
chatToDelete.value = chat
|
||||||
|
showDeleteModal.value = true
|
||||||
|
}
|
||||||
|
|
||||||
|
const confirmDeleteChat = async () => {
|
||||||
|
if (!chatToDelete.value || !selectedInstance.value?.value) return
|
||||||
|
|
||||||
|
isDeleting.value = true
|
||||||
|
try {
|
||||||
|
const result = await $fetch(`/api/messages/${selectedInstance.value.value}/${chatToDelete.value.id}`, {
|
||||||
|
method: 'DELETE'
|
||||||
|
})
|
||||||
|
|
||||||
|
if (result.success) {
|
||||||
|
// Remove chat from list
|
||||||
|
chats.value = chats.value.filter(c => c.id !== chatToDelete.value.id)
|
||||||
|
|
||||||
|
// Clear selected chat if it was the deleted one
|
||||||
|
if (selectedChat.value?.id === chatToDelete.value.id) {
|
||||||
|
selectedChat.value = null
|
||||||
|
messages.value = []
|
||||||
|
}
|
||||||
|
|
||||||
|
toast.add({
|
||||||
|
title: 'Chat eliminado',
|
||||||
|
description: `Se eliminaron ${result.deleted.messagesDeleted} mensajes`,
|
||||||
|
icon: 'i-lucide-check',
|
||||||
|
color: 'success',
|
||||||
|
timeout: 3000
|
||||||
|
})
|
||||||
|
|
||||||
|
showDeleteModal.value = false
|
||||||
|
chatToDelete.value = null
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('Error deleting chat:', error)
|
||||||
|
toast.add({
|
||||||
|
title: 'Error al eliminar',
|
||||||
|
description: error?.data?.message || error?.message || 'No se pudo eliminar el chat',
|
||||||
|
icon: 'i-lucide-x',
|
||||||
|
color: 'error',
|
||||||
|
timeout: 5000
|
||||||
|
})
|
||||||
|
} finally {
|
||||||
|
isDeleting.value = false
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
// New chat modal functions
|
// New chat modal functions
|
||||||
const closeNewChatModal = () => {
|
const closeNewChatModal = () => {
|
||||||
showNewChatModal.value = false
|
showNewChatModal.value = false
|
||||||
|
|||||||
72
server/api/messages/[instanceId]/[chatId]/index.delete.ts
Normal file
72
server/api/messages/[instanceId]/[chatId]/index.delete.ts
Normal file
@@ -0,0 +1,72 @@
|
|||||||
|
/**
|
||||||
|
* DELETE /api/messages/:instanceId/:chatId
|
||||||
|
* Delete a chat and all its related data (messages, etc.)
|
||||||
|
*/
|
||||||
|
import { query } from '../../../../utils/database'
|
||||||
|
|
||||||
|
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')
|
||||||
|
|
||||||
|
if (!instanceId) {
|
||||||
|
throw createError({ statusCode: 400, message: 'Missing instanceId' })
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!chatId) {
|
||||||
|
throw createError({ statusCode: 400, message: 'Missing chatId' })
|
||||||
|
}
|
||||||
|
|
||||||
|
try {
|
||||||
|
// First, get chat info for logging
|
||||||
|
const chatResult = await query(
|
||||||
|
`SELECT id, jid, name, alias FROM chats WHERE id = $1 AND instance_id = $2`,
|
||||||
|
[chatId, instanceId]
|
||||||
|
)
|
||||||
|
|
||||||
|
if (chatResult.rows.length === 0) {
|
||||||
|
throw createError({ statusCode: 404, message: 'Chat not found' })
|
||||||
|
}
|
||||||
|
|
||||||
|
const chat = chatResult.rows[0]
|
||||||
|
|
||||||
|
// Count messages that will be deleted
|
||||||
|
const messageCount = await query(
|
||||||
|
`SELECT COUNT(*) as count FROM messages WHERE chat_id = $1`,
|
||||||
|
[chatId]
|
||||||
|
)
|
||||||
|
|
||||||
|
// Delete the chat (messages will be deleted via CASCADE)
|
||||||
|
await query(
|
||||||
|
`DELETE FROM chats WHERE id = $1 AND instance_id = $2`,
|
||||||
|
[chatId, instanceId]
|
||||||
|
)
|
||||||
|
|
||||||
|
console.log(`[Chat API] Deleted chat ${chat.jid} (${chat.name || chat.alias || 'unnamed'}) with ${messageCount.rows[0].count} messages`)
|
||||||
|
|
||||||
|
return {
|
||||||
|
success: true,
|
||||||
|
deleted: {
|
||||||
|
chatId: chat.id,
|
||||||
|
jid: chat.jid,
|
||||||
|
name: chat.name || chat.alias,
|
||||||
|
messagesDeleted: parseInt(messageCount.rows[0].count)
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} catch (error: any) {
|
||||||
|
console.error('[Chat API] Error deleting chat:', error)
|
||||||
|
|
||||||
|
if (error.statusCode) {
|
||||||
|
throw error
|
||||||
|
}
|
||||||
|
|
||||||
|
throw createError({
|
||||||
|
statusCode: 500,
|
||||||
|
message: error.message || 'Error deleting chat'
|
||||||
|
})
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user