// Debug const { DebugBuilder } = require("../utilities/debugBuilder.js"); const log = new DebugBuilder("server", "nodesController"); // Utilities const { getAllNodes, addNewNode, updateNodeInfo, getNodeInfoFromId, getOnlineNodes } = require("../utilities/mysqlHandler"); const utils = require("../utilities/utils"); const { sendHttpRequest, requestOptions } = require("../utilities/httpRequests.js"); const { nodeObject } = require("../utilities/recordHelper.js"); const refreshInterval = process.env.NODE_MONITOR_REFRESH_INTERVAL ?? 1200000; const digitalModes = ['p25']; /** * Check in with a singular node, mark it offline if it's offline and * * @param {*} node The node Object to check in with */ async function checkInWithNode(node) { const reqOptions = new requestOptions("/client/requestCheckIn", "GET", node.ip, node.port) sendHttpRequest(reqOptions, "", (responseObj) => { if (responseObj) { log.DEBUG("Response from: ", node.name, responseObj); const onlineNode = new nodeObject({ _online: true, _id: node.id }); log.DEBUG("Node update object: ", onlineNode); updateNodeInfo(onlineNode, (sqlResponse) => { if (!sqlResponse) this.log.ERROR("No response from SQL object"); log.DEBUG("Updated node: ", sqlResponse); return true }) } else { log.DEBUG("No response from node, assuming it's offline"); const offlineNode = new nodeObject({ _online: false, _id: node.id }); log.DEBUG("Offline node update object: ", offlineNode); updateNodeInfo(offlineNode, (sqlResponse) => { if (!sqlResponse) this.log.ERROR("No response from SQL object"); log.DEBUG("Updated offline node: ", sqlResponse); return false }) } }) } exports.checkInWithNode = checkInWithNode; /** * Check in with all online nodes and mark any nodes that are actually offline */ async function checkInWithOnlineNodes() { getOnlineNodes((nodes) => { log.DEBUG("Online Nodes: ", nodes); for (const node of nodes) { checkInWithNode(node); } return; }); } exports.checkInWithOnlineNodes = checkInWithOnlineNodes; /** * * @param {*} req Default express req from router * @param {*} res Defualt express res from router */ exports.listAllNodes = async (req, res) => { getAllNodes((allNodes) => { res.status(200).json({ "nodes_online": allNodes }); }); } /** * Add a new node to the storage * @param {*} req Default express req from router * @param {*} res Defualt express res from router */ exports.newNode = async (req, res) => { if (!req.body.name) return res.status(400).json("No name specified for new node"); try { // Try to add the new user with defaults if missing options const newNode = new nodeObject({ _name: req.body.name, _ip: req.body.ip ?? null, _port: req.body.port ?? null, _location: req.body.location ?? null, _nearbySystems: req.body.nearbySystems ?? null, _online: (req.body.online == "true" || req.body.online == "True") ? true : false }); addNewNode(newNode, (newNodeObject) => { // Send back a success if the user has been added and the ID for the client to keep track of res.status(202).json({ "nodeId": newNodeObject.id }); }) } catch (err) { // Catch any errors if (err === "No name provided") { return res.sendStatus(400); } else log.ERROR(err) return res.sendStatus(500); } } /** Get the known info for the node specified * * @param {*} req Default express req from router * @param {*} res Defualt express res from router */ exports.getNodeInfo = async (req, res) => { if (!req.params.id) return res.status(400).json("No id specified"); getNodeInfoFromId(req.params.id, (nodeInfo) => { res.status(200).json(nodeInfo); }) } /** Adds a specific system/preset on a given node * * @param {*} req Default express req from router * @param {*} res Defualt express res from router * @param {*} req.params.nodeId The Node ID to add the preset/system to * @param {*} req.body.systemName The name of the system to add * @param {*} req.body.mode The radio mode of the preset * @param {*} req.body.frequencies The frequencies of the preset * @param {*} req.body.trunkFile The trunk file to use for digital stations */ exports.addNodeSystem = async (req, res) => { if (!req.params.nodeId) return res.status(400).json("No id specified"); if (!req.body.systemName) return res.status(400).json("No system specified"); log.DEBUG("Adding system for node: ", req.params.nodeId, req.body); getNodeInfoFromId(req.params.nodeId, (node) => { const reqOptions = new requestOptions("/client/addPreset", "POST", node.ip, node.port); const reqBody = { 'systemName': req.body.systemName, 'mode': req.body.mode, 'frequencies': req.body.frequencies, } if(digitalModes.includes(req.body.mode)) reqBody['trunkFile'] = req.body.trunkFile ?? 'none' log.DEBUG("Request body for adding node system: ", reqBody, reqOptions); sendHttpRequest(reqOptions, JSON.stringify(reqBody), async (responseObj) => { if(responseObj){ // Good log.DEBUG("Response from adding node system: ", reqBody, responseObj); return res.sendStatus(200) } else { // Bad log.DEBUG("No Response from adding Node system"); return res.status(400).json("No Response from adding Node, could be offline"); } }) }) } /** Updates a specific system/preset on a given node * * @param {*} req Default express req from router * @param {*} res Defualt express res from router * @param {*} req.params.nodeId The Node ID to update the preset/system on * @param {*} req.body.systemName The name of the system to update * @param {*} req.body.mode The radio mode of the preset to * @param {*} req.body.frequencies The frequencies of the preset * @param {*} req.body.trunkFile The trunk file to use for digital stations */ exports.updateNodeSystem = async (req, res) => { if (!req.params.nodeId) return res.status(400).json("No id specified"); if (!req.body.systemName) return res.status(400).json("No system specified"); log.DEBUG("Updating system for node: ", req.params.nodeId, req.body); getNodeInfoFromId(req.params.nodeId, (node) => { const reqOptions = new requestOptions("/client/updatePreset", "POST", node.ip, node.port); const reqBody = { 'systemName': req.body.systemName, 'mode': req.body.mode, 'frequencies': req.body.frequencies, } if(digitalModes.includes(req.body.mode)) reqBody['trunkFile'] = req.body.trunkFile ?? 'none' log.DEBUG("Request body for updating node: ", reqBody, reqOptions); sendHttpRequest(reqOptions, JSON.stringify(reqBody), async (responseObj) => { if(responseObj){ // Good log.DEBUG("Response from updating node system: ", reqBody, responseObj); return res.sendStatus(200) } else { // Bad log.DEBUG("No Response from updating Node system"); return res.status(400).json("No Response from updating Node, could be offline"); } }) }) } /** Deletes a specific system/preset from a given node * * @param {*} req Default express req from router * @param {*} res Defualt express res from router * @param {*} req.params.nodeId The Node ID to update the preset/system on * @param {*} req.body.systemName The name of the system to update */ exports.removeNodeSystem = async (req, res) => { if (!req.params.nodeId) return res.status(400).json("No id specified"); if (!req.body.systemName) return res.status(400).json("No system specified"); log.DEBUG("Updating system for node: ", req.params.nodeId, req.body); getNodeInfoFromId(req.params.nodeId, (node) => { const reqOptions = new requestOptions("/client/removePreset", "POST", node.ip, node.port); const reqBody = { 'systemName': req.body.systemName } log.DEBUG("Request body for deleting preset: ", reqBody, reqOptions); sendHttpRequest(reqOptions, JSON.stringify(reqBody), async (responseObj) => { if(responseObj){ // Good log.DEBUG("Response from deleting preset: ", reqBody, responseObj); return res.sendStatus(200) } else { // Bad log.DEBUG("No Response from deleting preset"); return res.status(400).json("No Response from deleting preset, could be offline"); } }) }) } /** Updates the information received from the client based on ID * * @param {*} req Default express req from router * @param {*} res Defualt express res from router */ exports.updateExistingNode = async = (req, res) => { if (!req.params.nodeId) return res.status(400).json("No id specified"); getNodeInfoFromId(req.params.nodeId, (nodeInfo) => { let checkInObject = {}; // Convert the online status to a boolean to be worked with log.DEBUG("REQ Body: ", req.body); var isObjectUpdated = false; if (req.body.name && req.body.name != nodeInfo.name) { checkInObject._name = req.body.name; isObjectUpdated = true; } if (req.body.ip && req.body.ip != nodeInfo.ip) { checkInObject._ip = req.body.ip; isObjectUpdated = true; } if (req.body.port && req.body.port != nodeInfo.port) { checkInObject._port = req.body.port; isObjectUpdated = true; } if (req.body.location && req.body.location != nodeInfo.location) { checkInObject._location = req.body.location; isObjectUpdated = true; } if (req.body.nearbySystems && JSON.stringify(req.body.nearbySystems) !== JSON.stringify(nodeInfo.nearbySystems)) { checkInObject._nearbySystems = req.body.nearbySystems; isObjectUpdated = true; } if (req.body.online != nodeInfo.online || req.body.online && (req.body.online === "true") != nodeInfo.online) { checkInObject._online = req.body.online; isObjectUpdated = true; } // If no changes are made tell the client if (!isObjectUpdated) return res.status(200).json("No keys updated"); log.INFO("Updating the following keys for ID: ", req.params.nodeId, checkInObject); checkInObject._id = req.params.nodeId; checkInObject = new nodeObject(checkInObject); if (!nodeInfo) { log.WARN("No existing node found with this ID, adding node: ", checkInObject); addNewNode(checkInObject, async (newNode) => { await checkInWithNode(newNode); return res.status(201).json({ "updatedKeys": newNode }); }); } else { updateNodeInfo(checkInObject, async () => { await checkInWithNode(nodeInfo); return res.status(202).json({ "updatedKeys": checkInObject }); }); } }); } /** Allows the bots to check in and get any updates from the server * * @param {*} req Default express req from router * @param {*} res Defualt express res from router */ exports.nodeCheckIn = async (req, res) => { if (!req.params.nodeId) return res.status(400).json("No id specified"); getNodeInfoFromId(req.params.nodeId, (nodeInfo) => { if (!nodeInfo) return this.newNode(req, res); if (!nodeInfo.online) { nodeInfo.online = true; updateNodeInfo(nodeInfo, () => { return res.status(200).json(nodeInfo); }) } else return res.status(200).json(nodeInfo); }); } /** * Requests a specific node to check in with the server, if it's online * * @param {*} req Default express req from router * @param {*} res Defualt express res from router */ exports.requestNodeCheckIn = async (req, res) => { if (!req.params.nodeId) return res.status(400).json("No Node ID supplied in request"); const node = await getNodeInfoFromId(req.params.nodeId); if (!node) return res.status(400).json("No Node with the ID given"); await checkInWithNode(node); if (res) res.sendStatus(200); } /** * The node monitor service, this will periodically check in on the online nodes to make sure they are still online */ exports.nodeMonitorService = class nodeMonitorService { constructor() { this.log = new DebugBuilder("server", "nodeMonitorService"); } /** * Start the node monitor service in the background */ async start() { // Wait for the a portion of the refresh period before checking in with the nodes, so the rest of the bot can start await new Promise(resolve => setTimeout(resolve, refreshInterval / 10)); log.INFO("Starting Node Monitor Service"); // Check in before starting the infinite loop await checkInWithOnlineNodes(); while (true) { // Wait for the refresh interval, then wait for the posts to return, then wait a quarter of the refresh interval to make sure everything is cleared up await new Promise(resolve => setTimeout(resolve, refreshInterval)); await checkInWithOnlineNodes(); await new Promise(resolve => setTimeout(resolve, refreshInterval / 4)); continue; } } }