Feature: Cargar historial de WhatsApp desde la UI
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m5s

- Agregar endpoint oldest.get.ts para obtener mensaje mas antiguo de un chat
- Agregar boton 'Cargar historial de WhatsApp' en vista de mensajes
- Mejorar HistorySection.vue con selector de chats y auto-deteccion
This commit is contained in:
2025-12-02 21:59:27 -06:00
parent 3b0fb57ce4
commit c0af0a3478
3 changed files with 313 additions and 28 deletions

View File

@@ -4,24 +4,79 @@
<div class="space-y-4">
<h3 class="text-lg font-medium text-[var(--wa-text)]">Solicitar Historial de Mensajes</h3>
<p class="text-sm text-[var(--wa-text-muted)]">
Solicita mensajes adicionales del historial. Los resultados llegaran via el evento messaging-history.set.
Solicita mensajes adicionales del historial de WhatsApp. Los mensajes se guardaran en la base de datos.
</p>
<div class="grid grid-cols-2 gap-4">
<!-- Chat Selector -->
<div class="space-y-2">
<label class="text-sm text-[var(--wa-text-muted)]">Seleccionar Chat</label>
<USelectMenu
v-model="selectedChat"
:items="chatOptions"
placeholder="Seleccionar chat..."
searchable
:loading="loadingChats"
class="w-full"
/>
</div>
<!-- Selected Chat Info -->
<div v-if="selectedChat && oldestMessage" class="p-3 rounded bg-[var(--wa-bg-hover)] space-y-1">
<p class="text-sm text-[var(--wa-text)]">
<span class="text-[var(--wa-text-muted)]">Chat:</span> {{ selectedChat.label }}
</p>
<p class="text-sm text-[var(--wa-text)]">
<span class="text-[var(--wa-text-muted)]">Mensaje mas antiguo:</span>
{{ formatDate(oldestMessage.timestamp) }}
</p>
</div>
<div v-else-if="selectedChat && !loadingOldest" class="p-3 rounded bg-yellow-900/20 text-yellow-400 text-sm">
Este chat no tiene mensajes guardados en la base de datos.
</div>
<!-- Count Input -->
<div class="space-y-2">
<label class="text-sm text-[var(--wa-text-muted)]">Cantidad de mensajes a solicitar</label>
<UInput
v-model.number="count"
type="number"
placeholder="Cantidad de mensajes"
:min="1"
:max="1000"
class="w-48"
/>
</div>
<!-- Action Button -->
<UButton
:loading="loading"
:disabled="!instanceId || !count"
@click="fetchHistory"
color="primary"
>
Solicitar {{ count }} mensajes del historial
</UButton>
<!-- Advanced Mode Toggle -->
<div class="pt-4 border-t border-[var(--wa-border)]">
<UCheckbox v-model="showAdvanced" label="Modo avanzado (JSON manual)" />
</div>
<!-- Advanced Mode Fields -->
<div v-if="showAdvanced" class="space-y-4 p-4 rounded bg-[var(--wa-bg-hover)]">
<p class="text-xs text-[var(--wa-text-muted)]">
Estos campos permiten especificar manualmente el mensaje de referencia para la sincronizacion.
</p>
<div class="grid grid-cols-2 gap-4">
<UInput
v-model.number="oldestMsgTimestamp"
v-model.number="manualTimestamp"
type="number"
placeholder="Timestamp mas antiguo (opcional)"
placeholder="Timestamp (segundos)"
/>
</div>
<div class="space-y-2">
<p class="text-sm text-[var(--wa-text-muted)]">Oldest Message Key (opcional, JSON):</p>
<label class="text-sm text-[var(--wa-text-muted)]">Message Key (JSON):</label>
<UTextarea
v-model="oldestMsgKeyJson"
placeholder='{"remoteJid": "...", "id": "...", "fromMe": false}'
@@ -29,18 +84,25 @@
class="font-mono text-sm"
/>
</div>
<UButton
:loading="loading"
:disabled="!instanceId || !count"
@click="fetchHistory"
>
Solicitar Historial
</UButton>
</div>
</div>
</div>
</template>
<script setup lang="ts">
interface Chat {
id: string
jid: string
name: string
isGroup: boolean
}
interface OldestMessage {
hasMessages: boolean
messageKey: { remoteJid: string; id: string; fromMe: boolean } | null
timestamp: number | null
}
const props = defineProps<{
instanceId: string | null
}>()
@@ -49,15 +111,90 @@ const emit = defineEmits<{
(e: 'response', data: any): void
}>()
const count = ref<number>(50)
const oldestMsgTimestamp = ref<number | null>(null)
const oldestMsgKeyJson = ref('')
const count = ref<number>(100)
const loading = ref(false)
const showAdvanced = ref(false)
const oldestMsgKeyJson = ref('')
const manualTimestamp = ref<number | null>(null)
// Chat selection
const chats = ref<Chat[]>([])
const selectedChat = ref<{ label: string; value: string; jid: string } | null>(null)
const loadingChats = ref(false)
const oldestMessage = ref<OldestMessage | null>(null)
const loadingOldest = ref(false)
// Computed chat options for select menu
const chatOptions = computed(() =>
chats.value.map(chat => ({
label: chat.name || chat.jid.split('@')[0],
value: chat.id,
jid: chat.jid
}))
)
// Load chats when instanceId changes
watch(() => props.instanceId, async (instanceId) => {
if (!instanceId) {
chats.value = []
selectedChat.value = null
return
}
loadingChats.value = true
try {
chats.value = await $fetch<Chat[]>(`/api/messages/${instanceId}/chats`)
} catch (e) {
console.error('Error loading chats:', e)
chats.value = []
} finally {
loadingChats.value = false
}
}, { immediate: true })
// Load oldest message when chat is selected
watch(selectedChat, async (chat) => {
if (!chat || !props.instanceId) {
oldestMessage.value = null
return
}
loadingOldest.value = true
try {
oldestMessage.value = await $fetch<OldestMessage>(
`/api/messages/${props.instanceId}/${chat.value}/oldest`
)
// Auto-populate JSON field if not in advanced mode
if (oldestMessage.value?.hasMessages && oldestMessage.value.messageKey) {
oldestMsgKeyJson.value = JSON.stringify(oldestMessage.value.messageKey, null, 2)
manualTimestamp.value = oldestMessage.value.timestamp
} else {
oldestMsgKeyJson.value = ''
manualTimestamp.value = null
}
} catch (e) {
console.error('Error loading oldest message:', e)
oldestMessage.value = null
} finally {
loadingOldest.value = false
}
})
// Format timestamp to readable date
const formatDate = (timestamp: number | null) => {
if (!timestamp) return 'N/A'
return new Date(timestamp * 1000).toLocaleString()
}
const fetchHistory = async () => {
loading.value = true
try {
let oldestMsgKey = undefined
let oldestMsgTimestamp = undefined
if (showAdvanced.value) {
// Use manual JSON input
if (oldestMsgKeyJson.value.trim()) {
try {
oldestMsgKey = JSON.parse(oldestMsgKeyJson.value)
@@ -67,6 +204,12 @@ const fetchHistory = async () => {
return
}
}
oldestMsgTimestamp = manualTimestamp.value || undefined
} else if (oldestMessage.value?.hasMessages) {
// Use auto-detected oldest message
oldestMsgKey = oldestMessage.value.messageKey
oldestMsgTimestamp = oldestMessage.value.timestamp
}
const result = await $fetch('/api/debug/history/fetch', {
method: 'POST',
@@ -74,7 +217,7 @@ const fetchHistory = async () => {
instanceId: props.instanceId,
count: count.value,
oldestMsgKey,
oldestMsgTimestamp: oldestMsgTimestamp.value || undefined
oldestMsgTimestamp
}
})
emit('response', result)

View File

@@ -188,6 +188,32 @@
</button>
</div>
<!-- Load from WhatsApp when local DB is exhausted -->
<div
v-else-if="!hasMoreMessages && !loadingWhatsAppHistory"
class="flex flex-col items-center py-3 gap-2"
>
<span class="text-xs text-[var(--wa-text-muted)]">
No hay mas mensajes en la base de datos
</span>
<button
class="text-xs px-3 py-1.5 rounded bg-[var(--wa-green)] text-white hover:opacity-90 transition-opacity"
@click="fetchWhatsAppHistory"
>
Cargar historial de WhatsApp
</button>
</div>
<div
v-else-if="loadingWhatsAppHistory"
class="flex flex-col items-center py-3 gap-2"
>
<UIcon name="i-lucide-loader-2" class="w-5 h-5 animate-spin text-[var(--wa-green)]" />
<span class="text-xs text-[var(--wa-text-muted)]">
Solicitando historial de WhatsApp...
</span>
</div>
<MessagesMessageBubble
v-for="message in reversedMessages"
:key="message.id"
@@ -244,6 +270,7 @@ const messages = ref<any[]>([])
const loadingChats = ref(false)
const loadingMessages = ref(false)
const loadingMore = ref(false)
const loadingWhatsAppHistory = ref(false)
const hasMoreMessages = ref(true)
const showDebugPanel = ref(false)
const showChatsDebug = ref(false)
@@ -360,6 +387,56 @@ const handleScroll = () => {
}
}
// Fetch message history from WhatsApp (when local DB is exhausted)
const fetchWhatsAppHistory = async () => {
if (!selectedInstance.value?.value || !selectedChat.value) return
loadingWhatsAppHistory.value = true
try {
const instanceId = selectedInstance.value.value
const chatId = selectedChat.value.id
// Get the oldest message from local DB
const oldest = await $fetch<{
hasMessages: boolean
messageKey: { remoteJid: string; id: string; fromMe: boolean } | null
timestamp: number | null
}>(`/api/messages/${instanceId}/${chatId}/oldest`)
// Request history from WhatsApp
if (!oldest.hasMessages) {
// No messages in DB, request without reference
await $fetch('/api/debug/history/fetch', {
method: 'POST',
body: {
instanceId,
count: 100
}
})
} else {
// Request messages older than the oldest in DB
await $fetch('/api/debug/history/fetch', {
method: 'POST',
body: {
instanceId,
count: 100,
oldestMsgKey: oldest.messageKey,
oldestMsgTimestamp: oldest.timestamp
}
})
}
// Wait for history sync to complete and reload
await new Promise(r => setTimeout(r, 3000))
await reloadMessages()
hasMoreMessages.value = true
} catch (e) {
console.error('Error fetching WhatsApp history:', e)
} finally {
loadingWhatsAppHistory.value = false
}
}
// Computed presence text for current chat
const currentChatPresence = computed(() => {
if (!selectedChat.value?.jid) return null

View File

@@ -0,0 +1,65 @@
/**
* GET /api/messages/:instanceId/:chatId/oldest
* Get the oldest message from a chat for use with fetchMessageHistory
*/
import { query } from '../../../../utils/database'
interface ChatRow {
jid: string
}
interface MessageRow {
message_id: string
from_me: boolean
timestamp: Date
}
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 chat info (jid)
const chatResult = await query<ChatRow>(
'SELECT jid FROM chats WHERE id = $1 AND instance_id = $2',
[chatId, instanceId]
)
if (!chatResult.rows.length) {
throw createError({ statusCode: 404, message: 'Chat not found' })
}
// Get oldest message
const oldestResult = await query<MessageRow>(
`SELECT message_id, from_me, timestamp
FROM messages
WHERE chat_id = $1
ORDER BY timestamp ASC
LIMIT 1`,
[chatId]
)
if (!oldestResult.rows.length) {
return {
hasMessages: false,
messageKey: null,
timestamp: null
}
}
const oldest = oldestResult.rows[0]
return {
hasMessages: true,
messageKey: {
remoteJid: chatResult.rows[0].jid,
id: oldest.message_id,
fromMe: oldest.from_me
},
timestamp: Math.floor(new Date(oldest.timestamp).getTime() / 1000)
}
})