Compare commits

..

12 commits

Author SHA1 Message Date
Xargana 98f4afda2e added /kys 2025-05-10 15:27:38 +03:00
Xargana b9ed6cd6ec temporarily removed the notification service 2025-05-07 21:03:42 +03:00
Xargana 2e613573a2 fixed 4658be548a 2025-05-05 19:43:06 +03:00
Xargana 4658be548a fixed some more stuff 2025-05-05 19:33:40 +03:00
Xargana 395975a111 fixed stuff 2025-05-03 13:24:14 +03:00
Xargana 47e43be184 added stuff 2025-05-03 13:06:37 +03:00
Xargana 3d4552c183 this is not gonna work 2025-05-02 19:36:05 +03:00
Xargana 3947dec67c chaos 2025-05-01 10:46:42 +03:00
root e05db3ba4d Merge branch 'main' of https://github.com/Xargana/blahaj-srv 2025-05-01 09:44:32 +02:00
root 81a24e3636 aa 2025-05-01 09:39:06 +02:00
Xargana 917b12bb27 a 2025-05-01 10:37:56 +03:00
Xargana 22bebeaa06 added firebase thingey 2025-05-01 10:32:25 +03:00
11 changed files with 2635 additions and 247 deletions

3
.gitignore vendored
View file

@ -137,3 +137,6 @@ dist
# pm2 ecosystem file # pm2 ecosystem file
ecosystem.config.js ecosystem.config.js
# firebase service file
firebase-service-account.json

View file

