1389 lines
44 KiB
JavaScript
1389 lines
44 KiB
JavaScript
import { MongoClient } from 'mongodb';
|
||
import yaml from 'yaml';
|
||
import { readFileSync, existsSync } from 'fs';
|
||
import bcrypt from 'bcrypt';
|
||
import { RateLimiterMemory } from 'rate-limiter-flexible';
|
||
import { PATHS } from '../config/constants.js';
|
||
import { RATE_LIMIT } from '../config/constants.js';
|
||
import { readJSON } from '../utils/fileUtils.js';
|
||
|
||
let mongoClient = null;
|
||
let db = null;
|
||
let rateLimiter = null;
|
||
let config = null;
|
||
|
||
// Duración de sesión en milisegundos (24 horas)
|
||
const SESSION_DURATION = 24 * 60 * 60 * 1000;
|
||
|
||
// Inicializar MongoDB si está configurado
|
||
export async function initMongoDB() {
|
||
try {
|
||
config = yaml.parse(readFileSync(PATHS.CONFIG, 'utf8'));
|
||
const cacheConfig = config?.cache;
|
||
|
||
if (cacheConfig?.type === 'mongodb') {
|
||
const mongodbConfig = cacheConfig.mongodb;
|
||
// En Docker, usar el nombre del servicio si no se especifica host
|
||
const mongodbHost = process.env.MONGODB_HOST || mongodbConfig.host || 'localhost';
|
||
const mongodbPort = process.env.MONGODB_PORT || mongodbConfig.port || 27017;
|
||
const database = process.env.MONGODB_DATABASE || mongodbConfig.database || 'wallabicher';
|
||
const username = process.env.MONGODB_USERNAME || mongodbConfig.username;
|
||
const password = process.env.MONGODB_PASSWORD || mongodbConfig.password;
|
||
const authSource = mongodbConfig.auth_source || 'admin';
|
||
|
||
// Construir URL de conexión
|
||
let connectionString = 'mongodb://';
|
||
if (username && password) {
|
||
connectionString += `${encodeURIComponent(username)}:${encodeURIComponent(password)}@`;
|
||
}
|
||
connectionString += `${mongodbHost}:${mongodbPort}`;
|
||
if (username && password) {
|
||
connectionString += `/?authSource=${authSource}`;
|
||
}
|
||
|
||
mongoClient = new MongoClient(connectionString);
|
||
await mongoClient.connect();
|
||
db = mongoClient.db(database);
|
||
|
||
console.log(`✅ Conectado a MongoDB (${database})`);
|
||
|
||
// Crear índices
|
||
await createIndexes();
|
||
|
||
// Inicializar rate limiter con memoria (MongoDB no tiene rate limiter nativo, usar memoria)
|
||
try {
|
||
rateLimiter = new RateLimiterMemory({
|
||
points: RATE_LIMIT.POINTS,
|
||
duration: RATE_LIMIT.DURATION,
|
||
blockDuration: RATE_LIMIT.BLOCK_DURATION,
|
||
});
|
||
console.log('✅ Rate limiter inicializado con memoria');
|
||
} catch (error) {
|
||
console.error('Error inicializando rate limiter:', error.message);
|
||
}
|
||
|
||
// Inicializar usuario admin por defecto si no existe
|
||
await initDefaultAdmin();
|
||
|
||
// Migrar workers.json a MongoDB para admin si no existe
|
||
await migrateWorkersFromFile();
|
||
|
||
} else {
|
||
console.log('ℹ️ MongoDB no configurado, usando modo memoria');
|
||
console.log('⚠️ Rate limiting y autenticación requieren MongoDB');
|
||
}
|
||
} catch (error) {
|
||
console.error('Error inicializando MongoDB:', error.message);
|
||
}
|
||
}
|
||
|
||
// Crear índices necesarios
|
||
async function createIndexes() {
|
||
if (!db) return;
|
||
|
||
try {
|
||
// Índices para usuarios
|
||
await db.collection('users').createIndex({ username: 1 }, { unique: true });
|
||
|
||
// Índices para sesiones (con TTL)
|
||
await db.collection('sessions').createIndex({ token: 1 }, { unique: true });
|
||
await db.collection('sessions').createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
|
||
await db.collection('sessions').createIndex({ username: 1, fingerprint: 1 });
|
||
await db.collection('sessions').createIndex({ fingerprint: 1 });
|
||
|
||
// Índices para workers
|
||
await db.collection('workers').createIndex({ username: 1 });
|
||
|
||
// Índices para artículos notificados
|
||
await db.collection('articles').createIndex({ platform: 1, id: 1 }, { unique: true });
|
||
await db.collection('articles').createIndex({ 'user_info.username': 1 });
|
||
await db.collection('articles').createIndex({ 'user_info.worker_name': 1 });
|
||
await db.collection('articles').createIndex({ 'user_info.is_favorite': 1 });
|
||
// Índices para ordenamiento (NO usar user_info.notified_at porque es un array)
|
||
await db.collection('articles').createIndex({ createdAt: -1 });
|
||
await db.collection('articles').createIndex({ modified_at: -1 });
|
||
await db.collection('articles').createIndex({ expiresAt: 1 }, { expireAfterSeconds: 0 });
|
||
// Índices de compatibilidad con estructura antigua
|
||
await db.collection('articles').createIndex({ username: 1 });
|
||
await db.collection('articles').createIndex({ worker_name: 1 });
|
||
|
||
// Índices para búsqueda eficiente por palabras (regex case-insensitive)
|
||
// Estos índices mejoran el rendimiento de búsquedas regex en estos campos
|
||
try {
|
||
await db.collection('articles').createIndex({ title: 1 });
|
||
await db.collection('articles').createIndex({ description: 1 });
|
||
await db.collection('articles').createIndex({ location: 1 });
|
||
await db.collection('articles').createIndex({ price: 1 });
|
||
console.log('✅ Índices para búsqueda por palabras creados');
|
||
} catch (error) {
|
||
// Si los índices ya existen, ignorar el error
|
||
if (error.code !== 85 && error.code !== 86) {
|
||
console.error('Error creando índices de búsqueda:', error.message);
|
||
}
|
||
}
|
||
|
||
// Índice de texto para búsqueda eficiente por palabras (opcional, para búsqueda de texto completo)
|
||
// Nota: MongoDB solo permite un índice de texto por colección
|
||
try {
|
||
await db.collection('articles').createIndex(
|
||
{
|
||
title: 'text',
|
||
description: 'text',
|
||
location: 'text',
|
||
platform: 'text'
|
||
},
|
||
{
|
||
name: 'articles_text_search',
|
||
weights: {
|
||
title: 10,
|
||
description: 5,
|
||
location: 3,
|
||
platform: 2
|
||
},
|
||
default_language: 'spanish'
|
||
}
|
||
);
|
||
console.log('✅ Índice de texto para búsqueda creado');
|
||
} catch (error) {
|
||
// Si el índice ya existe, ignorar el error
|
||
if (error.code !== 85 && error.code !== 86) {
|
||
console.error('Error creando índice de texto:', error.message);
|
||
}
|
||
}
|
||
|
||
console.log('✅ Índices de MongoDB creados');
|
||
} catch (error) {
|
||
console.error('Error creando índices de MongoDB:', error.message);
|
||
}
|
||
}
|
||
|
||
// Inicializar usuario admin por defecto
|
||
async function initDefaultAdmin() {
|
||
if (!db) return;
|
||
|
||
try {
|
||
const usersCollection = db.collection('users');
|
||
const adminExists = await usersCollection.findOne({ username: 'admin' });
|
||
|
||
if (!adminExists) {
|
||
// Crear usuario admin por defecto con contraseña "admin"
|
||
// En producción, esto debería cambiarse
|
||
const defaultPassword = 'admin';
|
||
const hashedPassword = await bcrypt.hash(defaultPassword, 10);
|
||
await usersCollection.insertOne({
|
||
username: 'admin',
|
||
passwordHash: hashedPassword,
|
||
role: 'admin',
|
||
createdAt: new Date(),
|
||
});
|
||
console.log('✅ Usuario admin creado por defecto (usuario: admin, contraseña: admin)');
|
||
console.log('⚠️ IMPORTANTE: Cambia la contraseña por defecto en producción');
|
||
} else {
|
||
// Asegurar que el usuario admin tiene el rol correcto (para usuarios existentes)
|
||
await usersCollection.updateOne(
|
||
{ username: 'admin' },
|
||
{ $set: { role: 'admin' } }
|
||
);
|
||
}
|
||
} catch (error) {
|
||
console.error('Error inicializando usuario admin:', error.message);
|
||
}
|
||
}
|
||
|
||
// Migrar workers.json a MongoDB para el usuario admin si no existe
|
||
async function migrateWorkersFromFile() {
|
||
if (!db) return;
|
||
|
||
try {
|
||
const workersCollection = db.collection('workers');
|
||
const adminWorkers = await workersCollection.findOne({ username: 'admin' });
|
||
|
||
// Si ya existen workers para admin en MongoDB, no migrar
|
||
if (adminWorkers) {
|
||
console.log('ℹ️ Workers de admin ya existen en MongoDB, omitiendo migración');
|
||
return;
|
||
}
|
||
|
||
// Intentar leer workers.json
|
||
if (!existsSync(PATHS.WORKERS)) {
|
||
console.log('ℹ️ workers.json no existe, creando estructura vacía para admin');
|
||
// Crear estructura vacía por defecto
|
||
const defaultWorkers = {
|
||
username: 'admin',
|
||
general: {
|
||
title_exclude: [],
|
||
description_exclude: []
|
||
},
|
||
items: [],
|
||
disabled: [],
|
||
updatedAt: new Date(),
|
||
createdAt: new Date()
|
||
};
|
||
await workersCollection.insertOne(defaultWorkers);
|
||
return;
|
||
}
|
||
|
||
// Leer workers.json y migrar a MongoDB
|
||
const workersData = readJSON(PATHS.WORKERS, {
|
||
general: {
|
||
title_exclude: [],
|
||
description_exclude: []
|
||
},
|
||
items: [],
|
||
disabled: []
|
||
});
|
||
|
||
// Guardar en MongoDB para admin
|
||
await workersCollection.insertOne({
|
||
username: 'admin',
|
||
...workersData,
|
||
updatedAt: new Date(),
|
||
createdAt: new Date()
|
||
});
|
||
console.log(`✅ Workers migrados desde workers.json al usuario admin (${workersData.items?.length || 0} items)`);
|
||
|
||
} catch (error) {
|
||
console.error('Error migrando workers.json a MongoDB:', error.message);
|
||
// No lanzar error, solo registrar
|
||
}
|
||
}
|
||
|
||
// Getters
|
||
export function getMongoDBClient() {
|
||
return mongoClient;
|
||
}
|
||
|
||
export function getDB() {
|
||
return db;
|
||
}
|
||
|
||
export function getRateLimiter() {
|
||
return rateLimiter;
|
||
}
|
||
|
||
// Obtener información del rate limiter y bloqueos (para memoria)
|
||
export async function getRateLimiterInfo() {
|
||
if (!rateLimiter) {
|
||
return {
|
||
enabled: false,
|
||
type: 'none',
|
||
blocks: [],
|
||
stats: {
|
||
totalBlocks: 0,
|
||
activeBlocks: 0,
|
||
},
|
||
};
|
||
}
|
||
|
||
try {
|
||
// RateLimiterMemory no expone directamente las claves bloqueadas
|
||
// Necesitamos usar una aproximación diferente
|
||
// Por ahora, retornamos información básica
|
||
return {
|
||
enabled: true,
|
||
type: 'memory',
|
||
blocks: [],
|
||
stats: {
|
||
totalBlocks: 0,
|
||
activeBlocks: 0,
|
||
},
|
||
config: {
|
||
points: RATE_LIMIT.POINTS,
|
||
duration: RATE_LIMIT.DURATION,
|
||
blockDuration: RATE_LIMIT.BLOCK_DURATION,
|
||
},
|
||
note: 'Rate limiter en memoria no expone información detallada de bloqueos',
|
||
};
|
||
} catch (error) {
|
||
console.error('Error obteniendo información del rate limiter:', error.message);
|
||
return {
|
||
enabled: true,
|
||
type: 'memory',
|
||
blocks: [],
|
||
stats: {
|
||
totalBlocks: 0,
|
||
activeBlocks: 0,
|
||
},
|
||
error: error.message,
|
||
};
|
||
}
|
||
}
|
||
|
||
export function getConfig() {
|
||
return config;
|
||
}
|
||
|
||
export function reloadConfig() {
|
||
try {
|
||
config = yaml.parse(readFileSync(PATHS.CONFIG, 'utf8'));
|
||
return config;
|
||
} catch (error) {
|
||
console.error('Error recargando configuración:', error.message);
|
||
return config;
|
||
}
|
||
}
|
||
|
||
// Funciones de utilidad para artículos
|
||
export async function getNotifiedArticles(filter = {}) {
|
||
if (!db) {
|
||
return [];
|
||
}
|
||
|
||
try {
|
||
const articlesCollection = db.collection('articles');
|
||
|
||
// Construir query de filtro
|
||
const query = {};
|
||
if (filter.platform) query.platform = filter.platform;
|
||
|
||
// Si se especifica username, buscar en user_info
|
||
if (filter.username) {
|
||
query['user_info.username'] = filter.username;
|
||
}
|
||
|
||
// Si se especifica worker_name, buscar en user_info
|
||
if (filter.worker_name) {
|
||
query['user_info.worker_name'] = filter.worker_name;
|
||
}
|
||
|
||
// Ordenar SOLO por campos a nivel de artículo, NO por user_info.notified_at
|
||
// porque user_info es un array y puede causar orden incorrecto
|
||
const articles = await articlesCollection
|
||
.find(query)
|
||
.sort({ createdAt: -1, modified_at: -1 })
|
||
.toArray();
|
||
|
||
// Obtener el username del usuario actual para incluir su is_favorite
|
||
const currentUsername = filter.currentUsername || filter.username;
|
||
|
||
// Filtrar y transformar artículos según el usuario solicitado
|
||
return articles.map(article => {
|
||
// Si hay filtro de username, solo devolver el user_info correspondiente
|
||
let relevantUserInfo = null;
|
||
if (filter.username) {
|
||
relevantUserInfo = (article.user_info || []).find(
|
||
ui => ui.username === filter.username
|
||
);
|
||
// Si no hay user_info para este usuario, no incluir el artículo
|
||
if (!relevantUserInfo) return null;
|
||
} else if (filter.worker_name) {
|
||
// Si solo hay filtro de worker, buscar el primer user_info con ese worker
|
||
relevantUserInfo = (article.user_info || []).find(
|
||
ui => ui.worker_name === filter.worker_name
|
||
);
|
||
if (!relevantUserInfo) return null;
|
||
}
|
||
|
||
// Construir el artículo con la información relevante
|
||
const result = {
|
||
...article,
|
||
_id: article._id.toString(),
|
||
expiresAt: article.expiresAt?.getTime() || null,
|
||
};
|
||
|
||
// Si hay un user_info específico, usar sus datos
|
||
const fallbackTime =
|
||
article.createdAt?.getTime?.() ||
|
||
(typeof article.createdAt === 'number' ? article.createdAt : null) ||
|
||
article.modified_at?.getTime?.() ||
|
||
null;
|
||
|
||
if (relevantUserInfo) {
|
||
result.username = relevantUserInfo.username;
|
||
result.worker_name = relevantUserInfo.worker_name;
|
||
result.is_favorite = relevantUserInfo.is_favorite || false;
|
||
// NO usar Date.now() como fallback, para no mover artículos antiguos
|
||
result.notifiedAt =
|
||
relevantUserInfo.notified_at?.getTime?.() ||
|
||
(typeof relevantUserInfo.notified_at === 'number' ? relevantUserInfo.notified_at : null) ||
|
||
fallbackTime;
|
||
} else {
|
||
// Sin filtro específico, mostrar el primer user_info o datos generales
|
||
const firstUserInfo = (article.user_info || [])[0];
|
||
if (firstUserInfo) {
|
||
result.username = firstUserInfo.username;
|
||
result.worker_name = firstUserInfo.worker_name;
|
||
// Si hay un usuario actual, buscar su is_favorite específico, sino usar el del primer user_info
|
||
if (currentUsername) {
|
||
const currentUserInfo = (article.user_info || []).find(
|
||
ui => ui.username === currentUsername
|
||
);
|
||
result.is_favorite = currentUserInfo ? (currentUserInfo.is_favorite || false) : false;
|
||
} else {
|
||
result.is_favorite = firstUserInfo.is_favorite || false;
|
||
}
|
||
result.notifiedAt =
|
||
firstUserInfo.notified_at?.getTime?.() ||
|
||
(typeof firstUserInfo.notified_at === 'number' ? firstUserInfo.notified_at : null) ||
|
||
fallbackTime;
|
||
} else {
|
||
// Compatibilidad con estructura antigua
|
||
result.username = article.username;
|
||
result.worker_name = article.worker_name;
|
||
result.is_favorite = article.is_favorite || false;
|
||
result.notifiedAt =
|
||
article.notifiedAt?.getTime?.() ||
|
||
(typeof article.notifiedAt === 'number' ? article.notifiedAt : null) ||
|
||
fallbackTime;
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}).filter(article => article !== null);
|
||
} catch (error) {
|
||
console.error('Error obteniendo artículos de MongoDB:', error.message);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
export async function getArticleFacets(usernameFilter = null) {
|
||
if (!db) {
|
||
return {
|
||
platforms: [],
|
||
usernames: [],
|
||
workers: []
|
||
};
|
||
}
|
||
|
||
try {
|
||
const articlesCollection = db.collection('articles');
|
||
|
||
// Construir query base
|
||
const query = {};
|
||
if (usernameFilter) {
|
||
// Si hay filtro de username, solo buscar artículos de ese usuario
|
||
query['user_info.username'] = usernameFilter;
|
||
}
|
||
|
||
// Obtener todos los artículos que coincidan con el filtro
|
||
const articles = await articlesCollection.find(query).toArray();
|
||
|
||
// Extraer valores únicos
|
||
const platformsSet = new Set();
|
||
const usernamesSet = new Set();
|
||
const workersSet = new Set();
|
||
|
||
for (const article of articles) {
|
||
// Plataforma (campo directo del artículo)
|
||
if (article.platform) {
|
||
platformsSet.add(article.platform);
|
||
}
|
||
|
||
// Username y worker_name (pueden estar en user_info o en campos directos para compatibilidad)
|
||
const userInfoList = article.user_info || [];
|
||
|
||
if (userInfoList.length > 0) {
|
||
// Estructura nueva: usar user_info
|
||
for (const userInfo of userInfoList) {
|
||
if (userInfo.username) {
|
||
usernamesSet.add(userInfo.username);
|
||
}
|
||
if (userInfo.worker_name) {
|
||
workersSet.add(userInfo.worker_name);
|
||
}
|
||
}
|
||
} else {
|
||
// Estructura antigua: usar campos directos
|
||
if (article.username) {
|
||
usernamesSet.add(article.username);
|
||
}
|
||
if (article.worker_name) {
|
||
workersSet.add(article.worker_name);
|
||
}
|
||
}
|
||
}
|
||
|
||
return {
|
||
platforms: Array.from(platformsSet).sort(),
|
||
usernames: Array.from(usernamesSet).sort(),
|
||
workers: Array.from(workersSet).sort()
|
||
};
|
||
} catch (error) {
|
||
console.error('Error obteniendo facets de artículos:', error.message);
|
||
return {
|
||
platforms: [],
|
||
usernames: [],
|
||
workers: []
|
||
};
|
||
}
|
||
}
|
||
|
||
// Buscar artículos por texto de forma eficiente usando índices de MongoDB
|
||
// La búsqueda es por palabras con soporte para modo AND u OR
|
||
// AND: todas las palabras deben estar presentes
|
||
// OR: al menos una palabra debe estar presente
|
||
export async function searchArticles(searchQuery, filter = {}, searchMode = 'AND') {
|
||
if (!db) {
|
||
return [];
|
||
}
|
||
|
||
try {
|
||
const articlesCollection = db.collection('articles');
|
||
|
||
// Limpiar y dividir la consulta en palabras
|
||
const queryWords = searchQuery
|
||
.trim()
|
||
.split(/\s+/)
|
||
.filter(word => word.length > 0)
|
||
.map(word => word.replace(/[.*+?^${}()|[\]\\]/g, '\\$&')); // Escapar caracteres especiales de regex
|
||
|
||
if (queryWords.length === 0) {
|
||
return [];
|
||
}
|
||
|
||
// Construir query base con filtros
|
||
const query = {};
|
||
|
||
// Aplicar filtros de usuario y worker
|
||
if (filter.username) {
|
||
query['user_info.username'] = filter.username;
|
||
}
|
||
if (filter.worker_name) {
|
||
query['user_info.worker_name'] = filter.worker_name;
|
||
}
|
||
if (filter.platform) {
|
||
query.platform = filter.platform;
|
||
}
|
||
|
||
// Campos de texto donde buscar (ordenados por importancia)
|
||
const textFields = ['title', 'description', 'location', 'platform', 'id'];
|
||
|
||
if (searchMode.toUpperCase() === 'OR') {
|
||
// Modo OR: al menos una palabra debe estar presente
|
||
const orConditions = [];
|
||
|
||
// Para cada palabra, crear una condición que busque en todos los campos
|
||
for (const word of queryWords) {
|
||
const wordRegex = new RegExp(word, 'i'); // Case-insensitive
|
||
|
||
// Condiciones para esta palabra en campos de texto principales
|
||
const fieldConditions = textFields.map(field => ({
|
||
[field]: { $regex: wordRegex }
|
||
}));
|
||
|
||
// Buscar en user_info si no hay filtro específico
|
||
if (!filter.username && !filter.worker_name) {
|
||
fieldConditions.push(
|
||
{ 'user_info.username': { $regex: wordRegex } },
|
||
{ 'user_info.worker_name': { $regex: wordRegex } },
|
||
{ username: { $regex: wordRegex } }, // Compatibilidad con estructura antigua
|
||
{ worker_name: { $regex: wordRegex } } // Compatibilidad con estructura antigua
|
||
);
|
||
}
|
||
|
||
// Buscar precio como número (si la palabra es un número)
|
||
const priceNum = parseFloat(word.replace(/[^\d.,]/g, '').replace(',', '.'));
|
||
if (!isNaN(priceNum) && priceNum > 0) {
|
||
// Buscar precio exacto o cercano (±10%)
|
||
const priceLower = priceNum * 0.9;
|
||
const priceUpper = priceNum * 1.1;
|
||
fieldConditions.push({
|
||
price: { $gte: priceLower, $lte: priceUpper }
|
||
});
|
||
}
|
||
|
||
// Esta palabra debe estar en al menos uno de estos campos
|
||
orConditions.push({ $or: fieldConditions });
|
||
}
|
||
|
||
// Al menos una palabra debe estar presente
|
||
if (orConditions.length > 0) {
|
||
query.$or = orConditions;
|
||
}
|
||
} else {
|
||
// Modo AND (por defecto): todas las palabras deben estar presentes
|
||
const wordConditions = [];
|
||
|
||
// Para cada palabra, crear una condición que busque en todos los campos de texto
|
||
for (const word of queryWords) {
|
||
const wordRegex = new RegExp(word, 'i'); // Case-insensitive
|
||
|
||
// Condiciones para esta palabra en campos de texto principales
|
||
const fieldConditions = textFields.map(field => ({
|
||
[field]: { $regex: wordRegex }
|
||
}));
|
||
|
||
// Buscar en user_info si no hay filtro específico
|
||
if (!filter.username && !filter.worker_name) {
|
||
fieldConditions.push(
|
||
{ 'user_info.username': { $regex: wordRegex } },
|
||
{ 'user_info.worker_name': { $regex: wordRegex } },
|
||
{ username: { $regex: wordRegex } }, // Compatibilidad con estructura antigua
|
||
{ worker_name: { $regex: wordRegex } } // Compatibilidad con estructura antigua
|
||
);
|
||
}
|
||
|
||
// Buscar precio como número (si la palabra es un número)
|
||
const priceNum = parseFloat(word.replace(/[^\d.,]/g, '').replace(',', '.'));
|
||
if (!isNaN(priceNum) && priceNum > 0) {
|
||
// Buscar precio exacto o cercano (±10%)
|
||
const priceLower = priceNum * 0.9;
|
||
const priceUpper = priceNum * 1.1;
|
||
fieldConditions.push({
|
||
price: { $gte: priceLower, $lte: priceUpper }
|
||
});
|
||
}
|
||
|
||
// Esta palabra debe estar en al menos uno de estos campos
|
||
wordConditions.push({ $or: fieldConditions });
|
||
}
|
||
|
||
// Todas las palabras deben estar presentes
|
||
if (wordConditions.length > 0) {
|
||
query.$and = wordConditions;
|
||
}
|
||
}
|
||
|
||
// Ejecutar búsqueda con ordenamiento
|
||
const articles = await articlesCollection
|
||
.find(query)
|
||
.sort({ createdAt: -1, modified_at: -1 })
|
||
.toArray();
|
||
|
||
// Obtener el username del usuario actual para incluir su is_favorite
|
||
const currentUsername = filter.currentUsername || filter.username;
|
||
|
||
// Transformar artículos según el usuario solicitado (similar a getNotifiedArticles)
|
||
return articles.map(article => {
|
||
let relevantUserInfo = null;
|
||
if (filter.username) {
|
||
relevantUserInfo = (article.user_info || []).find(
|
||
ui => ui.username === filter.username
|
||
);
|
||
if (!relevantUserInfo) return null;
|
||
} else if (filter.worker_name) {
|
||
relevantUserInfo = (article.user_info || []).find(
|
||
ui => ui.worker_name === filter.worker_name
|
||
);
|
||
if (!relevantUserInfo) return null;
|
||
}
|
||
|
||
const result = {
|
||
...article,
|
||
_id: article._id.toString(),
|
||
expiresAt: article.expiresAt?.getTime() || null,
|
||
};
|
||
|
||
const fallbackTime =
|
||
article.createdAt?.getTime?.() ||
|
||
(typeof article.createdAt === 'number' ? article.createdAt : null) ||
|
||
article.modified_at?.getTime?.() ||
|
||
null;
|
||
|
||
if (relevantUserInfo) {
|
||
result.username = relevantUserInfo.username;
|
||
result.worker_name = relevantUserInfo.worker_name;
|
||
result.is_favorite = relevantUserInfo.is_favorite || false;
|
||
result.notifiedAt =
|
||
relevantUserInfo.notified_at?.getTime?.() ||
|
||
(typeof relevantUserInfo.notified_at === 'number' ? relevantUserInfo.notified_at : null) ||
|
||
fallbackTime;
|
||
} else {
|
||
const firstUserInfo = (article.user_info || [])[0];
|
||
if (firstUserInfo) {
|
||
result.username = firstUserInfo.username;
|
||
result.worker_name = firstUserInfo.worker_name;
|
||
// Si hay un usuario actual, buscar su is_favorite específico, sino usar el del primer user_info
|
||
if (currentUsername) {
|
||
const currentUserInfo = (article.user_info || []).find(
|
||
ui => ui.username === currentUsername
|
||
);
|
||
result.is_favorite = currentUserInfo ? (currentUserInfo.is_favorite || false) : false;
|
||
} else {
|
||
result.is_favorite = firstUserInfo.is_favorite || false;
|
||
}
|
||
result.notifiedAt =
|
||
firstUserInfo.notified_at?.getTime?.() ||
|
||
(typeof firstUserInfo.notified_at === 'number' ? firstUserInfo.notified_at : null) ||
|
||
fallbackTime;
|
||
} else {
|
||
// Compatibilidad con estructura antigua
|
||
result.username = article.username;
|
||
result.worker_name = article.worker_name;
|
||
result.is_favorite = article.is_favorite || false;
|
||
result.notifiedAt =
|
||
article.notifiedAt?.getTime?.() ||
|
||
(typeof article.notifiedAt === 'number' ? article.notifiedAt : null) ||
|
||
fallbackTime;
|
||
}
|
||
}
|
||
|
||
return result;
|
||
}).filter(article => article !== null);
|
||
} catch (error) {
|
||
console.error('Error buscando artículos en MongoDB:', error.message);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
export async function getFavorites(username = null) {
|
||
if (!db) {
|
||
return [];
|
||
}
|
||
|
||
try {
|
||
const articlesCollection = db.collection('articles');
|
||
|
||
// Si se especifica username, buscar solo favoritos de ese usuario
|
||
let query = {};
|
||
if (username) {
|
||
query['user_info.username'] = username;
|
||
query['user_info.is_favorite'] = true;
|
||
} else {
|
||
// Sin username, buscar cualquier artículo con algún favorito
|
||
query['user_info.is_favorite'] = true;
|
||
}
|
||
|
||
const articles = await articlesCollection
|
||
.find(query)
|
||
.sort({ createdAt: -1, modified_at: -1 })
|
||
.toArray();
|
||
|
||
// Filtrar y transformar para devolver solo los favoritos relevantes
|
||
const favorites = [];
|
||
for (const article of articles) {
|
||
const userInfoList = article.user_info || [];
|
||
|
||
if (username) {
|
||
// Solo devolver favoritos del usuario especificado
|
||
const userInfo = userInfoList.find(ui =>
|
||
ui.username === username && ui.is_favorite === true
|
||
);
|
||
if (userInfo) {
|
||
favorites.push({
|
||
...article,
|
||
_id: article._id.toString(),
|
||
username: userInfo.username,
|
||
worker_name: userInfo.worker_name,
|
||
is_favorite: true,
|
||
// NOTA: no usar Date.now() como fallback, para no mover favoritos antiguos
|
||
notifiedAt:
|
||
userInfo.notified_at?.getTime?.() ||
|
||
(typeof userInfo.notified_at === 'number' ? userInfo.notified_at : null) ||
|
||
article.createdAt?.getTime?.() ||
|
||
(typeof article.createdAt === 'number' ? article.createdAt : null) ||
|
||
article.modified_at?.getTime?.() ||
|
||
null,
|
||
expiresAt: article.expiresAt?.getTime() || null,
|
||
});
|
||
}
|
||
} else {
|
||
// Sin filtro de usuario, devolver todos los favoritos (uno por user_info)
|
||
for (const userInfo of userInfoList) {
|
||
if (userInfo.is_favorite === true) {
|
||
favorites.push({
|
||
...article,
|
||
_id: article._id.toString(),
|
||
username: userInfo.username,
|
||
worker_name: userInfo.worker_name,
|
||
is_favorite: true,
|
||
// NOTA: no usar Date.now() como fallback, para no mover favoritos antiguos
|
||
notifiedAt:
|
||
userInfo.notified_at?.getTime?.() ||
|
||
(typeof userInfo.notified_at === 'number' ? userInfo.notified_at : null) ||
|
||
article.createdAt?.getTime?.() ||
|
||
(typeof article.createdAt === 'number' ? article.createdAt : null) ||
|
||
article.modified_at?.getTime?.() ||
|
||
null,
|
||
expiresAt: article.expiresAt?.getTime() || null,
|
||
});
|
||
}
|
||
}
|
||
}
|
||
}
|
||
|
||
return favorites;
|
||
} catch (error) {
|
||
console.error('Error obteniendo favoritos de MongoDB:', error.message);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
// Inicializar claves conocidas para evitar notificar artículos existentes
|
||
export async function initNotifiedArticleKeys() {
|
||
if (!db) {
|
||
return new Set();
|
||
}
|
||
|
||
try {
|
||
const articlesCollection = db.collection('articles');
|
||
const articles = await articlesCollection.find({}, { projection: { platform: 1, id: 1 } }).toArray();
|
||
const keysSet = new Set(articles.map(a => `notified:${a.platform}:${a.id}`));
|
||
console.log(`📋 ${keysSet.size} artículos ya notificados detectados`);
|
||
return keysSet;
|
||
} catch (error) {
|
||
console.error('Error inicializando claves de artículos:', error.message);
|
||
return new Set();
|
||
}
|
||
}
|
||
|
||
// Funciones para manejar workers por usuario
|
||
export async function getWorkers(username) {
|
||
if (!db) {
|
||
throw new Error('MongoDB no está disponible');
|
||
}
|
||
|
||
try {
|
||
const workersCollection = db.collection('workers');
|
||
const workersData = await workersCollection.findOne({ username });
|
||
|
||
if (!workersData) {
|
||
// Retornar estructura vacía por defecto
|
||
return {
|
||
general: {
|
||
title_exclude: [],
|
||
description_exclude: []
|
||
},
|
||
items: [],
|
||
disabled: []
|
||
};
|
||
}
|
||
|
||
// Remover campos de MongoDB y devolver solo los datos relevantes
|
||
const { _id, username: _, updatedAt, createdAt, ...data } = workersData;
|
||
return data;
|
||
} catch (error) {
|
||
console.error(`Error obteniendo workers para ${username}:`, error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
export async function setWorkers(username, workers) {
|
||
if (!db) {
|
||
throw new Error('MongoDB no está disponible');
|
||
}
|
||
|
||
try {
|
||
const workersCollection = db.collection('workers');
|
||
|
||
// Usar upsert para insertar o actualizar
|
||
await workersCollection.updateOne(
|
||
{ username },
|
||
{
|
||
$set: {
|
||
...workers,
|
||
username,
|
||
updatedAt: new Date(),
|
||
},
|
||
$setOnInsert: {
|
||
createdAt: new Date(),
|
||
}
|
||
},
|
||
{ upsert: true }
|
||
);
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error(`Error guardando workers para ${username}:`, error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Funciones para usuarios
|
||
export async function getUser(username) {
|
||
if (!db) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
const usersCollection = db.collection('users');
|
||
const user = await usersCollection.findOne({ username });
|
||
// Si el usuario no tiene rol, asignar 'user' por defecto (para usuarios antiguos)
|
||
if (user && !user.role) {
|
||
await usersCollection.updateOne(
|
||
{ username },
|
||
{ $set: { role: username === 'admin' ? 'admin' : 'user' } }
|
||
);
|
||
user.role = username === 'admin' ? 'admin' : 'user';
|
||
}
|
||
return user;
|
||
} catch (error) {
|
||
console.error(`Error obteniendo usuario ${username}:`, error.message);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
export async function createUser(userData) {
|
||
if (!db) {
|
||
throw new Error('MongoDB no está disponible');
|
||
}
|
||
|
||
try {
|
||
const usersCollection = db.collection('users');
|
||
const result = await usersCollection.insertOne({
|
||
...userData,
|
||
role: userData.role || 'user', // Por defecto 'user', a menos que se especifique
|
||
createdAt: new Date(),
|
||
});
|
||
return result.insertedId;
|
||
} catch (error) {
|
||
console.error('Error creando usuario:', error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
export async function deleteUser(username) {
|
||
if (!db) {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
const usersCollection = db.collection('users');
|
||
const result = await usersCollection.deleteOne({ username });
|
||
return result.deletedCount > 0;
|
||
} catch (error) {
|
||
console.error(`Error eliminando usuario ${username}:`, error.message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
export async function getAllUsers(currentUser = null) {
|
||
if (!db) {
|
||
return [];
|
||
}
|
||
|
||
try {
|
||
const usersCollection = db.collection('users');
|
||
|
||
// Si hay un usuario actual, verificar si es admin
|
||
if (currentUser) {
|
||
const currentUserData = await getUser(currentUser); // Usar getUser para asegurar que tiene rol
|
||
// Si es admin, puede ver todos los usuarios
|
||
if (currentUserData && currentUserData.role === 'admin') {
|
||
const users = await usersCollection.find({}, { projection: { passwordHash: 0 } }).toArray();
|
||
return users;
|
||
}
|
||
// Si no es admin, solo puede ver su propio usuario
|
||
const user = await usersCollection.findOne(
|
||
{ username: currentUser },
|
||
{ projection: { passwordHash: 0 } }
|
||
);
|
||
return user ? [user] : [];
|
||
}
|
||
|
||
// Sin usuario actual, devolver todos (compatibilidad)
|
||
const users = await usersCollection.find({}, { projection: { passwordHash: 0 } }).toArray();
|
||
return users;
|
||
} catch (error) {
|
||
console.error('Error obteniendo usuarios:', error.message);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
export async function updateUserPassword(username, passwordHash) {
|
||
if (!db) {
|
||
throw new Error('MongoDB no está disponible');
|
||
}
|
||
|
||
try {
|
||
const usersCollection = db.collection('users');
|
||
await usersCollection.updateOne(
|
||
{ username },
|
||
{ $set: { passwordHash, updatedAt: new Date() } }
|
||
);
|
||
return true;
|
||
} catch (error) {
|
||
console.error(`Error actualizando contraseña de ${username}:`, error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Funciones para configuración de Telegram
|
||
export async function getTelegramConfig(username) {
|
||
if (!db) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
const usersCollection = db.collection('users');
|
||
const user = await usersCollection.findOne({ username });
|
||
|
||
if (user && user.telegram) {
|
||
return {
|
||
token: user.telegram.token || '',
|
||
channel: user.telegram.channel || '',
|
||
enable_polling: user.telegram.enable_polling || false
|
||
};
|
||
}
|
||
|
||
return null;
|
||
} catch (error) {
|
||
console.error(`Error obteniendo configuración de Telegram para ${username}:`, error.message);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
export async function setTelegramConfig(username, telegramConfig) {
|
||
if (!db) {
|
||
throw new Error('MongoDB no está disponible');
|
||
}
|
||
|
||
try {
|
||
const usersCollection = db.collection('users');
|
||
|
||
// Verificar que el usuario existe
|
||
const user = await usersCollection.findOne({ username });
|
||
if (!user) {
|
||
throw new Error(`Usuario ${username} no existe`);
|
||
}
|
||
|
||
// Actualizar configuración de Telegram
|
||
await usersCollection.updateOne(
|
||
{ username },
|
||
{
|
||
$set: {
|
||
telegram: {
|
||
token: telegramConfig.token || '',
|
||
channel: telegramConfig.channel || '',
|
||
enable_polling: telegramConfig.enable_polling || false
|
||
},
|
||
updatedAt: new Date()
|
||
}
|
||
}
|
||
);
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error(`Error guardando configuración de Telegram para ${username}:`, error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Funciones para sesiones
|
||
export async function createSession(username, fingerprint = null, deviceInfo = null) {
|
||
if (!db) {
|
||
throw new Error('MongoDB no está disponible');
|
||
}
|
||
|
||
const crypto = await import('crypto');
|
||
const token = crypto.randomBytes(32).toString('hex');
|
||
const expiresAt = new Date(Date.now() + SESSION_DURATION);
|
||
|
||
try {
|
||
const sessionsCollection = db.collection('sessions');
|
||
|
||
// Si hay fingerprint, eliminar sesiones anteriores del mismo dispositivo/usuario
|
||
if (fingerprint) {
|
||
await sessionsCollection.deleteMany({
|
||
username,
|
||
fingerprint,
|
||
expiresAt: { $gt: new Date() } // Solo eliminar sesiones activas
|
||
});
|
||
}
|
||
|
||
// Crear nueva sesión con fingerprint
|
||
await sessionsCollection.insertOne({
|
||
token,
|
||
username,
|
||
fingerprint: fingerprint || null,
|
||
deviceInfo: deviceInfo || null,
|
||
createdAt: new Date(),
|
||
expiresAt,
|
||
});
|
||
return token;
|
||
} catch (error) {
|
||
console.error('Error creando sesión:', error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
export async function getSession(token) {
|
||
if (!db) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
const sessionsCollection = db.collection('sessions');
|
||
return await sessionsCollection.findOne({ token });
|
||
} catch (error) {
|
||
console.error('Error obteniendo sesión:', error.message);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
export async function deleteSession(token) {
|
||
if (!db) {
|
||
return false;
|
||
}
|
||
|
||
try {
|
||
const sessionsCollection = db.collection('sessions');
|
||
const result = await sessionsCollection.deleteOne({ token });
|
||
return result.deletedCount > 0;
|
||
} catch (error) {
|
||
console.error('Error eliminando sesión:', error.message);
|
||
return false;
|
||
}
|
||
}
|
||
|
||
export async function deleteUserSessions(username) {
|
||
if (!db) {
|
||
return 0;
|
||
}
|
||
|
||
try {
|
||
const sessionsCollection = db.collection('sessions');
|
||
const result = await sessionsCollection.deleteMany({ username });
|
||
return result.deletedCount;
|
||
} catch (error) {
|
||
console.error(`Error eliminando sesiones de ${username}:`, error.message);
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
export async function getAllSessions() {
|
||
if (!db) {
|
||
return [];
|
||
}
|
||
|
||
try {
|
||
const sessionsCollection = db.collection('sessions');
|
||
const sessions = await sessionsCollection.find({}).toArray();
|
||
return sessions.map(session => ({
|
||
token: session.token,
|
||
username: session.username,
|
||
fingerprint: session.fingerprint || null,
|
||
deviceInfo: session.deviceInfo || null,
|
||
createdAt: session.createdAt,
|
||
expiresAt: session.expiresAt,
|
||
isExpired: session.expiresAt ? new Date(session.expiresAt) < new Date() : false,
|
||
}));
|
||
} catch (error) {
|
||
console.error('Error obteniendo todas las sesiones:', error.message);
|
||
return [];
|
||
}
|
||
}
|
||
|
||
// Funciones para artículos
|
||
export async function getArticle(platform, id, currentUsername = null) {
|
||
if (!db) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
const articlesCollection = db.collection('articles');
|
||
const article = await articlesCollection.findOne({ platform, id });
|
||
|
||
if (!article) {
|
||
return null;
|
||
}
|
||
|
||
// Transformar el artículo similar a getNotifiedArticles para incluir is_favorite
|
||
const result = {
|
||
...article,
|
||
_id: article._id.toString(),
|
||
expiresAt: article.expiresAt?.getTime() || null,
|
||
};
|
||
|
||
const fallbackTime =
|
||
article.createdAt?.getTime?.() ||
|
||
(typeof article.createdAt === 'number' ? article.createdAt : null) ||
|
||
article.modified_at?.getTime?.() ||
|
||
null;
|
||
|
||
// Si hay un usuario actual, buscar su is_favorite específico
|
||
if (currentUsername) {
|
||
const currentUserInfo = (article.user_info || []).find(
|
||
ui => ui.username === currentUsername
|
||
);
|
||
if (currentUserInfo) {
|
||
result.is_favorite = currentUserInfo.is_favorite || false;
|
||
result.notifiedAt =
|
||
currentUserInfo.notified_at?.getTime?.() ||
|
||
(typeof currentUserInfo.notified_at === 'number' ? currentUserInfo.notified_at : null) ||
|
||
fallbackTime;
|
||
} else {
|
||
// Si no hay user_info para este usuario, usar false
|
||
result.is_favorite = false;
|
||
}
|
||
} else {
|
||
// Sin usuario específico, usar el primer user_info o datos generales
|
||
const firstUserInfo = (article.user_info || [])[0];
|
||
if (firstUserInfo) {
|
||
result.username = firstUserInfo.username;
|
||
result.worker_name = firstUserInfo.worker_name;
|
||
result.is_favorite = firstUserInfo.is_favorite || false;
|
||
result.notifiedAt =
|
||
firstUserInfo.notified_at?.getTime?.() ||
|
||
(typeof firstUserInfo.notified_at === 'number' ? firstUserInfo.notified_at : null) ||
|
||
fallbackTime;
|
||
} else {
|
||
// Compatibilidad con estructura antigua
|
||
result.username = article.username;
|
||
result.worker_name = article.worker_name;
|
||
result.is_favorite = article.is_favorite || false;
|
||
result.notifiedAt =
|
||
article.notifiedAt?.getTime?.() ||
|
||
(typeof article.notifiedAt === 'number' ? article.notifiedAt : null) ||
|
||
fallbackTime;
|
||
}
|
||
}
|
||
|
||
return result;
|
||
} catch (error) {
|
||
console.error('Error obteniendo artículo:', error.message);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
export async function updateArticleFavorite(platform, id, is_favorite, username) {
|
||
if (!db) {
|
||
throw new Error('MongoDB no está disponible');
|
||
}
|
||
|
||
if (!username) {
|
||
throw new Error('username es requerido para actualizar favoritos');
|
||
}
|
||
|
||
try {
|
||
const articlesCollection = db.collection('articles');
|
||
const article = await articlesCollection.findOne({ platform, id });
|
||
|
||
if (!article) {
|
||
throw new Error('Artículo no encontrado');
|
||
}
|
||
|
||
const userInfoList = article.user_info || [];
|
||
const userInfoIndex = userInfoList.findIndex(ui => ui.username === username);
|
||
|
||
if (userInfoIndex >= 0) {
|
||
// Actualizar user_info existente
|
||
userInfoList[userInfoIndex].is_favorite = is_favorite;
|
||
} else {
|
||
// Si no existe user_info para este usuario, crear uno
|
||
userInfoList.push({
|
||
username,
|
||
worker_name: null,
|
||
notified: false,
|
||
notified_at: null,
|
||
is_favorite: is_favorite,
|
||
});
|
||
}
|
||
|
||
await articlesCollection.updateOne(
|
||
{ platform, id },
|
||
{
|
||
$set: {
|
||
user_info: userInfoList,
|
||
updatedAt: new Date()
|
||
}
|
||
}
|
||
);
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error('Error actualizando favorito:', error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
// Funciones para suscripciones
|
||
export async function getUserSubscription(username) {
|
||
if (!db) {
|
||
return null;
|
||
}
|
||
|
||
try {
|
||
const usersCollection = db.collection('users');
|
||
const user = await usersCollection.findOne({ username });
|
||
|
||
if (!user) {
|
||
return null;
|
||
}
|
||
|
||
// Si no tiene suscripción, retornar plan gratuito por defecto
|
||
if (!user.subscription) {
|
||
return {
|
||
planId: 'free',
|
||
status: 'active',
|
||
currentPeriodStart: user.createdAt || new Date(),
|
||
currentPeriodEnd: null, // Plan gratuito no expira
|
||
cancelAtPeriodEnd: false,
|
||
};
|
||
}
|
||
|
||
return user.subscription;
|
||
} catch (error) {
|
||
console.error(`Error obteniendo suscripción de ${username}:`, error.message);
|
||
return null;
|
||
}
|
||
}
|
||
|
||
export async function updateUserSubscription(username, subscriptionData) {
|
||
if (!db) {
|
||
throw new Error('MongoDB no está disponible');
|
||
}
|
||
|
||
try {
|
||
const usersCollection = db.collection('users');
|
||
|
||
// Verificar que el usuario existe
|
||
const user = await usersCollection.findOne({ username });
|
||
if (!user) {
|
||
throw new Error(`Usuario ${username} no existe`);
|
||
}
|
||
|
||
// Actualizar suscripción
|
||
await usersCollection.updateOne(
|
||
{ username },
|
||
{
|
||
$set: {
|
||
subscription: {
|
||
planId: subscriptionData.planId || 'free',
|
||
status: subscriptionData.status || 'active',
|
||
billingPeriod: subscriptionData.billingPeriod || 'monthly',
|
||
currentPeriodStart: subscriptionData.currentPeriodStart || new Date(),
|
||
currentPeriodEnd: subscriptionData.currentPeriodEnd || null,
|
||
cancelAtPeriodEnd: subscriptionData.cancelAtPeriodEnd || false,
|
||
stripeCustomerId: subscriptionData.stripeCustomerId || null,
|
||
stripeSubscriptionId: subscriptionData.stripeSubscriptionId || null,
|
||
updatedAt: new Date(),
|
||
},
|
||
updatedAt: new Date(),
|
||
},
|
||
}
|
||
);
|
||
|
||
return true;
|
||
} catch (error) {
|
||
console.error(`Error actualizando suscripción de ${username}:`, error.message);
|
||
throw error;
|
||
}
|
||
}
|
||
|
||
export async function getWorkerCount(username) {
|
||
if (!db) {
|
||
return 0;
|
||
}
|
||
|
||
try {
|
||
const workersCollection = db.collection('workers');
|
||
const workersData = await workersCollection.findOne({ username });
|
||
|
||
if (!workersData || !workersData.items) {
|
||
return 0;
|
||
}
|
||
|
||
// Contar solo workers activos (no deshabilitados)
|
||
const activeWorkers = (workersData.items || []).filter(
|
||
(item, index) => !(workersData.disabled || []).includes(index)
|
||
);
|
||
|
||
return activeWorkers.length;
|
||
} catch (error) {
|
||
console.error(`Error contando workers de ${username}:`, error.message);
|
||
return 0;
|
||
}
|
||
}
|
||
|
||
// Cerrar conexión
|
||
export async function closeMongoDB() {
|
||
if (mongoClient) {
|
||
await mongoClient.close();
|
||
mongoClient = null;
|
||
db = null;
|
||
console.log('✅ Conexión a MongoDB cerrada');
|
||
}
|
||
}
|
||
|