Feature: Cargar historial de WhatsApp desde la UI
All checks were successful
build-and-deploy / build-and-deploy (push) Successful in 1m5s
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:
@@ -4,24 +4,79 @@
|
|||||||
<div class="space-y-4">
|
<div class="space-y-4">
|
||||||
<h3 class="text-lg font-medium text-[var(--wa-text)]">Solicitar Historial de Mensajes</h3>
|
<h3 class="text-lg font-medium text-[var(--wa-text)]">Solicitar Historial de Mensajes</h3>
|
||||||
<p class="text-sm text-[var(--wa-text-muted)]">
|
<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>
|
</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
|
<UInput
|
||||||
v-model.number="count"
|
v-model.number="count"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Cantidad de mensajes"
|
placeholder="Cantidad de mensajes"
|
||||||
:min="1"
|
:min="1"
|
||||||
:max="1000"
|
: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
|
<UInput
|
||||||
v-model.number="oldestMsgTimestamp"
|
v-model.number="manualTimestamp"
|
||||||
type="number"
|
type="number"
|
||||||
placeholder="Timestamp mas antiguo (opcional)"
|
placeholder="Timestamp (segundos)"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<div class="space-y-2">
|
<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
|
<UTextarea
|
||||||
v-model="oldestMsgKeyJson"
|
v-model="oldestMsgKeyJson"
|
||||||
placeholder='{"remoteJid": "...", "id": "...", "fromMe": false}'
|
placeholder='{"remoteJid": "...", "id": "...", "fromMe": false}'
|
||||||
@@ -29,18 +84,25 @@
|
|||||||
class="font-mono text-sm"
|
class="font-mono text-sm"
|
||||||
/>
|
/>
|
||||||
</div>
|
</div>
|
||||||
<UButton
|
</div>
|
||||||
:loading="loading"
|
|
||||||
:disabled="!instanceId || !count"
|
|
||||||
@click="fetchHistory"
|
|
||||||
>
|
|
||||||
Solicitar Historial
|
|
||||||
</UButton>
|
|
||||||
</div>
|
</div>
|
||||||
</div>
|
</div>
|
||||||
</template>
|
</template>
|
||||||
|
|
||||||
<script setup lang="ts">
|
<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<{
|
const props = defineProps<{
|
||||||
instanceId: string | null
|
instanceId: string | null
|
||||||
}>()
|
}>()
|
||||||
@@ -49,15 +111,90 @@ const emit = defineEmits<{
|
|||||||
(e: 'response', data: any): void
|
(e: 'response', data: any): void
|
||||||
}>()
|
}>()
|
||||||
|
|
||||||
const count = ref<number>(50)
|
const count = ref<number>(100)
|
||||||
const oldestMsgTimestamp = ref<number | null>(null)
|
|
||||||
const oldestMsgKeyJson = ref('')
|
|
||||||
const loading = ref(false)
|
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 () => {
|
const fetchHistory = async () => {
|
||||||
loading.value = true
|
loading.value = true
|
||||||
try {
|
try {
|
||||||
let oldestMsgKey = undefined
|
let oldestMsgKey = undefined
|
||||||
|
let oldestMsgTimestamp = undefined
|
||||||
|
|
||||||
|
if (showAdvanced.value) {
|
||||||
|
// Use manual JSON input
|
||||||
if (oldestMsgKeyJson.value.trim()) {
|
if (oldestMsgKeyJson.value.trim()) {
|
||||||
try {
|
try {
|
||||||
oldestMsgKey = JSON.parse(oldestMsgKeyJson.value)
|
oldestMsgKey = JSON.parse(oldestMsgKeyJson.value)
|
||||||
@@ -67,6 +204,12 @@ const fetchHistory = async () => {
|
|||||||
return
|
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', {
|
const result = await $fetch('/api/debug/history/fetch', {
|
||||||
method: 'POST',
|
method: 'POST',
|
||||||
@@ -74,7 +217,7 @@ const fetchHistory = async () => {
|
|||||||
instanceId: props.instanceId,
|
instanceId: props.instanceId,
|
||||||
count: count.value,
|
count: count.value,
|
||||||
oldestMsgKey,
|
oldestMsgKey,
|
||||||
oldestMsgTimestamp: oldestMsgTimestamp.value || undefined
|
oldestMsgTimestamp
|
||||||
}
|
}
|
||||||
})
|
})
|
||||||
emit('response', result)
|
emit('response', result)
|
||||||
|
|||||||
@@ -188,6 +188,32 @@
|
|||||||
</button>
|
</button>
|
||||||
</div>
|
</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
|
<MessagesMessageBubble
|
||||||
v-for="message in reversedMessages"
|
v-for="message in reversedMessages"
|
||||||
:key="message.id"
|
:key="message.id"
|
||||||
@@ -244,6 +270,7 @@ const messages = ref<any[]>([])
|
|||||||
const loadingChats = ref(false)
|
const loadingChats = ref(false)
|
||||||
const loadingMessages = ref(false)
|
const loadingMessages = ref(false)
|
||||||
const loadingMore = ref(false)
|
const loadingMore = ref(false)
|
||||||
|
const loadingWhatsAppHistory = ref(false)
|
||||||
const hasMoreMessages = ref(true)
|
const hasMoreMessages = ref(true)
|
||||||
const showDebugPanel = ref(false)
|
const showDebugPanel = ref(false)
|
||||||
const showChatsDebug = 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
|
// Computed presence text for current chat
|
||||||
const currentChatPresence = computed(() => {
|
const currentChatPresence = computed(() => {
|
||||||
if (!selectedChat.value?.jid) return null
|
if (!selectedChat.value?.jid) return null
|
||||||
|
|||||||
65
server/api/messages/[instanceId]/[chatId]/oldest.get.ts
Normal file
65
server/api/messages/[instanceId]/[chatId]/oldest.get.ts
Normal 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)
|
||||||
|
}
|
||||||
|
})
|
||||||
Reference in New Issue
Block a user