@ -1,171 +1,171 @@
{ {
"USD": { "USD": {
"lastUpdated": "2025-04-07T19:30:40.700Z", "lastUpdated": "2025-04-30T14:50:02.050Z",
"rates": { "rates": {
"USD": 1, "USD": 1,
"AED": 3.6725, "AED": 3.6725,
"AFN": 71.3562, "AFN": 70.9584,
"ALL": 90.1377, "ALL": 86.782,
"AMD": 391.1786, "AMD": 389.9021,
"ANG": 1.79, "ANG": 1.79,
"AOA": 918.5748, "AOA": 918.243,
"ARS": 1075.88, "ARS": 1170.67,
"AUD": 1.662, "AUD": 1.5645,
"AWG": 1.79, "AWG": 1.79,
"AZN": 1.6999, "AZN": 1.7005,
"BAM": 1.7805, "BAM": 1.7173,
"BBD": 2, "BBD": 2,
"BDT": 121.3674, "BDT": 121.5,
"BGN": 1.7804, "BGN": 1.7175,
"BHD": 0.376, "BHD": 0.376,
"BIF": 2960.3407, "BIF": 2954.2386,
"BMD": 1, "BMD": 1,
"BND": 1.3404, "BND": 1.3085,
"BOB": 6.8885, "BOB": 6.9176,
"BRL": 5.7194, "BRL": 5.655,
"BSD": 1, "BSD": 1,
"BTN": 85.5708, "BTN": 85.2183,
"BWP": 13.8733, "BWP": 13.7041,
"BYN": 3.1354, "BYN": 3.0524,
"BZD": 2, "BZD": 2,
"CAD": 1.423, "CAD": 1.3839,
"CDF": 2899.0345, "CDF": 2882.8224,
"CHF": 0.8524, "CHF": 0.8241,
"CLP": 960.6602, "CLP": 943.6795,
"CNY": 7.2857, "CNY": 7.2743,
"COP": 4203.747, "COP": 4210.8649,
"CRC": 502.4294, "CRC": 504.9542,
"CUP": 24, "CUP": 24,
"CVE": 100.3829, "CVE": 96.8156,
"CZK": 22.9524, "CZK": 21.9027,
"DJF": 177.721, "DJF": 177.721,
"DKK": 6.7905, "DKK": 6.5522,
"DOP": 62.8304, "DOP": 58.8495,
"DZD": 132.995, "DZD": 132.3973,
"EGP": 50.8068, "EGP": 50.8016,
"ERN": 15, "ERN": 15,
"ETB": 131.8684, "ETB": 132.3384,
"EUR": 0.9102, "EUR": 0.8781,
"FJD": 2.3154, "FJD": 2.2528,
"FKP": 0.7757, "FKP": 0.746,
"FOK": 6.7935, "FOK": 6.5526,
"GBP": 0.7751, "GBP": 0.7461,
"GEL": 2.7559, "GEL": 2.7497,
"GGP": 0.7757, "GGP": 0.746,
"GHS": 15.5067, "GHS": 14.3404,
"GIP": 0.7757, "GIP": 0.746,
"GMD": 72.6441, "GMD": 72.7,
"GNF": 8570.968, "GNF": 8698.1694,
"GTQ": 7.6902, "GTQ": 7.7017,
"GYD": 209.8017, "GYD": 209.6895,
"HKD": 7.7734, "HKD": 7.7583,
"HNL": 25.5147, "HNL": 25.9262,
"HRK": 6.8592, "HRK": 6.6155,
"HTG": 130.7844, "HTG": 130.6641,
"HUF": 369.6088, "HUF": 355.0713,
"IDR": 16757.5574, "IDR": 16731.4579,
"ILS": 3.7448, "ILS": 3.624,
"IMP": 0.7757, "IMP": 0.746,
"INR": 85.5703, "INR": 85.2184,
"IQD": 1309.7144, "IQD": 1308.7194,
"IRR": 42008.9149, "IRR": 41991.8107,
"ISK": 131.125, "ISK": 128.3463,
"JEP": 0.7757, "JEP": 0.746,
"JMD": 157.719, "JMD": 158.4119,
"JOD": 0.709, "JOD": 0.709,
"JPY": 145.2473, "JPY": 142.3246,
"KES": 129.2338, "KES": 129.2923,
"KGS": 86.8492, "KGS": 87.0935,
"KHR": 3997.3556, "KHR": 4026.7938,
"KID": 1.6621, "KID": 1.565,
"KMF": 447.8769, "KMF": 431.9608,
"KRW": 1457.9608, "KRW": 1433.504,
"KWD": 0.307, "KWD": 0.3064,
"KYD": 0.8333, "KYD": 0.8333,
"KZT": 510.3326, "KZT": 512.1031,
"LAK": 21751.6772, "LAK": 21704.1039,
"LBP": 89500, "LBP": 89500,
"LKR": 295.3817, "LKR": 299.3413,
"LRD": 199.3433, "LRD": 199.8861,
"LSL": 19.1915, "LSL": 18.5447,
"LYD": 4.8358, "LYD": 5.4665,
"MAD": 9.5166, "MAD": 9.2601,
"MDL": 17.6443, "MDL": 17.1502,
"MGA": 4654.3433, "MGA": 4505.4667,
"MKD": 55.9208, "MKD": 54.2379,
"MMK": 2090.2388, "MMK": 2098.8531,
"MNT": 3479.6583, "MNT": 3581.085,
"MOP": 8.0068, "MOP": 7.991,
"MRU": 39.8843, "MRU": 39.7408,
"MUR": 44.5309, "MUR": 45.0517,
"MVR": 15.4592, "MVR": 15.4444,
"MWK": 1740.2553, "MWK": 1741.6056,
"MXN": 20.5515, "MXN": 19.5726,
"MYR": 4.437, "MYR": 4.3277,
"MZN": 63.6781, "MZN": 63.782,
"NAD": 19.1915, "NAD": 18.5447,
"NGN": 1529.0612, "NGN": 1601.1782,
"NIO": 36.6793, "NIO": 36.7914,
"NOK": 10.7699, "NOK": 10.3694,
"NPR": 136.9132, "NPR": 136.3493,
"NZD": 1.7956, "NZD": 1.6836,
"OMR": 0.3845, "OMR": 0.3845,
"PAB": 1, "PAB": 1,
"PEN": 3.6809, "PEN": 3.6672,
"PGK": 4.0959, "PGK": 4.1028,
"PHP": 57.3644, "PHP": 56.0867,
"PKR": 280.7358, "PKR": 281.1451,
"PLN": 3.8787, "PLN": 3.749,
"PYG": 8001.2022, "PYG": 8026.0982,
"QAR": 3.64, "QAR": 3.64,
"RON": 4.5185, "RON": 4.3713,
"RSD": 106.3911, "RSD": 102.9144,
"RUB": 84.4536, "RUB": 81.9349,
"RWF": 1422.8596, "RWF": 1451.6279,
"SAR": 3.75, "SAR": 3.75,
"SBD": 8.3385, "SBD": 8.5503,
"SCR": 14.8196, "SCR": 14.6335,
"SDG": 458.3047, "SDG": 460.9319,
"SEK": 10.0072, "SEK": 9.6342,
"SGD": 1.3405, "SGD": 1.3085,
"SHP": 0.7757, "SHP": 0.746,
"SLE": 22.7181, "SLE": 22.6844,
"SLL": 22718.051, "SLL": 22684.4001,
"SOS": 571.0444, "SOS": 571.3184,
"SRD": 36.8241, "SRD": 36.6923,
"SSP": 4519.748, "SSP": 4594.8425,
"STN": 22.3043, "STN": 21.5117,
"SYP": 12873.9497, "SYP": 12884.7736,
"SZL": 19.1915, "SZL": 18.5447,
"THB": 34.3823, "THB": 33.4103,
"TJS": 10.9221, "TJS": 10.7469,
"TMT": 3.4983, "TMT": 3.4996,
"TND": 3.0571, "TND": 2.9792,
"TOP": 2.3833, "TOP": 2.3554,
"TRY": 38.0295, "TRY": 38.5176,
"TTD": 6.7342, "TTD": 6.7425,
"TVD": 1.6621, "TVD": 1.565,
"TWD": 33.1309, "TWD": 32.2124,
"TZS": 2647.2453, "TZS": 2687.3856,
"UAH": 41.1747, "UAH": 41.5604,
"UGX": 3662.2001, "UGX": 3661.3257,
"UYU": 42.042, "UYU": 42.0282,
"UZS": 12937.493, "UZS": 12935.0729,
"VES": 72.1856, "VES": 86.8465,
"VND": 25642.7185, "VND": 26020.9877,
"VUV": 121.865, "VUV": 120.688,
"WST": 2.8015, "WST": 2.7584,
"XAF": 597.1692, "XAF": 575.9477,
"XCD": 2.7, "XCD": 2.7,
"XCG": 1.79, "XCG": 1.79,
"XDR": 0.751, "XDR": 0.7372,
"XOF": 597.1692, "XOF": 575.9477,
"XPF": 108.6373, "XPF": 104.7767,
"YER": 244.8828, "YER": 245.1184,
"ZAR": 19.2142, "ZAR": 18.5441,
"ZMW": 27.9801, "ZMW": 28.1983,
"ZWL": 6.7864 "ZWL": 26.8115
}, },
"nextUpdateTime": "2025-04-08T19:30:40.700Z" "nextUpdateTime": "2025-05-01T14:50:02.050Z"
} }
} }

