Compare commits
12 commits
b33f04886d
...
98f4afda2e
Author | SHA1 | Date | |
---|---|---|---|
![]() |
98f4afda2e | ||
![]() |
b9ed6cd6ec | ||
![]() |
2e613573a2 | ||
![]() |
4658be548a | ||
![]() |
395975a111 | ||
![]() |
47e43be184 | ||
![]() |
3d4552c183 | ||
![]() |
3947dec67c | ||
![]() |
e05db3ba4d | ||
![]() |
81a24e3636 | ||
![]() |
917b12bb27 | ||
![]() |
22bebeaa06 |
3
.gitignore
vendored
3
.gitignore
vendored
|
@ -137,3 +137,6 @@ dist
|
|||
|
||||
# pm2 ecosystem file
|
||||
ecosystem.config.js
|
||||
|
||||
# firebase service file
|
||||
firebase-service-account.json
|
||||
|
|
|
@ -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"
|
||||
}
|
||||
}
|
192
api/message-board/message-board.js
Normal file
192
api/message-board/message-board.js
Normal 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;
|
|
@ -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}`);
|
||||
}
|
||||
sslOptions = null;
|
||||
}
|
||||
|
||||
console.log("Starting server without SSL...");
|
||||
|
||||
// start http server as fallback
|
||||
// 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}`);
|
||||
});
|
||||
}
|
||||
|
|
|
@ -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;
|
||||
}
|
||||
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;
|
||||
}
|
||||
pm2.list(async (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;
|
||||
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;
|
||||
|
||||
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()
|
||||
};
|
||||
});
|
||||
const processName = process.name;
|
||||
const isNowOnline = process.pm2_env.status === 'online';
|
||||
|
||||
pm2.disconnect();
|
||||
resolve();
|
||||
});
|
||||
});
|
||||
});
|
||||
// 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);
|
||||
|
|
|
@ -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) {
|
||||
|
|
42
discord/commands/shutdown.js
Normal file
42
discord/commands/shutdown.js
Normal 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
1933
package-lock.json
generated
File diff suppressed because it is too large
Load diff
|
@ -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
61
test-firebase.js
Normal 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);
|
||||
}
|
Loading…
Reference in a new issue