Refactor caching and Telegram integration

- Updated configuration to enforce Redis caching for notified articles, removing memory cache options.
- Enhanced wallamonitor.py to load Redis cache settings and handle errors more effectively.
- Implemented new API endpoints for clearing Redis cache and retrieving Telegram forum topics.
- Improved frontend components to support fetching and displaying available Telegram threads.
- Added functionality for clearing cache from the UI, ensuring better management of notified articles.
This commit is contained in:
Omar Sánchez Pizarro
2026-01-19 21:24:46 +01:00
parent 96db30ff00
commit 5cc96a2371
7 changed files with 382 additions and 88 deletions

View File

@@ -327,6 +327,51 @@ app.delete('/api/favorites/:platform/:id', async (req, res) => {
}
});
// Limpiar toda la caché de Redis
app.delete('/api/cache', async (req, res) => {
try {
if (!redisClient) {
return res.status(500).json({ error: 'Redis no está disponible' });
}
// Obtener todas las claves que empiezan con 'notified:'
const keys = await redisClient.keys('notified:*');
if (!keys || keys.length === 0) {
return res.json({
success: true,
message: 'Cache ya está vacío',
count: 0
});
}
// Eliminar todas las claves
const count = keys.length;
for (const key of keys) {
await redisClient.del(key);
}
// Notificar a los clientes WebSocket
broadcast({
type: 'cache_cleared',
data: { count, timestamp: Date.now() }
});
// También actualizar favoritos (debería estar vacío ahora)
const favorites = await getFavorites();
broadcast({ type: 'favorites_updated', data: favorites });
res.json({
success: true,
message: `Cache limpiado: ${count} artículos eliminados`,
count
});
} catch (error) {
console.error('Error limpiando cache de Redis:', error);
res.status(500).json({ error: error.message });
}
});
// Obtener artículos notificados
app.get('/api/articles', async (req, res) => {
try {
@@ -454,6 +499,64 @@ app.get('/api/config', (req, res) => {
}
});
// Obtener threads/topics de Telegram
app.get('/api/telegram/threads', async (req, res) => {
try {
if (!config) {
config = yaml.parse(readFileSync(CONFIG_PATH, 'utf8'));
}
const token = config?.telegram_token;
const channel = config?.telegram_channel;
if (!token || !channel) {
return res.status(400).json({ error: 'Token o canal de Telegram no configurados' });
}
// Convertir el canal a chat_id si es necesario
let chatId = channel;
if (channel.startsWith('@')) {
// Para canales con @, necesitamos obtener el chat_id primero
const getChatUrl = `https://api.telegram.org/bot${token}/getChat?chat_id=${encodeURIComponent(channel)}`;
const chatResponse = await fetch(getChatUrl);
const chatData = await chatResponse.json();
if (!chatData.ok) {
return res.status(400).json({ error: `Error obteniendo chat: ${chatData.description || 'Chat no encontrado'}` });
}
chatId = chatData.result.id;
}
// Intentar obtener forum topics
const forumTopicsUrl = `https://api.telegram.org/bot${token}/getForumTopics?chat_id=${chatId}&limit=100`;
const topicsResponse = await fetch(forumTopicsUrl);
const topicsData = await topicsResponse.json();
if (topicsData.ok && topicsData.result?.topics) {
const threads = topicsData.result.topics.map(topic => ({
id: topic.message_thread_id,
name: topic.name || `Thread ${topic.message_thread_id}`,
icon_color: topic.icon_color,
icon_custom_emoji_id: topic.icon_custom_emoji_id,
}));
return res.json({ threads, success: true });
} else {
// Si no hay forum topics, devolver un mensaje informativo
return res.json({
threads: [],
success: false,
message: 'El chat no tiene forum topics habilitados o no se pudieron obtener. Puedes obtener el Thread ID manualmente copiando el enlace del tema.',
info: 'Para obtener el Thread ID manualmente: 1. Haz clic derecho en el tema/hilo en Telegram 2. Selecciona "Copiar enlace del tema" 3. El número al final de la URL es el Thread ID (ej: t.me/c/1234567890/8 → Thread ID = 8)'
});
}
} catch (error) {
console.error('Error obteniendo threads de Telegram:', error.message);
res.status(500).json({ error: error.message });
}
});
// WebSocket connection
wss.on('connection', (ws) => {
console.log('Cliente WebSocket conectado');

View File

@@ -69,5 +69,17 @@ export default {
const response = await api.get('/config');
return response.data;
},
// Telegram
async getTelegramThreads() {
const response = await api.get('/telegram/threads');
return response.data;
},
// Cache
async clearCache() {
const response = await api.delete('/cache');
return response.data;
},
};

View File

@@ -6,6 +6,9 @@
<button @click="showGeneralModal = true" class="btn btn-secondary">
Configuración General
</button>
<button @click="handleClearCache" class="btn btn-secondary">
🗑 Limpiar Caché
</button>
<button @click="showAddModal = true" class="btn btn-primary">
+ Añadir Worker
</button>
@@ -226,8 +229,47 @@
</div>
<div>
<label class="block text-sm font-medium text-gray-700 mb-1">Thread ID (Telegram)</label>
<input v-model.number="workerForm.thread_id" type="number" class="input" />
<div class="flex gap-2">
<input v-model.number="workerForm.thread_id" type="number" class="input flex-1" placeholder="Ej: 8" />
<button
type="button"
@click="loadTelegramThreads"
:disabled="loadingThreads"
class="btn btn-secondary text-sm whitespace-nowrap"
>
{{ loadingThreads ? 'Cargando...' : '📋 Obtener Threads' }}
</button>
</div>
<p class="text-xs text-gray-500 mt-1">Opcional: ID del hilo donde enviar notificaciones</p>
<!-- Lista de threads disponibles -->
<div v-if="availableThreads.length > 0" class="mt-2 p-2 bg-gray-50 rounded border border-gray-200 max-h-40 overflow-y-auto">
<p class="text-xs font-medium text-gray-700 mb-2">Threads disponibles:</p>
<div
v-for="thread in availableThreads"
:key="thread.id"
@click="selectThread(thread.id)"
class="flex items-center justify-between p-2 mb-1 bg-white rounded border border-gray-200 cursor-pointer hover:bg-gray-50 transition-colors"
>
<div class="flex-1">
<span class="text-sm font-medium text-gray-900">{{ thread.name }}</span>
<span class="text-xs text-gray-500 ml-2">ID: {{ thread.id }}</span>
</div>
<button
type="button"
@click.stop="selectThread(thread.id)"
class="text-xs text-primary-600 hover:text-primary-700 font-medium"
>
Usar
</button>
</div>
</div>
<!-- Mensaje informativo si no hay threads -->
<div v-if="threadsMessage" class="mt-2 p-2 bg-blue-50 border border-blue-200 rounded">
<p class="text-xs text-blue-800">{{ threadsMessage }}</p>
<p v-if="threadsInfo" class="text-xs text-blue-700 mt-1">{{ threadsInfo }}</p>
</div>
</div>
</div>
</div>
@@ -458,6 +500,11 @@ const generalForm = ref({
description_exclude_text: '',
});
const availableThreads = ref([]);
const loadingThreads = ref(false);
const threadsMessage = ref('');
const threadsInfo = ref('');
async function loadWorkers() {
loading.value = true;
try {
@@ -474,6 +521,40 @@ async function loadWorkers() {
}
}
async function loadTelegramThreads() {
loadingThreads.value = true;
availableThreads.value = [];
threadsMessage.value = '';
threadsInfo.value = '';
try {
const result = await api.getTelegramThreads();
if (result.success && result.threads && result.threads.length > 0) {
availableThreads.value = result.threads;
threadsMessage.value = '';
threadsInfo.value = '';
} else {
availableThreads.value = [];
threadsMessage.value = result.message || 'No se pudieron obtener los threads automáticamente';
threadsInfo.value = result.info || '';
}
} catch (error) {
console.error('Error cargando threads de Telegram:', error);
availableThreads.value = [];
threadsMessage.value = 'Error al obtener threads de Telegram. Verifica que el bot y el canal estén configurados correctamente.';
threadsInfo.value = 'Para obtener el Thread ID manualmente: 1. Haz clic derecho en el tema/hilo en Telegram 2. Selecciona "Copiar enlace del tema" 3. El número al final de la URL es el Thread ID (ej: t.me/c/1234567890/8 → Thread ID = 8)';
} finally {
loadingThreads.value = false;
}
}
function selectThread(threadId) {
workerForm.value.thread_id = threadId;
// Opcional: limpiar la lista después de seleccionar
// availableThreads.value = [];
}
function editWorker(worker, index) {
editingWorker.value = { worker, index };
workerForm.value = {
@@ -649,6 +730,23 @@ async function deleteWorker(name) {
}
}
async function handleClearCache() {
if (!confirm('¿Estás seguro de que quieres limpiar toda la caché de Redis?\n\nEsto eliminará todos los artículos notificados de todas las instancias. Esta acción no se puede deshacer.')) {
return;
}
try {
const result = await api.clearCache();
const message = result.count > 0
? `✓ Caché limpiada exitosamente: ${result.count} artículos eliminados`
: 'La caché ya estaba vacía';
alert(message);
} catch (error) {
console.error('Error limpiando caché:', error);
alert('Error al limpiar la caché: ' + (error.response?.data?.error || error.message));
}
}
function handleWSMessage(event) {
const data = event.detail;
if (data.type === 'workers_updated') {