View file

@ -0,0 +1,192 @@
const express = require("express");
const fs = require("fs");
const path = require("path");
const router = express.Router();
// Path for persisting messages
const MESSAGES_FILE_PATH = path.join(__dirname, 'messages-cache.json');
// In-memory storage with persistence
let messages = [];
// Store active polling connections
let pendingConnections = [];
// Track the last message ID to help clients know what's new
let lastMessageId = 0;
// Load existing messages from file if it exists
function loadMessages() {
try {
if (fs.existsSync(MESSAGES_FILE_PATH)) {
const data = fs.readFileSync(MESSAGES_FILE_PATH, 'utf8');
messages = JSON.parse(data);
console.log(`Loaded ${messages.length} messages from cache`);
// Set the initial lastMessageId based on loaded messages
if (messages.length > 0) {
lastMessageId = messages.length;
// Add ID to each message if not present
messages.forEach((msg, index) => {
if (!msg.id) msg.id = index + 1;
});
}
}
} catch (error) {
console.error("Error loading messages from cache:", error);
}
}
// Save messages to file
function saveMessages() {
try {
fs.writeFileSync(MESSAGES_FILE_PATH, JSON.stringify(messages), 'utf8');
} catch (error) {
console.error("Error saving messages to cache:", error);
}
}
// Notify all pending connections about new messages
function notifyNewMessages() {
const connectionsToNotify = [...pendingConnections];
pendingConnections = [];
connectionsToNotify.forEach(connection => {
const { res, lastId } = connection;
sendNewMessages(res, lastId);
});
}
// Send new messages to a client
function sendNewMessages(res, lastKnownId) {
const newMessages = messages.filter(msg => msg.id > lastKnownId);
const currentLastId = messages.length > 0 ? messages[messages.length - 1].id : lastKnownId;
res.json({
messages: newMessages,
lastId: currentLastId
});
}
// Load messages at startup
loadMessages();
// Submit a new message
router.post("/submit", express.json(), (req, res) => {
try {
const { name, message } = req.body;
if (!name || !message) {
return res.status(400).json({ error: "Missing name or message fields" });
}
lastMessageId++;
const newMessage = {
id: lastMessageId,
name,
message,
time: new Date().toISOString()
};
messages.push(newMessage);
// Keep only the latest 100 messages
if (messages.length > 100) {
messages = messages.slice(-100);
}
// Save to file after update
saveMessages();
// Notify all waiting clients
notifyNewMessages();
res.json({ success: true, message: "Message received" });
} catch (error) {
console.error("Error handling message submission:", error);
res.status(500).json({ error: "Internal server error" });
}
});
// Get all messages as JSON (keep for backward compatibility)
router.get("/", (req, res) => {
try {
res.json({ messages });
} catch (error) {
console.error("Error sending messages:", error);
res.status(500).json({ error: "Internal server error" });
}
});
// Long polling endpoint for message updates
router.get("/poll", (req, res) => {
try {
const lastId = parseInt(req.query.lastId || "0", 10);
const newMessages = messages.filter(msg => msg.id > lastId);
// If there are new messages, send them immediately
if (newMessages.length > 0) {
sendNewMessages(res, lastId);
} else {
// No new messages, set timeout to avoid hanging forever
const timeoutId = setTimeout(() => {
// Remove this connection from pending list
pendingConnections = pendingConnections.filter(conn => conn.res !== res);
sendNewMessages(res, lastId);
}, 30000); // 30 second timeout
// Store the connection for later notification
pendingConnections.push({
res,
lastId,
timeoutId
});
// Handle client disconnect
req.on('close', () => {
clearTimeout(timeoutId);
pendingConnections = pendingConnections.filter(conn => conn.res !== res);
});
}
} catch (error) {
console.error("Error in long polling:", error);
res.status(500).json({ error: "Internal server error" });
}
});
// Get messages as HTML
router.get("/html", (req, res) => {
try {
const html = `
<!DOCTYPE html>
<html>
<head>
<title>Public Message Pool</title>
<style>
body { font-family: Arial, sans-serif; max-width: 800px; margin: 0 auto; padding: 20px; }
.message { border-bottom: 1px solid #eee; padding: 10px 0; }
.name { font-weight: bold; }
.time { color: #666; font-size: 0.8em; }
</style>
</head>
<body>
<h1>Public Message Pool</h1>
${messages.map(msg => `
<div class="message">
<span class="name">${msg.name}</span>
<span class="time">[${msg.time}]</span>:
<div class="content">${msg.message}</div>
</div>
`).join('')}
</body>
</html>
`;
res.setHeader('Content-Type', 'text/html');
res.send(html);
} catch (error) {
console.error("Error sending HTML messages:", error);
res.status(500).send("Internal server error");
}
});
module.exports = router;

