#!/usr/bin/env node /** * Nextcloud Activity Monitor * Мониторит события файлов через Activity API и публикует в Redis */ const https = require('https'); const Redis = require('ioredis'); const CONFIG = { nextcloud: { host: 'office.clientright.ru', port: 8443, username: 'admin', password: 'tGHKS-3cC9m-7Hggb-65Awk-zxWQE', auth: Buffer.from('admin:tGHKS-3cC9m-7Hggb-65Awk-zxWQE').toString('base64') }, redis: { host: '147.45.146.17', port: 6379, password: 'CRM_Redis_Pass_2025_Secure!' }, pollInterval: 30000, // 30 секунд stateKey: 'crm:nextcloud:activity:state' }; const redis = new Redis({ host: CONFIG.redis.host, port: CONFIG.redis.port, password: CONFIG.redis.password }); // Хранилище последнего обработанного activity_id let lastActivityId = 0; // Загрузка состояния из Redis async function loadState() { try { const data = await redis.get(CONFIG.stateKey); if (data) { const state = JSON.parse(data); lastActivityId = state.lastActivityId || 0; console.log(`📥 Последний обработанный activity_id: ${lastActivityId}`); } else { console.log('📥 Состояние пустое (первый запуск)'); } } catch (err) { console.error('⚠️ Ошибка загрузки состояния:', err.message); } } // Сохранение состояния в Redis async function saveState() { try { await redis.set(CONFIG.stateKey, JSON.stringify({ lastActivityId })); } catch (err) { console.error('⚠️ Ошибка сохранения состояния:', err.message); } } // Получение активностей из Nextcloud API function getActivities(limit = 100) { return new Promise((resolve, reject) => { const options = { hostname: CONFIG.nextcloud.host, port: CONFIG.nextcloud.port, path: `/ocs/v2.php/apps/activity/api/v2/activity?format=json&limit=${limit}`, method: 'GET', headers: { 'Authorization': `Basic ${CONFIG.nextcloud.auth}`, 'OCS-APIRequest': 'true' }, rejectUnauthorized: false }; const req = https.request(options, (res) => { let data = ''; res.on('data', (chunk) => { data += chunk; }); res.on('end', () => { try { const json = JSON.parse(data); resolve(json.ocs.data || []); } catch (err) { reject(new Error('Ошибка парсинга JSON: ' + err.message)); } }); }); req.on('error', (err) => { reject(err); }); req.setTimeout(10000, () => { req.destroy(); reject(new Error('Timeout')); }); req.end(); }); } // Публикация события в Redis async function publishEvent(event) { console.log(`\n 📢 ${event.type.toUpperCase()}: ${event.filename}`); console.log(` 🆔 file_id: ${event.file_id}`); console.log(` 👤 user: ${event.user}`); try { await redis.publish('crm:file:events', JSON.stringify(event)); console.log(` ✅ Опубликовано в Redis`); } catch (err) { console.error(` ❌ Ошибка публикации:`, err.message); } } // Сканирование новых активностей async function scanActivities() { try { console.log(`\n🔍 Проверка новых событий... (${new Date().toISOString()})`); const activities = await getActivities(100); const relevantTypes = ['file_created', 'file_changed', 'file_deleted', 'file_restored']; let newEvents = 0; let totalFiles = 0; // Обрабатываем активности (от новых к старым) for (const activity of activities) { // Пропускаем уже обработанные if (activity.activity_id <= lastActivityId) { continue; } // Только файловые события if (!relevantTypes.includes(activity.type)) { continue; } newEvents++; // РАЗБИВАЕМ агрегированные события на отдельные файлы! if (activity.objects && typeof activity.objects === 'object') { // Множественное событие - разбиваем const fileIds = Object.keys(activity.objects); totalFiles += fileIds.length; console.log(` 📦 Агрегированное событие: ${fileIds.length} файлов`); for (const fileId of fileIds) { const filePath = activity.objects[fileId]; const event = { type: activity.type, source: 'nextcloud_activity', timestamp: activity.datetime, file_id: parseInt(fileId), path: filePath, filename: filePath ? filePath.split('/').pop().replace(/^\//, '') : null, user: activity.user, activity_id: activity.activity_id, action: activity.type.replace('file_', '') }; await publishEvent(event); } } else { // Одиночное событие totalFiles++; const event = { type: activity.type, source: 'nextcloud_activity', timestamp: activity.datetime, file_id: activity.object_id, path: activity.object_name, filename: activity.object_name ? activity.object_name.split('/').pop().replace(/^\//, '') : null, user: activity.user, activity_id: activity.activity_id, action: activity.type.replace('file_', '') }; await publishEvent(event); } // Обновляем последний ID if (activity.activity_id > lastActivityId) { lastActivityId = activity.activity_id; } } // Сохраняем состояние await saveState(); console.log(`\n✅ Сканирование завершено:`); console.log(` 📊 Новых активностей: ${newEvents}`); console.log(` 📁 Файлов обработано: ${totalFiles}`); console.log(` 🆔 Последний activity_id: ${lastActivityId}`); } catch (err) { console.error('❌ Ошибка сканирования:', err.message); } } // Запуск async function start() { console.log('🚀 Nextcloud Activity Monitor'); console.log('════════════════════════════════════════════════════════════════════════════════'); console.log(`📡 Nextcloud: ${CONFIG.nextcloud.host}:${CONFIG.nextcloud.port}`); console.log(`📡 Redis: ${CONFIG.redis.host}:${CONFIG.redis.port}`); console.log(`🔄 Интервал: ${CONFIG.pollInterval / 1000}с`); console.log('════════════════════════════════════════════════════════════════════════════════\n'); await loadState(); console.log('👂 Начинаем мониторинг...\n'); // Первое сканирование await scanActivities(); // Периодическое сканирование setInterval(scanActivities, CONFIG.pollInterval); } // Запуск при подключении к Redis redis.on('connect', () => { console.log('✅ Подключились к Redis\n'); start(); }); redis.on('error', (err) => { console.error('❌ Redis ошибка:', err.message); }); process.on('SIGINT', () => { console.log('\n\n⛔ Остановка мониторинга...'); redis.disconnect(); process.exit(0); }); process.on('SIGTERM', () => { console.log('\n\n⛔ Получен сигнал SIGTERM, останавливаемся...'); redis.disconnect(); process.exit(0); });