diff --git a/.gitignore b/.gitignore index 4c61ae6..2865034 100644 --- a/.gitignore +++ b/.gitignore @@ -137,3 +137,6 @@ dist # pm2 ecosystem file ecosystem.config.js + +# firebase service file +firebase-service-account.json diff --git a/api/status/status.js b/api/status/status.js index 77bd762..0de7d41 100644 --- a/api/status/status.js +++ b/api/status/status.js @@ -3,6 +3,19 @@ const ping = require("ping"); const pm2 = require("pm2"); const fs = require("fs"); const path = require("path"); +const axios = require("axios"); +const admin = require("firebase-admin"); + +// Initialize Firebase Admin SDK +try { + const serviceAccount = require("../../firebase-service-account.json"); + admin.initializeApp({ + credential: admin.credential.cert(serviceAccount) + }); + console.log("Firebase Admin SDK initialized successfully"); +} catch (error) { + console.error("Error initializing Firebase Admin SDK:", error); +} const router = express.Router(); @@ -30,6 +43,10 @@ function ensureLogDirectories() { } } +// Track previous states for notifications +let previousServersStatus = {}; +let previousPM2Status = {}; + let serversStatus = {}; REMOTE_SERVERS.forEach(server => { serversStatus[server.name] = { @@ -37,11 +54,41 @@ REMOTE_SERVERS.forEach(server => { lastChecked: null, responseTime: null, }; + // Initialize previous status + previousServersStatus[server.name] = false; }); // Add PM2 services status object let pm2ServicesStatus = {}; +// Function to send FCM notification +async function sendFCMNotification(message, topic) { + try { + if (!admin.apps.length) { + console.warn("Firebase Admin not initialized, skipping notification"); + return; + } + + // Create the message object according to Firebase Admin SDK format + const fcmMessage = { + topic: topic, + notification: { + title: 'Server Status Alert', + body: message + }, + data: { + type: 'server_status', + timestamp: Date.now().toString() + } + }; + + await admin.messaging().send(fcmMessage); + console.log(`Notification sent: ${message}`); + } catch (error) { + console.error('Error sending notification:', error); + } +} + async function checkServers() { try { ensureLogDirectories(); @@ -51,13 +98,37 @@ async function checkServers() { const res = await ping.promise.probe(server.host, { timeout: 4, // Set a timeout of 4 seconds }); - serversStatus[server.name].online = res.alive; + + // Get previous status before updating + const wasOnline = previousServersStatus[server.name]; + const isNowOnline = res.alive; + + // Update status + serversStatus[server.name].online = isNowOnline; serversStatus[server.name].responseTime = res.time; + + // Send notifications for status changes + if (wasOnline === false && isNowOnline) { + await sendFCMNotification(`Server ${server.name} is back online`, 'service_online'); + } else if (wasOnline === true && !isNowOnline) { + await sendFCMNotification(`Server ${server.name} is offline`, 'service_offline'); + } + + // Update previous status + previousServersStatus[server.name] = isNowOnline; + } catch (error) { console.error(`Error pinging ${server.host}:`, error); serversStatus[server.name].online = false; serversStatus[server.name].responseTime = null; + + // Check if status changed from online to offline + if (previousServersStatus[server.name] === true) { + await sendFCMNotification(`Server ${server.name} is unreachable`, 'service_offline'); + previousServersStatus[server.name] = false; + } } + serversStatus[server.name].lastChecked = new Date().toISOString(); // Log server status to the appropriate folder @@ -85,47 +156,73 @@ async function checkServers() { } async function checkPM2Services() { - return new Promise((resolve, reject) => { - pm2.connect(function(err) { - if (err) { - console.error('Error connecting to PM2:', err); - pm2.disconnect(); - resolve(); - return; - } - - pm2.list((err, list) => { - if (err) { - console.error('Error getting PM2 process list:', err); - pm2.disconnect(); - resolve(); - return; - } - - // Update PM2 services status - list.forEach(process => { - // Calculate uptime correctly - pm_uptime is a timestamp, not a duration - const uptimeMs = process.pm2_env.pm_uptime ? - Date.now() - process.pm2_env.pm_uptime : - null; - - pm2ServicesStatus[process.name] = { - name: process.name, - id: process.pm_id, - status: process.pm2_env.status, - cpu: process.monit ? process.monit.cpu : null, - memory: process.monit ? process.monit.memory : null, - uptime: uptimeMs, // Store the uptime in milliseconds - restarts: process.pm2_env.restart_time, - lastChecked: new Date().toISOString() - }; - }); - - pm2.disconnect(); - resolve(); - }); - }); - }); + return new Promise((resolve, reject) => { + pm2.connect(function(err) { + if (err) { + console.error('Error connecting to PM2:', err); + pm2.disconnect(); + resolve(); + return; + } + + pm2.list(async (err, list) => { + if (err) { + console.error('Error getting PM2 process list:', err); + pm2.disconnect(); + resolve(); + return; + } + + try { + // Process each PM2 service sequentially with proper async handling + for (const process of list) { + const uptimeMs = process.pm2_env.pm_uptime ? + Date.now() - process.pm2_env.pm_uptime : + null; + + const processName = process.name; + const isNowOnline = process.pm2_env.status === 'online'; + + // Check if we've seen this process before + if (previousPM2Status[processName] === undefined) { + // First time seeing this process - initialize and don't send notification + previousPM2Status[processName] = isNowOnline; + console.log(`Initializing PM2 service status for ${processName}: ${isNowOnline ? 'online' : 'offline'}`); + } + // Check if status changed + else if (previousPM2Status[processName] === false && isNowOnline) { + await sendFCMNotification(`PM2 service ${processName} is back online`, 'service_online'); + console.log(`PM2 service ${processName} changed from offline to online`); + } + else if (previousPM2Status[processName] === true && !isNowOnline) { + await sendFCMNotification(`PM2 service ${processName} is offline (status: ${process.pm2_env.status})`, 'service_offline'); + console.log(`PM2 service ${processName} changed from online to ${process.pm2_env.status}`); + } + + // Update previous status + previousPM2Status[processName] = isNowOnline; + + // Update status object + pm2ServicesStatus[processName] = { + name: processName, + id: process.pm_id, + status: process.pm2_env.status, + cpu: process.monit ? process.monit.cpu : null, + memory: process.monit ? process.monit.memory : null, + uptime: uptimeMs, + restarts: process.pm2_env.restart_time, + lastChecked: new Date().toISOString() + }; + } + } catch (error) { + console.error('Error processing PM2 services:', error); + } + + pm2.disconnect(); + resolve(); + }); + }); + }); } async function checkAll() { diff --git a/package-lock.json b/package-lock.json index 4d708db..b355742 100644 --- a/package-lock.json +++ b/package-lock.json @@ -15,13 +15,17 @@ "dotenv": "^16.5.0", "express": "^4.21.2", "firebase-admin": "^13.3.0", +<<<<<<< HEAD "googleapis": "^148.0.0", +======= +>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8 "nodejs": "^0.0.0", "ping": "^0.4.4", "pm2": "^6.0.5", "whois-json": "^2.0.4" } }, +<<<<<<< HEAD "node_modules/@discordjs/builders": { "version": "1.11.1", "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.11.1.tgz", @@ -149,6 +153,8 @@ "url": "https://github.com/discordjs/discord.js?sponsor" } }, +======= +>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8 "node_modules/@fastify/busboy": { "version": "3.1.1", "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-3.1.1.tgz", @@ -884,6 +890,7 @@ "license": "BSD-3-Clause", "optional": true }, +<<<<<<< HEAD "node_modules/@sapphire/async-queue": { "version": "1.5.5", "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz", @@ -917,6 +924,8 @@ "npm": ">=7.0.0" } }, +======= +>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8 "node_modules/@tootallnate/once": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@tootallnate/once/-/once-2.0.0.tgz", @@ -1019,9 +1028,15 @@ "license": "MIT" }, "node_modules/@types/node": { +<<<<<<< HEAD "version": "22.15.2", "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.2.tgz", "integrity": "sha512-uKXqKN9beGoMdBfcaTY1ecwz6ctxuJAcUlwE55938g0ZJ8lRxwAZqRz2AJ4pzpt5dHdTPMB863UZ0ESiFUcP7A==", +======= + "version": "22.15.3", + "resolved": "https://registry.npmjs.org/@types/node/-/node-22.15.3.tgz", + "integrity": "sha512-lX7HFZeHf4QG/J7tBZqrCAXwz9J5RD56Y6MpP0eJkka8p+K0RY/yBTW7CYFJ4VGCclxqOLKmiGP5juQc6MKgcw==", +>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8 "license": "MIT", "dependencies": { "undici-types": "~6.21.0" @@ -1097,6 +1112,7 @@ "license": "MIT", "optional": true }, +<<<<<<< HEAD "node_modules/@types/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz", @@ -1116,6 +1132,8 @@ "npm": ">=7.0.0" } }, +======= +>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8 "node_modules/abort-controller": { "version": "3.0.0", "resolved": "https://registry.npmjs.org/abort-controller/-/abort-controller-3.0.0.tgz", @@ -2061,7 +2079,12 @@ "version": "3.1.3", "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==", +<<<<<<< HEAD "license": "MIT" +======= + "license": "MIT", + "optional": true +>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8 }, "node_modules/fast-json-patch": { "version": "3.1.1", @@ -2173,6 +2196,7 @@ "@google-cloud/storage": "^7.14.0" } }, +<<<<<<< HEAD "node_modules/firebase-admin/node_modules/uuid": { "version": "11.1.0", "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", @@ -2186,6 +2210,8 @@ "uuid": "dist/esm/bin/uuid" } }, +======= +>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8 "node_modules/follow-redirects": { "version": "1.15.9", "resolved": "https://registry.npmjs.org/follow-redirects/-/follow-redirects-1.15.9.tgz", @@ -2280,6 +2306,22 @@ "node": ">=14" } }, +<<<<<<< HEAD +======= + "node_modules/gaxios/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "bin": { + "uuid": "dist/bin/uuid" + } + }, +>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8 "node_modules/gcp-metadata": { "version": "6.1.1", "resolved": "https://registry.npmjs.org/gcp-metadata/-/gcp-metadata-6.1.1.tgz", @@ -2440,6 +2482,23 @@ "node": ">=14" } }, +<<<<<<< HEAD +======= + "node_modules/google-gax/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, +>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8 "node_modules/google-logging-utils": { "version": "0.0.2", "resolved": "https://registry.npmjs.org/google-logging-utils/-/google-logging-utils-0.0.2.tgz", @@ -2449,6 +2508,7 @@ "node": ">=14" } }, +<<<<<<< HEAD "node_modules/googleapis": { "version": "148.0.0", "resolved": "https://registry.npmjs.org/googleapis/-/googleapis-148.0.0.tgz", @@ -2479,6 +2539,8 @@ "node": ">=14.0.0" } }, +======= +>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8 "node_modules/gopd": { "version": "1.2.0", "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.2.0.tgz", @@ -3038,12 +3100,15 @@ "integrity": "sha512-Sb487aTOCr9drQVL8pIxOzVhafOjZN9UU54hiN8PU3uAiSV7lx1yYNpbNmex2PK6dSJoNTSJUUswT651yww3Mg==", "license": "MIT" }, +<<<<<<< HEAD "node_modules/lodash.snakecase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==", "license": "MIT" }, +======= +>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8 "node_modules/long": { "version": "5.3.2", "resolved": "https://registry.npmjs.org/long/-/long-5.3.2.tgz", @@ -3097,12 +3162,15 @@ "node": ">=10" } }, +<<<<<<< HEAD "node_modules/magic-bytes.js": { "version": "1.12.1", "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.12.1.tgz", "integrity": "sha512-ThQLOhN86ZkJ7qemtVRGYM+gRgR8GEXNli9H/PMvpnZsE44Xfh3wx9kGJaldg314v85m+bFW6WBMaVHJc/c3zA==", "license": "MIT" }, +======= +>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8 "node_modules/math-intrinsics": { "version": "1.1.0", "resolved": "https://registry.npmjs.org/math-intrinsics/-/math-intrinsics-1.1.0.tgz", @@ -4577,6 +4645,23 @@ "license": "MIT", "optional": true }, +<<<<<<< HEAD +======= + "node_modules/teeny-request/node_modules/uuid": { + "version": "9.0.1", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", + "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", + "funding": [ + "https://github.com/sponsors/broofa", + "https://github.com/sponsors/ctavan" + ], + "license": "MIT", + "optional": true, + "bin": { + "uuid": "dist/bin/uuid" + } + }, +>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8 "node_modules/title-case": { "version": "2.1.1", "resolved": "https://registry.npmjs.org/title-case/-/title-case-2.1.1.tgz", @@ -4613,12 +4698,15 @@ "integrity": "sha512-N3WMsuqV66lT30CrXNbEjx4GEwlow3v6rr4mCcv6prnfwhS01rkgyFdjPNBYd9br7LpXV1+Emh01fHnq2Gdgrw==", "license": "MIT" }, +<<<<<<< HEAD "node_modules/ts-mixer": { "version": "6.0.4", "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz", "integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==", "license": "MIT" }, +======= +>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8 "node_modules/tslib": { "version": "2.8.1", "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz", @@ -4670,6 +4758,7 @@ "integrity": "sha512-GMXzWtsc57XAtguZgaQViUOzs0KTkk8ojr3/xAxXLITqf/3EMwxC0inyETfDFjH/Krbhuep0HNbbjI9i/q3F3g==", "license": "MIT" }, +<<<<<<< HEAD "node_modules/undici": { "version": "6.21.1", "resolved": "https://registry.npmjs.org/undici/-/undici-6.21.1.tgz", @@ -4679,6 +4768,8 @@ "node": ">=18.17" } }, +======= +>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8 "node_modules/undici-types": { "version": "6.21.0", "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-6.21.0.tgz", @@ -4708,12 +4799,15 @@ "upper-case": "^1.1.1" } }, +<<<<<<< HEAD "node_modules/url-template": { "version": "2.0.8", "resolved": "https://registry.npmjs.org/url-template/-/url-template-2.0.8.tgz", "integrity": "sha512-XdVKMF4SJ0nP/O7XIPB0JwAEuT9lDIYnNsK8yGVe43y0AWoKeJNdv3ZNWh7ksJ6KqQFjOO6ox/VEitLnaVNufw==", "license": "BSD" }, +======= +>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8 "node_modules/util-deprecate": { "version": "1.0.2", "resolved": "https://registry.npmjs.org/util-deprecate/-/util-deprecate-1.0.2.tgz", @@ -4730,16 +4824,26 @@ } }, "node_modules/uuid": { +<<<<<<< HEAD "version": "9.0.1", "resolved": "https://registry.npmjs.org/uuid/-/uuid-9.0.1.tgz", "integrity": "sha512-b+1eJOlsR9K8HJpow9Ok3fiWOWSIcIzXodvv0rQjVoOVNpWMpxf1wZNpt4y9h10odCNrqnYp1OBzRktckBe3sA==", +======= + "version": "11.1.0", + "resolved": "https://registry.npmjs.org/uuid/-/uuid-11.1.0.tgz", + "integrity": "sha512-0/A9rDy9P7cJ+8w1c9WD9V//9Wj15Ce2MPz8Ri6032usz+NfePxx5AcN3bN+r6ZL6jEo066/yNYB3tn4pQEx+A==", +>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8 "funding": [ "https://github.com/sponsors/broofa", "https://github.com/sponsors/ctavan" ], "license": "MIT", "bin": { +<<<<<<< HEAD "uuid": "dist/bin/uuid" +======= + "uuid": "dist/esm/bin/uuid" +>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8 } }, "node_modules/vary": { @@ -4867,6 +4971,7 @@ "license": "ISC", "optional": true }, +<<<<<<< HEAD "node_modules/ws": { "version": "8.18.1", "resolved": "https://registry.npmjs.org/ws/-/ws-8.18.1.tgz", @@ -4888,6 +4993,8 @@ } } }, +======= +>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8 "node_modules/y18n": { "version": "4.0.3", "resolved": "https://registry.npmjs.org/y18n/-/y18n-4.0.3.tgz", diff --git a/package.json b/package.json index 23ccada..4df08f9 100644 --- a/package.json +++ b/package.json @@ -26,7 +26,10 @@ "dotenv": "^16.5.0", "express": "^4.21.2", "firebase-admin": "^13.3.0", +<<<<<<< HEAD "googleapis": "^148.0.0", +======= +>>>>>>> 917b12bb273711b9117a93963c6ee0ff957376a8 "nodejs": "^0.0.0", "ping": "^0.4.4", "pm2": "^6.0.5", diff --git a/test-firebase.js b/test-firebase.js new file mode 100644 index 0000000..8dc735b --- /dev/null +++ b/test-firebase.js @@ -0,0 +1,61 @@ +const admin = require('firebase-admin'); +const path = require('path'); + +// Get path to service account file +const serviceAccountPath = path.join(__dirname, 'firebase-service-account.json'); + +// Log the path to verify +console.log(`Loading Firebase service account from: ${serviceAccountPath}`); + +// Initialize Firebase Admin SDK +try { + const serviceAccount = require(serviceAccountPath); + + admin.initializeApp({ + credential: admin.credential.cert(serviceAccount) + }); + + console.log("Firebase Admin SDK initialized successfully"); + + // Function to send FCM notification + async function sendTestNotification() { + try { + // Send to both topics to test both scenarios + const topics = ['service_online', 'service_offline']; + + for (const topic of topics) { + // Correctly format the message for Firebase Cloud Messaging + const message = { + topic: topic, + notification: { + title: 'Test Notification', + body: `This is a test notification to the ${topic} topic` + }, + data: { + type: 'test', + timestamp: Date.now().toString() + } + }; + + console.log(`Sending test notification to topic: ${topic}`); + // Use the send method instead of sendToTopic + const response = await admin.messaging().send(message); + console.log(`Successfully sent message to ${topic}:`, response); + } + + console.log("All test notifications sent successfully!"); + process.exit(0); + } catch (error) { + console.error('Error sending notification:', error); + process.exit(1); + } + } + + // Execute the test + sendTestNotification(); + +} catch (error) { + console.error("Error initializing Firebase Admin SDK:", error); + console.error("Make sure firebase-service-account.json exists in the repository root"); + process.exit(1); +}