View file

@ -10,39 +10,66 @@ require('dotenv').config({ path: path.join(__dirname, '../.env') });
const status = require("./status/status"); const status = require("./status/status");
const exchangeRate = require("./exchange-rate/exchange-rate"); const exchangeRate = require("./exchange-rate/exchange-rate");
const whois = require("./whois/whois"); const whois = require("./whois/whois");
const messageBoard = require("./message-board/message-board");
// Main API app
const app = express(); const app = express();
const PORT = process.env.PORT || 2589; const PORT = process.env.PORT || 2589;
// Message board app (separate instance)
const messageBoardApp = express();
const MESSAGE_BOARD_PORT = process.env.MESSAGE_BOARD_PORT || 2845;
// SSL certificate paths
const key = process.env.SSL_KEY_PATH || "/etc/letsencrypt/live/xargana.tr/privkey.pem"; const key = process.env.SSL_KEY_PATH || "/etc/letsencrypt/live/xargana.tr/privkey.pem";
const cert = process.env.SSL_CERT_PATH || "/etc/letsencrypt/live/xargana.tr/fullchain.pem"; const cert = process.env.SSL_CERT_PATH || "/etc/letsencrypt/live/xargana.tr/fullchain.pem";
// Configure main API app
app.use(cors()); app.use(cors());
app.use("/status", status); app.use("/status", status);
app.use("/exchange-rate", exchangeRate); app.use("/exchange-rate", exchangeRate);
app.use("/whois", whois); app.use("/whois", whois);
// try to load certificates // Configure message board app
messageBoardApp.use(cors());
messageBoardApp.use("/", messageBoard);
// Try to load SSL certificates
let sslOptions;
try { try {
const sslOptions = { sslOptions = {
key: fs.readFileSync(key), key: fs.readFileSync(key),
cert: fs.readFileSync(cert), cert: fs.readFileSync(cert),
}; };
https.createServer(sslOptions, app).listen(PORT, () => {
console.log(`API running at https://localhost:${PORT}`);
});
} catch (e) { } catch (e) {
if (e.code === 'ENOENT') { if (e.code === 'ENOENT') {
console.warn(`SSL certificate file(s) not found: ${e.path}`); console.warn(`SSL certificate file(s) not found: ${e.path}`);
} else { } else {
console.warn(`Error loading SSL certificates: ${e.message}`); console.warn(`Error loading SSL certificates: ${e.message}`);
} }
sslOptions = null;
}
console.log("Starting server without SSL..."); // Start main API server
if (sslOptions) {
// start http server as fallback https.createServer(sslOptions, app).listen(PORT, () => {
console.log(`Main API running at https://localhost:${PORT}`);
});
} else {
console.log("Starting main API server without SSL...");
http.createServer(app).listen(PORT, () => { http.createServer(app).listen(PORT, () => {
console.log(`API running at http://localhost:${PORT}`); console.log(`Main API running at http://localhost:${PORT}`);
});
}
// Start message board server
if (sslOptions) {
https.createServer(sslOptions, messageBoardApp).listen(MESSAGE_BOARD_PORT, () => {
console.log(`Message Board running at https://localhost:${MESSAGE_BOARD_PORT}`);
});
} else {
console.log("Starting Message Board server without SSL...");
http.createServer(messageBoardApp).listen(MESSAGE_BOARD_PORT, () => {
console.log(`Message Board running at http://localhost:${MESSAGE_BOARD_PORT}`);
}); });
} }

