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
ecosystem.config.js
# firebase service file
firebase-service-account.json

View file

@ -1,171 +1,171 @@
{
"USD": {
"lastUpdated": "2025-04-07T19:30:40.700Z",
"lastUpdated": "2025-04-30T14:50:02.050Z",
"rates": {
"USD": 1,
"AED": 3.6725,
"AFN": 71.3562,
"ALL": 90.1377,
"AMD": 391.1786,
"AFN": 70.9584,
"ALL": 86.782,
"AMD": 389.9021,
"ANG": 1.79,
"AOA": 918.5748,
"ARS": 1075.88,
"AUD": 1.662,
"AOA": 918.243,
"ARS": 1170.67,
"AUD": 1.5645,
"AWG": 1.79,
"AZN": 1.6999,
"BAM": 1.7805,
"AZN": 1.7005,
"BAM": 1.7173,
"BBD": 2,
"BDT": 121.3674,
"BGN": 1.7804,
"BDT": 121.5,
"BGN": 1.7175,
"BHD": 0.376,
"BIF": 2960.3407,
"BIF": 2954.2386,
"BMD": 1,
"BND": 1.3404,
"BOB": 6.8885,
"BRL": 5.7194,
"BND": 1.3085,
"BOB": 6.9176,
"BRL": 5.655,
"BSD": 1,
"BTN": 85.5708,
"BWP": 13.8733,
"BYN": 3.1354,
"BTN": 85.2183,
"BWP": 13.7041,
"BYN": 3.0524,
"BZD": 2,
"CAD": 1.423,
"CDF": 2899.0345,
"CHF": 0.8524,
"CLP": 960.6602,
"CNY": 7.2857,
"COP": 4203.747,
"CRC": 502.4294,
"CAD": 1.3839,
"CDF": 2882.8224,
"CHF": 0.8241,
"CLP": 943.6795,
"CNY": 7.2743,
"COP": 4210.8649,
"CRC": 504.9542,
"CUP": 24,
"CVE": 100.3829,
"CZK": 22.9524,
"CVE": 96.8156,
"CZK": 21.9027,
"DJF": 177.721,
"DKK": 6.7905,
"DOP": 62.8304,
"DZD": 132.995,
"EGP": 50.8068,
"DKK": 6.5522,
"DOP": 58.8495,
"DZD": 132.3973,
"EGP": 50.8016,
"ERN": 15,
"ETB": 131.8684,
"EUR": 0.9102,
"FJD": 2.3154,
"FKP": 0.7757,
"FOK": 6.7935,
"GBP": 0.7751,
"GEL": 2.7559,
"GGP": 0.7757,
"GHS": 15.5067,
"GIP": 0.7757,
"GMD": 72.6441,
"GNF": 8570.968,
"GTQ": 7.6902,
"GYD": 209.8017,
"HKD": 7.7734,
"HNL": 25.5147,
"HRK": 6.8592,
"HTG": 130.7844,
"HUF": 369.6088,
"IDR": 16757.5574,
"ILS": 3.7448,
"IMP": 0.7757,
"INR": 85.5703,
"IQD": 1309.7144,
"IRR": 42008.9149,
"ISK": 131.125,
"JEP": 0.7757,
"JMD": 157.719,
"ETB": 132.3384,
"EUR": 0.8781,
"FJD": 2.2528,
"FKP": 0.746,
"FOK": 6.5526,
"GBP": 0.7461,
"GEL": 2.7497,
"GGP": 0.746,
"GHS": 14.3404,
"GIP": 0.746,
"GMD": 72.7,
"GNF": 8698.1694,
"GTQ": 7.7017,
"GYD": 209.6895,
"HKD": 7.7583,
"HNL": 25.9262,
"HRK": 6.6155,
"HTG": 130.6641,
"HUF": 355.0713,
"IDR": 16731.4579,
"ILS": 3.624,
"IMP": 0.746,
"INR": 85.2184,
"IQD": 1308.7194,
"IRR": 41991.8107,
"ISK": 128.3463,
"JEP": 0.746,
"JMD": 158.4119,
"JOD": 0.709,
"JPY": 145.2473,
"KES": 129.2338,
"KGS": 86.8492,
"KHR": 3997.3556,
"KID": 1.6621,
"KMF": 447.8769,
"KRW": 1457.9608,
"KWD": 0.307,
"JPY": 142.3246,
"KES": 129.2923,
"KGS": 87.0935,
"KHR": 4026.7938,
"KID": 1.565,
"KMF": 431.9608,
"KRW": 1433.504,
"KWD": 0.3064,
"KYD": 0.8333,
"KZT": 510.3326,
"LAK": 21751.6772,
"KZT": 512.1031,
"LAK": 21704.1039,
"LBP": 89500,
"LKR": 295.3817,
"LRD": 199.3433,
"LSL": 19.1915,
"LYD": 4.8358,
"MAD": 9.5166,
"MDL": 17.6443,
"MGA": 4654.3433,
"MKD": 55.9208,
"MMK": 2090.2388,
"MNT": 3479.6583,
"MOP": 8.0068,
"MRU": 39.8843,
"MUR": 44.5309,
"MVR": 15.4592,
"MWK": 1740.2553,
"MXN": 20.5515,
"MYR": 4.437,
"MZN": 63.6781,
"NAD": 19.1915,
"NGN": 1529.0612,
"NIO": 36.6793,
"NOK": 10.7699,
"NPR": 136.9132,
"NZD": 1.7956,
"LKR": 299.3413,
"LRD": 199.8861,
"LSL": 18.5447,
"LYD": 5.4665,
"MAD": 9.2601,
"MDL": 17.1502,
"MGA": 4505.4667,
"MKD": 54.2379,
"MMK": 2098.8531,
"MNT": 3581.085,
"MOP": 7.991,
"MRU": 39.7408,
"MUR": 45.0517,
"MVR": 15.4444,
"MWK": 1741.6056,
"MXN": 19.5726,
"MYR": 4.3277,
"MZN": 63.782,
"NAD": 18.5447,
"NGN": 1601.1782,
"NIO": 36.7914,
"NOK": 10.3694,
"NPR": 136.3493,
"NZD": 1.6836,
"OMR": 0.3845,
"PAB": 1,
"PEN": 3.6809,
"PGK": 4.0959,
"PHP": 57.3644,
"PKR": 280.7358,
"PLN": 3.8787,
"PYG": 8001.2022,
"PEN": 3.6672,
"PGK": 4.1028,
"PHP": 56.0867,
"PKR": 281.1451,
"PLN": 3.749,
"PYG": 8026.0982,
"QAR": 3.64,
"RON": 4.5185,
"RSD": 106.3911,
"RUB": 84.4536,
"RWF": 1422.8596,
"RON": 4.3713,
"RSD": 102.9144,
"RUB": 81.9349,
"RWF": 1451.6279,
"SAR": 3.75,
"SBD": 8.3385,
"SCR": 14.8196,
"SDG": 458.3047,
"SEK": 10.0072,
"SGD": 1.3405,
"SHP": 0.7757,
"SLE": 22.7181,
"SLL": 22718.051,
"SOS": 571.0444,
"SRD": 36.8241,
"SSP": 4519.748,
"STN": 22.3043,
"SYP": 12873.9497,
"SZL": 19.1915,
"THB": 34.3823,
"TJS": 10.9221,
"TMT": 3.4983,
"TND": 3.0571,
"TOP": 2.3833,
"TRY": 38.0295,
"TTD": 6.7342,
"TVD": 1.6621,
"TWD": 33.1309,
"TZS": 2647.2453,
"UAH": 41.1747,
"UGX": 3662.2001,
"UYU": 42.042,
"UZS": 12937.493,
"VES": 72.1856,
"VND": 25642.7185,
"VUV": 121.865,
"WST": 2.8015,
"XAF": 597.1692,
"SBD": 8.5503,
"SCR": 14.6335,
"SDG": 460.9319,
"SEK": 9.6342,
"SGD": 1.3085,
"SHP": 0.746,
"SLE": 22.6844,
"SLL": 22684.4001,
"SOS": 571.3184,
"SRD": 36.6923,
"SSP": 4594.8425,
"STN": 21.5117,
"SYP": 12884.7736,
"SZL": 18.5447,
"THB": 33.4103,
"TJS": 10.7469,
"TMT": 3.4996,
"TND": 2.9792,
"TOP": 2.3554,
"TRY": 38.5176,
"TTD": 6.7425,
"TVD": 1.565,
"TWD": 32.2124,
"TZS": 2687.3856,
"UAH": 41.5604,
"UGX": 3661.3257,
"UYU": 42.0282,
"UZS": 12935.0729,
"VES": 86.8465,
"VND": 26020.9877,
"VUV": 120.688,
"WST": 2.7584,
"XAF": 575.9477,
"XCD": 2.7,
"XCG": 1.79,
"XDR": 0.751,
"XOF": 597.1692,
"XPF": 108.6373,
"YER": 244.8828,
"ZAR": 19.2142,
"ZMW": 27.9801,
"ZWL": 6.7864
"XDR": 0.7372,
"XOF": 575.9477,
"XPF": 104.7767,
"YER": 245.1184,
"ZAR": 18.5441,
"ZMW": 28.1983,
"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 exchangeRate = require("./exchange-rate/exchange-rate");
const whois = require("./whois/whois");
const messageBoard = require("./message-board/message-board");
// Main API app
const app = express();
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 cert = process.env.SSL_CERT_PATH || "/etc/letsencrypt/live/xargana.tr/fullchain.pem";
// Configure main API app
app.use(cors());
app.use("/status", status);
app.use("/exchange-rate", exchangeRate);
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 {
const sslOptions = {
sslOptions = {
key: fs.readFileSync(key),
cert: fs.readFileSync(cert),
};
https.createServer(sslOptions, app).listen(PORT, () => {
console.log(`API running at https://localhost:${PORT}`);
});
} catch (e) {
if (e.code === 'ENOENT') {
console.warn(`SSL certificate file(s) not found: ${e.path}`);
} else {
console.warn(`Error loading SSL certificates: ${e.message}`);
}
console.log("Starting server without SSL...");
// start http server as fallback
sslOptions = null;
}
// Start main API server
if (sslOptions) {
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, () => {
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 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();
const REMOTE_SERVERS = [
{ name: "xargana.tr", host: "xargana.tr" },
{ name: "xargana.com", host: "xargana.com" },
{ name: "home server", host: "31.223.36.208" }
{ name : "google.com", host: "google.com" },
{ name: "xargana.tr", host: "xargana.tr"}
// { name: "home server", host: "31.223.36.208" } removed cause router not behaving, dropped all pings today.
];
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 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
function ensureLogDirectories() {
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 = {};
REMOTE_SERVERS.forEach(server => {
serversStatus[server.name] = {
@ -37,11 +61,43 @@ REMOTE_SERVERS.forEach(server => {
lastChecked: null,
responseTime: null,
};
// Initialize previous status
previousServersStatus[server.name] = false;
// Initialize failure counters
serverFailureCounts[server.name] = 0;
});
// 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 +107,57 @@ 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;
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) {
console.error(`Error pinging ${server.host}:`, error);
serversStatus[server.name].online = false;
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();
// Log server status to the appropriate folder
@ -70,6 +170,7 @@ async function checkServers() {
const logEntry = `[${timestamp}] Server: ${server.name} (${server.host})\n` +
`Status: ${serverStatus.online ? 'ONLINE' : 'OFFLINE'}\n` +
`Response Time: ${serverStatus.responseTime ? serverStatus.responseTime + 'ms' : 'N/A'}\n` +
`Failure Count: ${serverFailureCounts[server.name]}\n` +
`-----------------------------------\n`;
// Append to log file
@ -85,47 +186,86 @@ 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;
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,
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,
failureCount: pm2FailureCounts[processName],
lastChecked: new Date().toISOString()
};
}
} catch (error) {
console.error('Error processing PM2 services:', error);
}
pm2.disconnect();
resolve();
});
});
});
}
async function checkAll() {
@ -160,7 +300,10 @@ router.get("/", (req, res) => {
try {
res.json({
servers: serversStatus,
pm2Services: pm2ServicesStatus
pm2Services: pm2ServicesStatus,
serverFailureCounts: serverFailureCounts,
pm2FailureCounts: pm2FailureCounts,
notificationThreshold: NOTIFICATION_THRESHOLD
});
} catch (error) {
console.error("Error sending status response:", error);

View file

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

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",
"dependencies": {
"axios": "^1.8.4",
"axios": "^1.9.0",
"cors": "^2.8.5",
"dotenv": "^16.4.7",
"discord.js": "^14.19.1",
"dotenv": "^16.5.0",
"express": "^4.21.2",
"firebase-admin": "^13.3.0",
"googleapis": "^148.0.0",
"nodejs": "^0.0.0",
"ping": "^0.4.4",
"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);
}