View file

@ -3,13 +3,26 @@ const ping = require("ping");
const pm2 = require("pm2"); const pm2 = require("pm2");
const fs = require("fs"); const fs = require("fs");
const path = require("path"); 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(); const router = express.Router();
const REMOTE_SERVERS = [ const REMOTE_SERVERS = [
{ name: "xargana.tr", host: "xargana.tr" }, { name : "google.com", host: "google.com" },
{ name: "xargana.com", host: "xargana.com" }, { name: "xargana.tr", host: "xargana.tr"}
{ name: "home server", host: "31.223.36.208" } // { name: "home server", host: "31.223.36.208" } removed cause router not behaving, dropped all pings today.
]; ];
const CHECK_INTERVAL = 5 * 1000; const CHECK_INTERVAL = 5 * 1000;
@ -17,6 +30,9 @@ const LOGS_DIR = path.join(__dirname, '../../logs');
const ONLINE_LOGS_DIR = path.join(LOGS_DIR, 'online'); const ONLINE_LOGS_DIR = path.join(LOGS_DIR, 'online');
const OFFLINE_LOGS_DIR = path.join(LOGS_DIR, 'offline'); const OFFLINE_LOGS_DIR = path.join(LOGS_DIR, 'offline');
// Number of offline cycles before sending a notification
const NOTIFICATION_THRESHOLD = 3;
// Create log directories if they don't exist // Create log directories if they don't exist
function ensureLogDirectories() { function ensureLogDirectories() {
if (!fs.existsSync(LOGS_DIR)) { if (!fs.existsSync(LOGS_DIR)) {
@ -30,6 +46,14 @@ function ensureLogDirectories() {
} }
} }
// Track previous states for notifications
let previousServersStatus = {};
let previousPM2Status = {};
// Track consecutive offline cycles
let serverFailureCounts = {};
let pm2FailureCounts = {};
let serversStatus = {}; let serversStatus = {};
REMOTE_SERVERS.forEach(server => { REMOTE_SERVERS.forEach(server => {
serversStatus[server.name] = { serversStatus[server.name] = {
@ -37,11 +61,43 @@ REMOTE_SERVERS.forEach(server => {
lastChecked: null, lastChecked: null,
responseTime: null, responseTime: null,
}; };
// Initialize previous status
previousServersStatus[server.name] = false;
// Initialize failure counters
serverFailureCounts[server.name] = 0;
}); });
// Add PM2 services status object // Add PM2 services status object
let pm2ServicesStatus = {}; 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() { async function checkServers() {
try { try {
ensureLogDirectories(); ensureLogDirectories();
@ -51,13 +107,57 @@ async function checkServers() {
const res = await ping.promise.probe(server.host, { const res = await ping.promise.probe(server.host, {
timeout: 4, // Set a timeout of 4 seconds 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; serversStatus[server.name].responseTime = res.time;
if (isNowOnline) {
// Service is online
// Reset failure counter
serverFailureCounts[server.name] = 0;
// If service was previously offline, send online notification
if (wasOnline === false) {
await sendFCMNotification(`Server ${server.name} is back online`, 'service_online');
}
} else {
// Service is offline
// Increment failure counter
serverFailureCounts[server.name]++;
// Check if we've reached the notification threshold
if (serverFailureCounts[server.name] === NOTIFICATION_THRESHOLD) {
await sendFCMNotification(`Server ${server.name} is offline (after ${NOTIFICATION_THRESHOLD} checks)`, 'service_offline');
}
}
// Update previous status
previousServersStatus[server.name] = isNowOnline;
} catch (error) { } catch (error) {
console.error(`Error pinging ${server.host}:`, error); console.error(`Error pinging ${server.host}:`, error);
serversStatus[server.name].online = false; serversStatus[server.name].online = false;
serversStatus[server.name].responseTime = null; serversStatus[server.name].responseTime = null;
// Increment failure counter
serverFailureCounts[server.name]++;
// Check if we've reached the notification threshold
if (serverFailureCounts[server.name] === NOTIFICATION_THRESHOLD) {
await sendFCMNotification(`Server ${server.name} is unreachable (after ${NOTIFICATION_THRESHOLD} checks)`, 'service_offline');
} }
// Update previous status
previousServersStatus[server.name] = false;
}
serversStatus[server.name].lastChecked = new Date().toISOString(); serversStatus[server.name].lastChecked = new Date().toISOString();
// Log server status to the appropriate folder // Log server status to the appropriate folder
@ -70,6 +170,7 @@ async function checkServers() {
const logEntry = `[${timestamp}] Server: ${server.name} (${server.host})\n` + const logEntry = `[${timestamp}] Server: ${server.name} (${server.host})\n` +
`Status: ${serverStatus.online ? 'ONLINE' : 'OFFLINE'}\n` + `Status: ${serverStatus.online ? 'ONLINE' : 'OFFLINE'}\n` +
`Response Time: ${serverStatus.responseTime ? serverStatus.responseTime + 'ms' : 'N/A'}\n` + `Response Time: ${serverStatus.responseTime ? serverStatus.responseTime + 'ms' : 'N/A'}\n` +
`Failure Count: ${serverFailureCounts[server.name]}\n` +
`-----------------------------------\n`; `-----------------------------------\n`;
// Append to log file // Append to log file
@ -94,7 +195,7 @@ async function checkPM2Services() {
return; return;
} }
pm2.list((err, list) => { pm2.list(async (err, list) => {
if (err) { if (err) {
console.error('Error getting PM2 process list:', err); console.error('Error getting PM2 process list:', err);
pm2.disconnect(); pm2.disconnect();
@ -102,24 +203,63 @@ async function checkPM2Services() {
return; return;
} }
// Update PM2 services status try {
list.forEach(process => { // Process each PM2 service sequentially with proper async handling
// Calculate uptime correctly - pm_uptime is a timestamp, not a duration for (const process of list) {
const uptimeMs = process.pm2_env.pm_uptime ? const uptimeMs = process.pm2_env.pm_uptime ?
Date.now() - process.pm2_env.pm_uptime : Date.now() - process.pm2_env.pm_uptime :
null; null;
pm2ServicesStatus[process.name] = { const processName = process.name;
name: 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;
pm2FailureCounts[processName] = 0;
console.log(`Initializing PM2 service status for ${processName}: ${isNowOnline ? 'online' : 'offline'}`);
} else {
if (isNowOnline) {
// Service is online - reset failure counter
pm2FailureCounts[processName] = 0;
// If service was previously offline, send online notification
if (previousPM2Status[processName] === false) {
await sendFCMNotification(`PM2 service ${processName} is back online`, 'service_online');
console.log(`PM2 service ${processName} changed from offline to online`);
}
} else {
// Service is offline - increment failure counter
pm2FailureCounts[processName]++;
// Only send notification if threshold is reached
if (pm2FailureCounts[processName] === NOTIFICATION_THRESHOLD) {
await sendFCMNotification(`PM2 service ${processName} is offline (status: ${process.pm2_env.status}, after ${NOTIFICATION_THRESHOLD} checks)`, 'service_offline');
console.log(`PM2 service ${processName} is offline for ${NOTIFICATION_THRESHOLD} consecutive checks`);
}
}
}
// Update previous status
previousPM2Status[processName] = isNowOnline;
// Update status object
pm2ServicesStatus[processName] = {
name: processName,
id: process.pm_id, id: process.pm_id,
status: process.pm2_env.status, status: process.pm2_env.status,
cpu: process.monit ? process.monit.cpu : null, cpu: process.monit ? process.monit.cpu : null,
memory: process.monit ? process.monit.memory : null, memory: process.monit ? process.monit.memory : null,
uptime: uptimeMs, // Store the uptime in milliseconds uptime: uptimeMs,
restarts: process.pm2_env.restart_time, restarts: process.pm2_env.restart_time,
failureCount: pm2FailureCounts[processName],
lastChecked: new Date().toISOString() lastChecked: new Date().toISOString()
}; };
}); }
} catch (error) {
console.error('Error processing PM2 services:', error);
}
pm2.disconnect(); pm2.disconnect();
resolve(); resolve();
@ -160,7 +300,10 @@ router.get("/", (req, res) => {
try { try {
res.json({ res.json({
servers: serversStatus, servers: serversStatus,
pm2Services: pm2ServicesStatus pm2Services: pm2ServicesStatus,
serverFailureCounts: serverFailureCounts,
pm2FailureCounts: pm2FailureCounts,
notificationThreshold: NOTIFICATION_THRESHOLD
}); });
} catch (error) { } catch (error) {
console.error("Error sending status response:", error); console.error("Error sending status response:", error);

View file

@ -1,6 +1,6 @@
const { Client, GatewayIntentBits, ChannelType } = require("discord.js"); const { Client, GatewayIntentBits, ChannelType } = require("discord.js");
const CommandManager = require('./CommandManager'); const CommandManager = require('./CommandManager');
const NotificationService = require('./NotificationService'); // const NotificationService = require('./NotificationService');
const fs = require('fs'); const fs = require('fs');
const path = require('path'); const path = require('path');
@ -31,7 +31,7 @@ class Bot {
this.setupEventHandlers(); this.setupEventHandlers();
// Initialize notification service // Initialize notification service
this.notificationService = null; // this.notificationService = null;
} }
setupTempDirectory() { setupTempDirectory() {
@ -56,13 +56,13 @@ class Bot {
await this.commandManager.registerGlobalCommands(); await this.commandManager.registerGlobalCommands();
// Initialize and start the notification service // Initialize and start the notification service
this.notificationService = new NotificationService(this.client, { // this.notificationService = new NotificationService(this.client, {
checkInterval: process.env.STATUS_CHECK_INTERVAL ? parseInt(process.env.STATUS_CHECK_INTERVAL) : 5000, // checkInterval: process.env.STATUS_CHECK_INTERVAL ? parseInt(process.env.STATUS_CHECK_INTERVAL) : 5000,
statusEndpoint: process.env.STATUS_ENDPOINT || 'https://blahaj.tr:2589/status' // statusEndpoint: process.env.STATUS_ENDPOINT || 'https://blahaj.tr:2589/status'
}); // });
await this.notificationService.initialize(); // await this.notificationService.initialize();
this.notificationService.start(); // this.notificationService.start();
// Send startup notification // Send startup notification
await this.sendStartupNotification(); await this.sendStartupNotification();
@ -108,12 +108,12 @@ class Bot {
name: "Relative Time", name: "Relative Time",
value: `<t:${Math.floor(Date.now() / 1000)}:R>`, value: `<t:${Math.floor(Date.now() / 1000)}:R>`,
inline: true inline: true
},
{
name: "Status Monitoring",
value: this.notificationService?.isRunning ? "✅ Active" : "❌ Inactive",
inline: true
} }
// {
// name: "Status Monitoring",
// value: this.notificationService?.isRunning ? "✅ Active" : "❌ Inactive",
// inline: true
// }
], ],
footer: { footer: {
text: "blahaj.tr" text: "blahaj.tr"
@ -131,14 +131,14 @@ class Bot {
} }
// Also notify in status channel if configured // Also notify in status channel if configured
if (this.notificationService?.statusChannel) { // if (this.notificationService?.statusChannel) {
try { // try {
await this.notificationService.statusChannel.send({ embeds: [startupEmbed] }); // await this.notificationService.statusChannel.send({ embeds: [startupEmbed] });
console.log(`Sent startup notification to status channel: ${this.notificationService.statusChannel.name}`); // console.log(`Sent startup notification to status channel: ${this.notificationService.statusChannel.name}`);
} catch (error) { // } catch (error) {
console.error("Failed to send startup notification to status channel:", error.message); // console.error("Failed to send startup notification to status channel:", error.message);
} // }
} // }
} }
async sendShutdownNotification(reason = "Manual shutdown", error = null) { async sendShutdownNotification(reason = "Manual shutdown", error = null) {
@ -178,9 +178,9 @@ class Bot {
} }
// Stop notification service if running // Stop notification service if running
if (this.notificationService?.isRunning) { // if (this.notificationService?.isRunning) {
this.notificationService.stop(); // this.notificationService.stop();
} // }
// Notify authorized user // Notify authorized user
try { try {
@ -192,14 +192,14 @@ class Bot {
} }
// Also notify in status channel if available // Also notify in status channel if available
if (this.notificationService?.statusChannel) { // if (this.notificationService?.statusChannel) {
try { // try {
await this.notificationService.statusChannel.send({ embeds: [shutdownEmbed] }); // await this.notificationService.statusChannel.send({ embeds: [shutdownEmbed] });
console.log(`Sent shutdown notification to status channel: ${this.notificationService.statusChannel.name}`); // console.log(`Sent shutdown notification to status channel: ${this.notificationService.statusChannel.name}`);
} catch (error) { // } catch (error) {
console.error("Failed to send shutdown notification to status channel:", error.message); // console.error("Failed to send shutdown notification to status channel:", error.message);
} // }
} // }
} }
async start() { async start() {
@ -210,9 +210,9 @@ class Bot {
async stop() { async stop() {
// Stop notification service // Stop notification service
if (this.notificationService) { // if (this.notificationService) {
this.notificationService.stop(); // this.notificationService.stop();
} // }
// Destroy the client // Destroy the client
if (this.client) { if (this.client) {

View file

@ -0,0 +1,42 @@
const { SlashCommandBuilder } = require('@discordjs/builders');
module.exports = {
data: new SlashCommandBuilder()
.setName('shutdown')
.setDescription('Shuts down the bot gracefully')
.setDefaultMemberPermissions(0), // Restricts to administrators only
async execute(interaction) {
// Check if user has admin permissions
if (!interaction.member.permissions.has('ADMINISTRATOR')) {
return await interaction.reply({
content: 'You do not have permission to use this command.',
ephemeral: true
});
}
await interaction.reply({
content: 'Shutting down the bot. Goodbye!',
ephemeral: true
});
// Trigger the shutdown process
console.log(`Manual shutdown triggered by ${interaction.user.tag}`);
// Give Discord API time to send the reply
setTimeout(async () => {
try {
// Get the bot instance
const bot = interaction.client;
// Use the existing shutdown mechanism
await bot.sendShutdownNotification(`Manual shutdown triggered by ${interaction.user.tag}`);
await bot.stop();
} catch (error) {
console.error('Error during shutdown command:', error);
} finally {
process.exit(0);
}
}, 1000);
},
};

1933
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -20,10 +20,13 @@
}, },
"homepage": "https://github.com/Xargana/blahaj-srv#readme", "homepage": "https://github.com/Xargana/blahaj-srv#readme",
"dependencies": { "dependencies": {
"axios": "^1.8.4", "axios": "^1.9.0",
"cors": "^2.8.5", "cors": "^2.8.5",
"dotenv": "^16.4.7", "discord.js": "^14.19.1",
"dotenv": "^16.5.0",
"express": "^4.21.2", "express": "^4.21.2",
"firebase-admin": "^13.3.0",
"googleapis": "^148.0.0",
"nodejs": "^0.0.0", "nodejs": "^0.0.0",
"ping": "^0.4.4", "ping": "^0.4.4",
"pm2": "^6.0.5", "pm2": "^6.0.5",

61
test-firebase.js Normal file
View file

@ -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);
}