From ce072d92874bf107de71b444e71ca34c55851897 Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Sat, 18 Feb 2023 20:41:43 -0500 Subject: [PATCH] Major update - Working client server interactions - Can create radio config - Needs radio testing --- .gitignore | 1 + Client/.env | 1 + Client/app.js | 4 + Client/config/clientConfig.js | 6 + Client/config/radioPresets.json | 2 +- Client/controllers/botController.js | 3 +- Client/controllers/radioController.js | 147 ++++++++ Client/package-lock.json | 327 ++++++++++++++++++ Client/package.json | 1 + Client/routes/bot.js | 11 +- Client/routes/radio.js | 30 ++ Client/utilities/radioConfigHelper.js | 161 +++++++++ Client/utilities/updatePresets.js | 23 +- Server/.env | 1 + Server/.vscode/launch.json | 20 ++ Server/bin/www | 2 + Server/config/discordConfig.js | 5 + Server/controllers/adminController.js | 129 +++++++ Server/discord-admin-bot/app.js | 150 ++++++++ Server/discord-admin-bot/commands/ping.js | 6 + .../discord-admin-bot/config/botConfig.json | 7 + Server/routes/admin.js | 13 +- Server/utilities/httpRequests.js | 68 ++++ Server/utilities/mysqlHandler.js | 23 +- Server/utilities/utils.js | 12 +- 25 files changed, 1114 insertions(+), 39 deletions(-) create mode 100644 Client/.env create mode 100644 Client/controllers/radioController.js create mode 100644 Client/routes/radio.js create mode 100644 Client/utilities/radioConfigHelper.js create mode 100644 Server/.env create mode 100644 Server/.vscode/launch.json create mode 100644 Server/config/discordConfig.js create mode 100644 Server/controllers/adminController.js create mode 100644 Server/discord-admin-bot/app.js create mode 100644 Server/discord-admin-bot/commands/ping.js create mode 100644 Server/discord-admin-bot/config/botConfig.json create mode 100644 Server/utilities/httpRequests.js diff --git a/.gitignore b/.gitignore index 891989e..a14e113 100644 --- a/.gitignore +++ b/.gitignore @@ -4,4 +4,5 @@ node_modules/ # Development files *.log +*.txt diff --git a/Client/.env b/Client/.env new file mode 100644 index 0000000..58fb251 --- /dev/null +++ b/Client/.env @@ -0,0 +1 @@ +DEBUG="client:*"; \ No newline at end of file diff --git a/Client/app.js b/Client/app.js index 6be566d..e199b0c 100644 --- a/Client/app.js +++ b/Client/app.js @@ -7,6 +7,7 @@ var logger = require('morgan'); var indexRouter = require('./routes/index'); var botRouter = require('./routes/bot'); var clientRouter = require('./routes/client'); +var radioRouter = require('./routes/radio'); var app = express(); @@ -28,6 +29,9 @@ app.use('/bot', botRouter); // Local client control route app.use("/client", clientRouter); +// Local radio controller route +app.use("/radio", radioRouter); + // catch 404 and forward to error handler app.use(function(req, res, next) { next(createError(404)); diff --git a/Client/config/clientConfig.js b/Client/config/clientConfig.js index 7090f42..c9977dc 100644 --- a/Client/config/clientConfig.js +++ b/Client/config/clientConfig.js @@ -1,4 +1,5 @@ // Core config settings for the node, these are the settings that are checked with the server +const path = require("path"); exports.clientConfig = { "id": 13, "name": "boilin balls in the hall", @@ -14,4 +15,9 @@ exports.serverConfig = { "ip": "127.0.0.1", "hostname": "localhost", "port": 3000 +} + +// Configuration of the local OP25 application +exports.radioAppConfig = { + "bin": "H:/Logan/Projects/Discord-Radio-Bot-CnC/Client/.idea/testOP25Dir/multi_rx.py" } \ No newline at end of file diff --git a/Client/config/radioPresets.json b/Client/config/radioPresets.json index e46d278..368539b 100644 --- a/Client/config/radioPresets.json +++ b/Client/config/radioPresets.json @@ -1 +1 @@ -{"Westchester Cty. Simulcast":{"frequencies":[470575000,470375000,470525000,470575000,470550000],"mode":"p25","trunkFile":"trunk.tsv"}} \ No newline at end of file +{"Westchester Cty. Simulcast":{"frequencies":[470575000,470375000,470525000,470575000,470550000],"mode":"p25","trunkFile":"trunk.tsv"},"coppies":{"frequencies":[154875000],"mode":"nbfm","trunkFile":"none"}} \ No newline at end of file diff --git a/Client/controllers/botController.js b/Client/controllers/botController.js index ac68a30..c5ec0a6 100644 --- a/Client/controllers/botController.js +++ b/Client/controllers/botController.js @@ -6,7 +6,7 @@ const path = require('path'); const fork = require('child_process').fork; const discordBotPath = path.resolve('discord-bot/app.js'); -let botChildProcess, radioChildProcess, tempRes; +let botChildProcess, tempRes; /** * Bot Process Object Builder @@ -67,6 +67,7 @@ exports.joinServer = (req, res) => { tempRes.sendStatus(202); tempRes = undefined; botChildProcess.kill(); + botChildProcess = undefined; return; case "ChgPreSet": tempRes.sendStatus(200); diff --git a/Client/controllers/radioController.js b/Client/controllers/radioController.js new file mode 100644 index 0000000..943693a --- /dev/null +++ b/Client/controllers/radioController.js @@ -0,0 +1,147 @@ +// Debug +const { DebugBuilder } = require("../utilities/debugBuilder.js"); +const log = new DebugBuilder("client", "radioController"); +// Modules +const { resolve, dirname } = require('path'); +const fs = require('fs'); +const radioConfig = require('../config/clientConfig').radioAppConfig; +const radioConfigHelper = require("../utilities/radioConfigHelper"); +const presetWrappers = require("../utilities/updatePresets"); +const spawn = require('child_process').spawn; +const converter = require("convert-units"); + +let radioChildProcess, tempRes, radioConfigPath; + +/** + * Closes the radio executable if it's in one + */ +exports.closeRadioSession = (req, res) => { + if (!radioChildProcess) return res.sendStatus(200) + tempRes = res; + radioChildProcess.kill(); + radioChildProcess = undefined; +} + +/** + * Change the current 'cfg.json' file to the preset specified + * @param {string} presetName + */ +exports.changeCurrentConfig = (req, res) => { + // Check if the given config is saved + log.DEBUG("[/radio/changeCurrentConfig] - Checking if provided preset is in the config"); + if (!checkIfPresetExists(req.body.presetName)) return res.status(500).JSON("No preset with given name found in config"); // No preset with the given name is in the config + + // Check if the current config is the same as the preset given + const currentConfig = readOP25Config(); + if (currentConfig.channels && currentConfig.channels.name === req.body.presetName) { + log.DEBUG("[/radio/changeCurrentConfig] - Current config is the same as the preset given"); + return res.sendStatus(202); + } + + // Convert radioPreset to OP25 'cfg.json. file + log.DEBUG("[/radio/changeCurrentConfig] - Converting radioPreset to OP25 config"); + const updatedConfigObject = convertRadioPresetsToOP25Config(req.body.presetName); + + // Replace current JSON file with the updated file + writeOP25Config(updatedConfigObject, () => { + res.sendStatus(200); + }) +} + +/** + * Open a new OP25 process tuned to the specified system + */ +exports.openRadioSession = () => { + if (radioChildProcess) closeRadioSession(); + radioChildProcess = spawn(getRadioBinPath()); +} + +/** + * Get the location of the 'multi_rx.py' binary from the config + */ +function getRadioBinPath(){ + return resolve(radioConfig.bin); +} + +/** + * Write the given config to the JSON file in OP25 the bin dir + * @param config The full config to be written to the file + * @param {function} callback The function to be called when this wrapper completes + */ +function writeOP25Config(config, callback = undefined) { + log.DEBUG("Updating OP25 config with: ", config); + fs.writeFile(getRadioConfigPath(), JSON.stringify(config), (err) => { + // Error checking + if (err) { + log.ERROR(err); + throw err; + } + log.DEBUG("Write Complete"); + if (callback) callback() + }); +} + +/** + * Get the current config file in use by OP25 + * @returns {object|*} The parsed config object currently set in OP25 + */ +function readOP25Config() { + const configPath = getRadioConfigPath(); + log.DEBUG(`Reading from config path: '${configPath}'`); + return JSON.parse(fs.readFileSync(configPath)); +} + +/** + * Get the path of the config for the radio app (OP25) and set the global variable + */ +function getRadioConfigPath(){ + let radioConfigDirPath = dirname(getRadioBinPath()); + return resolve(`${radioConfigDirPath}/cfg.json`); +} + +/** + * Check to see if the preset name exists in the config + * @param {string} presetName The system name as saved in the preset + * @returns {true||false} + */ +function checkIfPresetExists(presetName) { + const savedPresets = presetWrappers.getPresets(); + if (!Object.keys(savedPresets).includes(presetName)) return false; + else return true; +} + +/** + * Convert a radioPreset to OP25's cfg.json file + */ +function convertRadioPresetsToOP25Config(presetName){ + const savedPresets = presetWrappers.getPresets(); + let frequencyString = ""; + for (const frequency of savedPresets[presetName].frequencies){ + frequencyString += `${converter(frequency).from("Hz").to("MHz")},` + } + frequencyString = frequencyString.slice(0, -1); + + let updatedOP25Config; + switch (savedPresets[presetName].mode){ + case "p25": + updatedOP25Config = new radioConfigHelper.P25({ + "systemName": presetName, + "controlChannelsString": frequencyString, + "tagsFile": savedPresets[presetName].trunkFile + }); + break; + case "nbfm": + //code for nbfm here + updatedOP25Config = new radioConfigHelper.NBFM({ + "frequency": frequencyString, + "systemName": presetName + }); + break; + default: + throw new Error("Radio mode of selected preset not recognized"); + } + + log.DEBUG(updatedOP25Config); + return updatedOP25Config; +} + diff --git a/Client/package-lock.json b/Client/package-lock.json index 579f70e..018bc30 100644 --- a/Client/package-lock.json +++ b/Client/package-lock.json @@ -8,6 +8,7 @@ "name": "client", "version": "0.0.0", "dependencies": { + "convert-units": "^2.3.4", "cookie-parser": "~1.4.4", "debug": "~2.6.9", "ejs": "~2.6.1", @@ -174,6 +175,15 @@ "node": ">= 0.6" } }, + "node_modules/convert-units": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/convert-units/-/convert-units-2.3.4.tgz", + "integrity": "sha512-ERHfdA0UhHJp1IpwE6PnFJx8LqG7B1ZjJ20UvVCmopEnVCfER68Tbe3kvN63dLbYXDA2xFWRE6zd4Wsf0w7POg==", + "dependencies": { + "lodash.foreach": "2.3.x", + "lodash.keys": "2.3.x" + } + }, "node_modules/cookie": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", @@ -443,6 +453,160 @@ "node": ">=8" } }, + "node_modules/lodash._basebind": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash._basebind/-/lodash._basebind-2.3.0.tgz", + "integrity": "sha512-SHqM7YCuJ+BeGTs7lqpWnmdHEeF4MWxS3dksJctHFNxR81FXPOzA4bS5Vs5CpcGTkBpM8FCl+YEbQEblRw8ABg==", + "dependencies": { + "lodash._basecreate": "~2.3.0", + "lodash._setbinddata": "~2.3.0", + "lodash.isobject": "~2.3.0" + } + }, + "node_modules/lodash._basecreate": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-2.3.0.tgz", + "integrity": "sha512-vwZaWldZwS2y9b99D8i9+WtgiZXbHKsBsMrpxJEqTsNW20NhJo5W8PBQkeQO9CmxuqEYn8UkMnfEM2MMT4cVrw==", + "dependencies": { + "lodash._renative": "~2.3.0", + "lodash.isobject": "~2.3.0", + "lodash.noop": "~2.3.0" + } + }, + "node_modules/lodash._basecreatecallback": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash._basecreatecallback/-/lodash._basecreatecallback-2.3.0.tgz", + "integrity": "sha512-Ev+pDzzfVfgbiucpXijconLGRBar7/+KNCf05kSnk4CmdDVhAy1RdbU9efCJ/o9GXI08JdUGwZ+5QJ3QX3kj0g==", + "dependencies": { + "lodash._setbinddata": "~2.3.0", + "lodash.bind": "~2.3.0", + "lodash.identity": "~2.3.0", + "lodash.support": "~2.3.0" + } + }, + "node_modules/lodash._basecreatewrapper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash._basecreatewrapper/-/lodash._basecreatewrapper-2.3.0.tgz", + "integrity": "sha512-YLycQ7k8AB9Wc1EOvLNxuRWcqipDkMXq2GCgnLWQR6qtgTb3gY3LELzEpnFshrEO4LOLs+R2EpcY+uCOZaLQ8Q==", + "dependencies": { + "lodash._basecreate": "~2.3.0", + "lodash._setbinddata": "~2.3.0", + "lodash._slice": "~2.3.0", + "lodash.isobject": "~2.3.0" + } + }, + "node_modules/lodash._createwrapper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash._createwrapper/-/lodash._createwrapper-2.3.0.tgz", + "integrity": "sha512-XjaI/rzg9W+WO4WJDQ+PRlHD5sAMJ1RhJLuT65cBxLCb1kIYs4U20jqvTDGAWyVT3c34GYiLd9AreHYuB/8yJA==", + "dependencies": { + "lodash._basebind": "~2.3.0", + "lodash._basecreatewrapper": "~2.3.0", + "lodash.isfunction": "~2.3.0" + } + }, + "node_modules/lodash._objecttypes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.3.0.tgz", + "integrity": "sha512-jbA6QyHt9cw3BzvbWzIcnU3Z12jSneT6xBgz3Y782CJsN1tV5aTBKrFo2B4AkeHBNaxSrbPYZZpi1Lwj3xjdtg==" + }, + "node_modules/lodash._renative": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash._renative/-/lodash._renative-2.3.0.tgz", + "integrity": "sha512-v44MRirqYqZGK/h5UKoVqXWF2L+LUiLTU+Ogu5rHRVWJUA1uWIlHaMpG8f/OA8j++BzPMQij9+erXHtgFcbuwg==" + }, + "node_modules/lodash._setbinddata": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash._setbinddata/-/lodash._setbinddata-2.3.0.tgz", + "integrity": "sha512-xMFfbF7dL+sFtrdE49uHFmfpBAEwlFtfgMp86nQRlAF6aizYL+3MTbnYMKJSkP1W501PhsgiBED5kBbZd8kR2g==", + "dependencies": { + "lodash._renative": "~2.3.0", + "lodash.noop": "~2.3.0" + } + }, + "node_modules/lodash._shimkeys": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash._shimkeys/-/lodash._shimkeys-2.3.0.tgz", + "integrity": "sha512-9Iuyi7TiWMGa/9+2rqEE+Zwye4b/U2w7Saw6UX1h6Xs88mEER+uz9FZcEBPKMVKsad9Pw5GNAcIBRnW2jNpneQ==", + "dependencies": { + "lodash._objecttypes": "~2.3.0" + } + }, + "node_modules/lodash._slice": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash._slice/-/lodash._slice-2.3.0.tgz", + "integrity": "sha512-7C61GhzRUv36gTafr+RIb+AomCAYsSATEoK4OP0VkNBcwvsM022Z22AVgqjjzikeNO1U29LzsJZDvLbiNPUYvA==" + }, + "node_modules/lodash.bind": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-2.3.0.tgz", + "integrity": "sha512-goakyOo+FMN8lttMPnZ0UNlr5RlzX4IrUXyTJPT2A0tGCMXySupond9wzvDqTvVmYTcQjIKGrj8naJDS2xWAlQ==", + "dependencies": { + "lodash._createwrapper": "~2.3.0", + "lodash._renative": "~2.3.0", + "lodash._slice": "~2.3.0" + } + }, + "node_modules/lodash.foreach": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-2.3.0.tgz", + "integrity": "sha512-yLnyptVRJd0//AbGp480grgQG9iaDIV5uOgSbpurRy1dYybPbjNTLQ3FyLEQ84buVLPG7jyaiyvpzgfOutRB3Q==", + "dependencies": { + "lodash._basecreatecallback": "~2.3.0", + "lodash.forown": "~2.3.0" + } + }, + "node_modules/lodash.forown": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash.forown/-/lodash.forown-2.3.0.tgz", + "integrity": "sha512-dUnCsuQTtq3Y7bxPNoEEqjJjPL2ftLtcz2PTeRKvhbpdM514AvnqCjewHGsm/W+dwspIwa14KoWEZeizJ7smxA==", + "dependencies": { + "lodash._basecreatecallback": "~2.3.0", + "lodash._objecttypes": "~2.3.0", + "lodash.keys": "~2.3.0" + } + }, + "node_modules/lodash.identity": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash.identity/-/lodash.identity-2.3.0.tgz", + "integrity": "sha512-NYJ2r2cwy3tkx/saqbIZEX6oQUzjWTnGRu7d/zmBjMCZos3eHBxCpbvWFWSetv8jFVrptsp6EbWjzNgBKhUoOA==" + }, + "node_modules/lodash.isfunction": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-2.3.0.tgz", + "integrity": "sha512-X5lteBYlCrVO7Qc00fxP8W90fzRp6Ax9XcHANmU3OsZHdSyIVZ9ZlX5QTTpRq8aGY+9I5Rmd0UTzTIIyWPugEQ==" + }, + "node_modules/lodash.isobject": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.3.0.tgz", + "integrity": "sha512-jo1pfV61C4TE8BfEzqaHj6EIKiSkFANJrB6yscwuCJMSRw5tbqjk4Gv7nJzk4Z6nFKobZjGZ8Qd41vmnwgeQqQ==", + "dependencies": { + "lodash._objecttypes": "~2.3.0" + } + }, + "node_modules/lodash.keys": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.3.0.tgz", + "integrity": "sha512-c0UW0ffqMxSCtoVbmVt2lERJLkEqgoOn2ejPsWXzr0ZrqRbl3uruGgwHzhtqXxi6K/ei3Ey7zimOqSwXgzazPg==", + "dependencies": { + "lodash._renative": "~2.3.0", + "lodash._shimkeys": "~2.3.0", + "lodash.isobject": "~2.3.0" + } + }, + "node_modules/lodash.noop": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash.noop/-/lodash.noop-2.3.0.tgz", + "integrity": "sha512-NpSm8HRm1WkBBWHUveDukLF4Kfb5P5E3fjHc9Qre9A11nNubozLWD2wH3UBTZbu+KSuX8aSUvy9b+PUyEceJ8g==" + }, + "node_modules/lodash.support": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash.support/-/lodash.support-2.3.0.tgz", + "integrity": "sha512-etc7VWbB0U3Iya8ixj2xy4sDBN3jvPX7ODi8iXtn4KkkjNpdngrdc7Vlt5jub/Vgqx6/dWtp7Ml9awhCQPYKGQ==", + "dependencies": { + "lodash._renative": "~2.3.0" + } + }, "node_modules/media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", @@ -949,6 +1113,15 @@ "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.4.tgz", "integrity": "sha512-hIP3EEPs8tB9AT1L+NUqtwOAps4mk2Zob89MWXMHjHWg9milF/j4osnnQLXBCBFBk/tvIG/tUc9mOUJiPBhPXA==" }, + "convert-units": { + "version": "2.3.4", + "resolved": "https://registry.npmjs.org/convert-units/-/convert-units-2.3.4.tgz", + "integrity": "sha512-ERHfdA0UhHJp1IpwE6PnFJx8LqG7B1ZjJ20UvVCmopEnVCfER68Tbe3kvN63dLbYXDA2xFWRE6zd4Wsf0w7POg==", + "requires": { + "lodash.foreach": "2.3.x", + "lodash.keys": "2.3.x" + } + }, "cookie": { "version": "0.4.1", "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.1.tgz", @@ -1160,6 +1333,160 @@ "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==" }, + "lodash._basebind": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash._basebind/-/lodash._basebind-2.3.0.tgz", + "integrity": "sha512-SHqM7YCuJ+BeGTs7lqpWnmdHEeF4MWxS3dksJctHFNxR81FXPOzA4bS5Vs5CpcGTkBpM8FCl+YEbQEblRw8ABg==", + "requires": { + "lodash._basecreate": "~2.3.0", + "lodash._setbinddata": "~2.3.0", + "lodash.isobject": "~2.3.0" + } + }, + "lodash._basecreate": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-2.3.0.tgz", + "integrity": "sha512-vwZaWldZwS2y9b99D8i9+WtgiZXbHKsBsMrpxJEqTsNW20NhJo5W8PBQkeQO9CmxuqEYn8UkMnfEM2MMT4cVrw==", + "requires": { + "lodash._renative": "~2.3.0", + "lodash.isobject": "~2.3.0", + "lodash.noop": "~2.3.0" + } + }, + "lodash._basecreatecallback": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash._basecreatecallback/-/lodash._basecreatecallback-2.3.0.tgz", + "integrity": "sha512-Ev+pDzzfVfgbiucpXijconLGRBar7/+KNCf05kSnk4CmdDVhAy1RdbU9efCJ/o9GXI08JdUGwZ+5QJ3QX3kj0g==", + "requires": { + "lodash._setbinddata": "~2.3.0", + "lodash.bind": "~2.3.0", + "lodash.identity": "~2.3.0", + "lodash.support": "~2.3.0" + } + }, + "lodash._basecreatewrapper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash._basecreatewrapper/-/lodash._basecreatewrapper-2.3.0.tgz", + "integrity": "sha512-YLycQ7k8AB9Wc1EOvLNxuRWcqipDkMXq2GCgnLWQR6qtgTb3gY3LELzEpnFshrEO4LOLs+R2EpcY+uCOZaLQ8Q==", + "requires": { + "lodash._basecreate": "~2.3.0", + "lodash._setbinddata": "~2.3.0", + "lodash._slice": "~2.3.0", + "lodash.isobject": "~2.3.0" + } + }, + "lodash._createwrapper": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash._createwrapper/-/lodash._createwrapper-2.3.0.tgz", + "integrity": "sha512-XjaI/rzg9W+WO4WJDQ+PRlHD5sAMJ1RhJLuT65cBxLCb1kIYs4U20jqvTDGAWyVT3c34GYiLd9AreHYuB/8yJA==", + "requires": { + "lodash._basebind": "~2.3.0", + "lodash._basecreatewrapper": "~2.3.0", + "lodash.isfunction": "~2.3.0" + } + }, + "lodash._objecttypes": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.3.0.tgz", + "integrity": "sha512-jbA6QyHt9cw3BzvbWzIcnU3Z12jSneT6xBgz3Y782CJsN1tV5aTBKrFo2B4AkeHBNaxSrbPYZZpi1Lwj3xjdtg==" + }, + "lodash._renative": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash._renative/-/lodash._renative-2.3.0.tgz", + "integrity": "sha512-v44MRirqYqZGK/h5UKoVqXWF2L+LUiLTU+Ogu5rHRVWJUA1uWIlHaMpG8f/OA8j++BzPMQij9+erXHtgFcbuwg==" + }, + "lodash._setbinddata": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash._setbinddata/-/lodash._setbinddata-2.3.0.tgz", + "integrity": "sha512-xMFfbF7dL+sFtrdE49uHFmfpBAEwlFtfgMp86nQRlAF6aizYL+3MTbnYMKJSkP1W501PhsgiBED5kBbZd8kR2g==", + "requires": { + "lodash._renative": "~2.3.0", + "lodash.noop": "~2.3.0" + } + }, + "lodash._shimkeys": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash._shimkeys/-/lodash._shimkeys-2.3.0.tgz", + "integrity": "sha512-9Iuyi7TiWMGa/9+2rqEE+Zwye4b/U2w7Saw6UX1h6Xs88mEER+uz9FZcEBPKMVKsad9Pw5GNAcIBRnW2jNpneQ==", + "requires": { + "lodash._objecttypes": "~2.3.0" + } + }, + "lodash._slice": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash._slice/-/lodash._slice-2.3.0.tgz", + "integrity": "sha512-7C61GhzRUv36gTafr+RIb+AomCAYsSATEoK4OP0VkNBcwvsM022Z22AVgqjjzikeNO1U29LzsJZDvLbiNPUYvA==" + }, + "lodash.bind": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-2.3.0.tgz", + "integrity": "sha512-goakyOo+FMN8lttMPnZ0UNlr5RlzX4IrUXyTJPT2A0tGCMXySupond9wzvDqTvVmYTcQjIKGrj8naJDS2xWAlQ==", + "requires": { + "lodash._createwrapper": "~2.3.0", + "lodash._renative": "~2.3.0", + "lodash._slice": "~2.3.0" + } + }, + "lodash.foreach": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-2.3.0.tgz", + "integrity": "sha512-yLnyptVRJd0//AbGp480grgQG9iaDIV5uOgSbpurRy1dYybPbjNTLQ3FyLEQ84buVLPG7jyaiyvpzgfOutRB3Q==", + "requires": { + "lodash._basecreatecallback": "~2.3.0", + "lodash.forown": "~2.3.0" + } + }, + "lodash.forown": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash.forown/-/lodash.forown-2.3.0.tgz", + "integrity": "sha512-dUnCsuQTtq3Y7bxPNoEEqjJjPL2ftLtcz2PTeRKvhbpdM514AvnqCjewHGsm/W+dwspIwa14KoWEZeizJ7smxA==", + "requires": { + "lodash._basecreatecallback": "~2.3.0", + "lodash._objecttypes": "~2.3.0", + "lodash.keys": "~2.3.0" + } + }, + "lodash.identity": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash.identity/-/lodash.identity-2.3.0.tgz", + "integrity": "sha512-NYJ2r2cwy3tkx/saqbIZEX6oQUzjWTnGRu7d/zmBjMCZos3eHBxCpbvWFWSetv8jFVrptsp6EbWjzNgBKhUoOA==" + }, + "lodash.isfunction": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-2.3.0.tgz", + "integrity": "sha512-X5lteBYlCrVO7Qc00fxP8W90fzRp6Ax9XcHANmU3OsZHdSyIVZ9ZlX5QTTpRq8aGY+9I5Rmd0UTzTIIyWPugEQ==" + }, + "lodash.isobject": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.3.0.tgz", + "integrity": "sha512-jo1pfV61C4TE8BfEzqaHj6EIKiSkFANJrB6yscwuCJMSRw5tbqjk4Gv7nJzk4Z6nFKobZjGZ8Qd41vmnwgeQqQ==", + "requires": { + "lodash._objecttypes": "~2.3.0" + } + }, + "lodash.keys": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.3.0.tgz", + "integrity": "sha512-c0UW0ffqMxSCtoVbmVt2lERJLkEqgoOn2ejPsWXzr0ZrqRbl3uruGgwHzhtqXxi6K/ei3Ey7zimOqSwXgzazPg==", + "requires": { + "lodash._renative": "~2.3.0", + "lodash._shimkeys": "~2.3.0", + "lodash.isobject": "~2.3.0" + } + }, + "lodash.noop": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash.noop/-/lodash.noop-2.3.0.tgz", + "integrity": "sha512-NpSm8HRm1WkBBWHUveDukLF4Kfb5P5E3fjHc9Qre9A11nNubozLWD2wH3UBTZbu+KSuX8aSUvy9b+PUyEceJ8g==" + }, + "lodash.support": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/lodash.support/-/lodash.support-2.3.0.tgz", + "integrity": "sha512-etc7VWbB0U3Iya8ixj2xy4sDBN3jvPX7ODi8iXtn4KkkjNpdngrdc7Vlt5jub/Vgqx6/dWtp7Ml9awhCQPYKGQ==", + "requires": { + "lodash._renative": "~2.3.0" + } + }, "media-typer": { "version": "0.3.0", "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", diff --git a/Client/package.json b/Client/package.json index 0826685..5076056 100644 --- a/Client/package.json +++ b/Client/package.json @@ -6,6 +6,7 @@ "start": "node ./bin/www" }, "dependencies": { + "convert-units": "^2.3.4", "cookie-parser": "~1.4.4", "debug": "~2.6.9", "ejs": "~2.6.1", diff --git a/Client/routes/bot.js b/Client/routes/bot.js index 40ed5c2..858ebdd 100644 --- a/Client/routes/bot.js +++ b/Client/routes/bot.js @@ -1,6 +1,6 @@ -var express = require('express'); +const express = require('express'); +const router = express.Router(); const botController = require("../controllers/botController"); -var router = express.Router(); /** GET bot status * Check to see if the bot is online and if so, if it is currently connected to anything @@ -27,11 +27,4 @@ router.post('/join', botController.joinServer); */ router.post('/leave', botController.leaveServer); -/** POST change bot preset - * This will change the bot to the preset specified (if different from what is currently playing) - * - * The status of the bot: 200 = no change, 202 = changed successfully, 500 + JSON = encountered error - * @returns status - */ - module.exports = router; diff --git a/Client/routes/radio.js b/Client/routes/radio.js new file mode 100644 index 0000000..c5f10ac --- /dev/null +++ b/Client/routes/radio.js @@ -0,0 +1,30 @@ +// Controllers +const radioController = require("../controllers/radioController"); +// Debug +const { DebugBuilder } = require("../utilities/debugBuilder.js"); +const log = new DebugBuilder("client", "clientController"); +// Modules +const express = require('express'); +const router = express.Router(); + +/** + * POST Open a new radio session + * This will open OP25 to the current specified radio settings + */ +router.post('/start', radioController.openRadioSession); + +/** + * POST Close the current radio session + */ +router.post('/stop', radioController.closeRadioSession); + +/** POST change current radio preset + * This will change the bot to the preset specified (if different from what is currently playing) + * @param req The request sent from the master + * @param {string} req.body.presetName The name of the system to be updated + * The status of the bot: 200 = no change, 202 = changed successfully, 500 + JSON = encountered error + * @returns status + */ +router.post('/changeConfig', radioController.changeCurrentConfig); + +module.exports = router; \ No newline at end of file diff --git a/Client/utilities/radioConfigHelper.js b/Client/utilities/radioConfigHelper.js new file mode 100644 index 0000000..a843662 --- /dev/null +++ b/Client/utilities/radioConfigHelper.js @@ -0,0 +1,161 @@ +exports.P25 = class P25ConfigGenerator { + constructor({systemName, controlChannelsString, tagsFile}) { + this.channels = new channelConfig({ + "systemName": systemName, + "enableAnalog": "off", + "demodType": "cqpsk", + "cqpskTracking": true, + "filterType": "rc" + }); + this.devices = new deviceConfig({ + "gain": "LNA:36" + }); + this.trunking = new trunkingConfig({ + "module": "tk_p25.py", + "systemName": systemName, + "controlChannelsString": controlChannelsString, + "tagsFile": tagsFile + }); + this.audio = new audioConfig({}); + this.terminal = new terminalConfig({}); + } +} + +exports.NBFM = class NBFMConfigGenerator { + constructor({systemName, frequency, nbfmSquelch = -70}) { + this.channels = new channelConfig({ + "channelName": systemName, + "enableAnalog": "on", + "nbfmSquelch": nbfmSquelch, + "frequency": frequency, + "demodType": "fsk4", + "filterType": "widepulse" + }); + this.devices = new deviceConfig({ + "gain": "LNA:32" + }); + this.audio = new audioConfig({}); + this.terminal = new terminalConfig({}); + } +} + +class channelConfig { + constructor({ + channelName = "Voice_ch1", + device = "sdr0", + systemName, + metaStreamName, + demodType, // cqpsk: P25; fsk4: everything else + cqpskTracking, + trackingThreshold = 120, + trackingFeedback = 0.75, + destination = "udp://127.0.0.1:23456", + excess_bw = 0.2, + filterType = "rc", // rc: P25; widepulse: analog + ifRate = 24000, + plot = "", + symbolRate = 4800, + enableAnalog, //[on, off, auto] + nbfmDeviation = 4000, // only needed if analog is enabled + nbfmSquelch = -50, // only needed if analog is enabled + frequency, // only needed if analog is enabled + blacklist, + whitelist, + cryptKeys + }){ + // Core Configs + this.name = channelName; + this.device = device; + this.demod_type = demodType; + this.destination = destination; + this.excess_bw = excess_bw; + this.filter_type = filterType; + this.if_rate = ifRate; + this.plot = plot; + this.symbol_rate = symbolRate; + this.enable_analog = enableAnalog; + + // P25 config + if (!enableAnalog || enableAnalog === "off" || systemName) this.trunking_sysname = systemName; + if (!enableAnalog || enableAnalog === "off" || systemName && metaStreamName) this.meta_stream_name = metaStreamName ?? ""; + if (!enableAnalog || enableAnalog === "off" || systemName) this.cqpsk_tracking = cqpskTracking; + if (!enableAnalog || enableAnalog === "off" || systemName) this.tracking_threshold = trackingThreshold; + if (!enableAnalog || enableAnalog === "off" || systemName) this.tracking_feedback = trackingFeedback; + if (!enableAnalog || enableAnalog === "off" || systemName && blacklist) this.blacklist = blacklist ?? ""; + if (!enableAnalog || enableAnalog === "off" || systemName && whitelist) this.whitelist = whitelist ?? ""; + if (!enableAnalog || enableAnalog === "off" || systemName && cryptKeys) this.crypt_keys = cryptKeys ?? ""; + + // Analog config + if (enableAnalog === "on" || enableAnalog === "auto") this.nbfm_deviation = nbfmDeviation; + if (enableAnalog === "on" || enableAnalog === "auto") this.nbfm_squelch = nbfmSquelch; + if (enableAnalog === "on" || enableAnalog === "auto") this.frequency = frequency; + } +} + +class deviceConfig { + constructor({args = "rtl", gain = "LNA:32", gainMode = false, name = "sdr0", offset = 0, ppm = 0.0, sampleRate = 1920000, tunable=true}) { + this.args = args + this.gains = gain + this.gain_mode = gainMode + this.name = name + this.offset = offset + this.ppm = ppm + this.rate = sampleRate + this.usable_bw_pct = 0.85 + this.tunable = tunable + } +} + +class trunkingConfig { + /** + * + * @param {object} * + */ + constructor({module, systemName, controlChannelsString, tagsFile = "", nac = "0x0", wacn = "0x0", cryptBehavior = 2, whitelist = "", blacklist = ""}) { + this.module = module; + this.chans = [{ + "nac": nac, + "wacn": wacn, + "sysname": systemName, + "control_channel_list": controlChannelsString, + "whitelist": whitelist, + "blacklist": blacklist, + "tgid_tags_file": tagsFile, + "tdma_cc": false, + "crypt_behavior": cryptBehavior + }]; + } +} + +class audioConfig { + constructor({module = "sockaudio.py", port = 23456, deviceName = "default"}) { + this.module = module; + this.instances = [{ + "instance_name": "audio0", + "device_name": deviceName, + "udp_port": port, + "audio_gain": 1.0, + "number_channels": 1 + }]; + } +} + +class metadataStreamConfig { + constructor({}) { + this.module = ""; + this.streams = []; + } +} + +class terminalConfig { + constructor({module = "terminal.py", terminalType = "http:0.0.0.0:8080"}) { + this.module = module; + this.terminal_type = terminalType; + this.curses_plot_interval= 0.1; + this.http_plot_interval= 1.0; + this.http_plot_directory = "../www/images"; + this.tuning_step_large= 1200; + this.tuning_step_small = 100; + } +} + diff --git a/Client/utilities/updatePresets.js b/Client/utilities/updatePresets.js index 6a7fee1..2a7b526 100644 --- a/Client/utilities/updatePresets.js +++ b/Client/utilities/updatePresets.js @@ -3,6 +3,8 @@ const { DebugBuilder } = require("../utilities/debugBuilder.js"); const log = new DebugBuilder("client", "updatePresets"); // Modules const fs = require('fs'); +const path = require("path"); +const converter = require("convert-units"); /** * Write the given presets to the JSON file @@ -10,7 +12,8 @@ const fs = require('fs'); * @param {function} callback The function to be called when this wrapper completes */ function writePresets(presets, callback = undefined) { - fs.writeFile("../config/radioPresets.json", JSON.stringify(presets), (err) => { + log.DEBUG(`${__dirname}`); + fs.writeFile("./config/radioPresets.json", JSON.stringify(presets), (err) => { // Error checking if (err) throw err; log.DEBUG("Write Complete"); @@ -37,7 +40,7 @@ function sanitizeFrequencies(frequenciesArray) { /** * Function to convert a string or a float into the integer type needed to be saved * @param frequency Could be a string, number or float, - * @returns {number|number|*} Return the value to be saved in Hz format ("154875000" = 154.875MHz) + * @returns {number|number|*} Return the value to be saved in Hz format ("154.875"MHz format = "154875000") */ function convertFrequencyToHertz(frequency){ // check if the passed value is a number @@ -50,18 +53,7 @@ function convertFrequencyToHertz(frequency){ else { log.DEBUG(`${frequency} is a float value.`); // Convert to a string to remove the decimal in place and then correct the length - frequency = frequency.toString(); - // One extra digit checked for the '.' included in the string - if (frequency.length >= 8 && frequency.length <= 10) return parseInt(frequency.replace(".", "")); - else if (frequency.length <= 7) { - // Check to see if the frequency is 1s, 10s or 100s of MHz - let zerosToBeRemoved = 3 - frequency.split(".")[0].length; - // Need to add more 0s since it was in MHz format - let neededZeros = (9 - frequency.length) - zerosToBeRemoved; - frequency = frequency.replace(".", "") + '0'.repeat(neededZeros) - - return parseInt(frequency); - } + return converter(frequency).from("MHz").to("Hz"); } } else { log.DEBUG(`${frequency} is not a number`); @@ -76,7 +68,8 @@ function convertFrequencyToHertz(frequency){ * @returns {any} The object containing the different systems the bot is near */ exports.getPresets = function getPresets() { - return JSON.parse(fs.readFileSync("../config/radioPresets.json")); + log.DEBUG(`Getting presets from directory: '${__dirname}'`); + return JSON.parse(fs.readFileSync( "./config/radioPresets.json")); } /** diff --git a/Server/.env b/Server/.env new file mode 100644 index 0000000..dbfce5a --- /dev/null +++ b/Server/.env @@ -0,0 +1 @@ +DEBUG="server:*"; \ No newline at end of file diff --git a/Server/.vscode/launch.json b/Server/.vscode/launch.json new file mode 100644 index 0000000..6a8ba47 --- /dev/null +++ b/Server/.vscode/launch.json @@ -0,0 +1,20 @@ +{ + // Use IntelliSense to learn about possible attributes. + // Hover to view descriptions of existing attributes. + // For more information, visit: https://go.microsoft.com/fwlink/?linkid=830387 + "version": "0.2.0", + "configurations": [ + { + "type": "node", + "request": "launch", + "name": "Launch Program", + "skipFiles": [ + "/**" + ], + "program": "${workspaceFolder}\\bin\\www", + "env": { + "DEBUG":"*" + } + } + ] +} \ No newline at end of file diff --git a/Server/bin/www b/Server/bin/www index 39c066d..a34f33b 100644 --- a/Server/bin/www +++ b/Server/bin/www @@ -6,6 +6,7 @@ var app = require('../app'); // Debug +const debug = require('debug')('server'); const { DebugBuilder } = require("../utilities/debugBuilder.js"); const log = new DebugBuilder("server", "www"); var http = require('http'); @@ -89,4 +90,5 @@ function onListening() { ? 'pipe ' + addr : 'port ' + addr.port; log.DEBUG('Listening on ' + bind); + debug("testing"); } diff --git a/Server/config/discordConfig.js b/Server/config/discordConfig.js new file mode 100644 index 0000000..06e4004 --- /dev/null +++ b/Server/config/discordConfig.js @@ -0,0 +1,5 @@ +const discordConfig = { + channelID: '367396189529833476' +} + +module.exports = discordConfig; \ No newline at end of file diff --git a/Server/controllers/adminController.js b/Server/controllers/adminController.js new file mode 100644 index 0000000..f630af4 --- /dev/null +++ b/Server/controllers/adminController.js @@ -0,0 +1,129 @@ +// Config +const discordConfig = require("../config/discordConfig"); +// Debug +const { DebugBuilder } = require("../utilities/debugBuilder.js"); +const log = new DebugBuilder("server", "adminController"); +// Utilities +const mysqlHandler = require("../utilities/mysqlHandler"); +const utils = require("../utilities/utils"); +const requests = require("../utilities/httpRequests"); + +/** Get the presets of all online nodes, can be used for functions + * + * @param callback Callback function + * @returns {*} A list of the systems online + */ +async function getPresetsOfOnlineNodes(callback) { + mysqlHandler.getOnlineNodes((onlineNodes) => { + let systems = {}; + onlineNodes.forEach(onlineNode => { + systems[onlineNode.id] = utils.BufferToJson(onlineNode.nearbySystems); + }); + + callback(systems); + }); +} + +async function requestNodeListenToPreset(preset, nodeId, callback) { + mysqlHandler.getNodeInfoFromId(nodeId, (nodeObject) =>{ + reqOptions = new requests.requestOptions("/bot/join", "POST", nodeObject.ip, nodeObject.port); + requests.sendHttpRequest(reqOptions, JSON.stringify({ + "channelID": discordConfig.channelID, + "presetName": preset + }), (responseObject) => { + callback(responseObject) + }); + }) +} + +async function getNodeBotStatus(nodeId, callback) { + mysqlHandler.getNodeInfoFromId(nodeId, (nodeObject) =>{ + reqOptions = new requests.requestOptions("/bot/status", "GET", nodeObject.ip, nodeObject.port, undefined, 5); + requests.sendHttpRequest(reqOptions, JSON.stringify({}), (responseObject) => { + if (responseObject === false) { + // Bot is joined + } + else { + // Bot is free + } + callback(responseObject); + }); + }); +} + +async function requestNodeLeaveServer(nodeId, callback) { + getNodeBotStatus(nodeId, (responseObject) => { + if (responseObject === false) { + // Bot is joined + mysqlHandler.getNodeInfoFromId(nodeId, (nodeObject) =>{ + reqOptions = new requests.requestOptions("/bot/leave", "POST", nodeObject.ip, nodeObject.port); + requests.sendHttpRequest(reqOptions, JSON.stringify({}), (responseObject) => { + callback(responseObject); + }); + }); + } + else { + // Bot is free + callback(false); + } + }) +} + + +/** Return to requests for the presets of all online nodes, cannot be used in functions + * + * @param {*} req Express request parameter + * @param {*} res Express response parameter + */ +exports.getAvailablePresets = async (req, res) => { + await getPresetsOfOnlineNodes((systems) => { + res.status(200).json({ + "systemsOnline": systems + }); + }) +} + +/** Request a node to join the server listening to a specific preset + * + * @param {*} req Express request parameter + * @var {*} req.body.preset The preset to join (REQ) + * @var {*} req.body.nodeId The specific node to join (OPT/REQ if more than one node has the preset) + * @param {*} res Express response parameter + */ +exports.joinPreset = async (req, res) => { + if (!req.body.preset) return res.status(400).json("No preset specified"); + await getPresetsOfOnlineNodes((systems) => { + const systemsWithSelectedPreset = Object.values(systems).filter(nodePresets => nodePresets.includes(req.body.preset)).length + if (!systemsWithSelectedPreset) return res.status(400).json("No system online with that preset"); + if (systemsWithSelectedPreset > 1) { + if (!req.body.nodeId) return res.status(175).json("Multiple locations with the selected channel, please specify a nodeID (nodeId)") + requestNodeListenToPreset(req.body.preset, req.body.nodeId, (responseObject) => { + if (responseObject === false) return res.status(400).json("Timeout reached"); + return res.sendStatus(responseObject.statusCode); + }); + } + else { + let nodeId; + if (!req.body.nodeId) nodeId = utils.getKeyByArrayValue(systems, req.body.preset); + else nodeId = req.body.nodeId; + requestNodeListenToPreset(req.body.preset, nodeId, (responseObject) => { + if (responseObject === false) return res.status(400).json("Timeout reached"); + return res.sendStatus(responseObject.statusCode); + }); + } + }); +} + +/** Request a node to join the server listening to a specific preset + * + * @param {*} req Express request parameter + * @param {*} res Express response parameter + */ +exports.leaveServer = async (req, res) => { + if (!req.body.nodeId) return res.status(400).json("No nodeID specified"); + + requestNodeLeaveServer(req.body.nodeId, (responseObject) => { + if (responseObject === false) return res.status(400).json("Bot not joined to server"); + return res.sendStatus(responseObject.statusCode); + }); +} \ No newline at end of file diff --git a/Server/discord-admin-bot/app.js b/Server/discord-admin-bot/app.js new file mode 100644 index 0000000..e965409 --- /dev/null +++ b/Server/discord-admin-bot/app.js @@ -0,0 +1,150 @@ +//Config +import { getTOKEN, getGuildID, getApplicationID } from './utilities/configHandler.js.js'; +// Commands +import ping from './commands/ping.js'; +import join from './commands/join.js.js'; +import leave from './commands/leave.js.js'; +import status from './commands/status.js.js'; +// Debug +import ModuleDebugBuilder from "./utilities/moduleDebugBuilder.js.js"; +const log = new ModuleDebugBuilder("bot", "app"); +// Modules +import { Client, GatewayIntentBits } from 'discord.js'; +// Utilities +import registerCommands from './utilities/registerCommands.js.js'; + +/** + * Host Process Object Builder + * + * This constructor is used to easily construct responses to the host process + */ +class HPOB { + /** + * Build an object to be passed to the host process + * @param command The command to that was run ("Status", "Join", "Leave", "ChgPreSet") + * @param response The response from the command that was run + */ + constructor(command = "Status"||"Join"||"Leave"||"ChgPreSet", response) { + this.cmd = command; + this.msg = response; + } +} + +// Create the Discord client +const client = new Client({ + intents: [ + GatewayIntentBits.Guilds, + GatewayIntentBits.GuildMessages, + GatewayIntentBits.MessageContent, + GatewayIntentBits.GuildVoiceStates + ] +}); + +/** + * When the parent process sends a message, this will interpret the message and act accordingly + * + * DRB IPC Message Structure: + * msg.cmd = The command keyword; Commands covered on the server side + * msg.params = An array containing the parameters for the command + * + */ +process.on('message', (msg) => { + log.DEBUG('IPC Message: ', msg); + const guildID = getGuilds()[0]; + + log.DEBUG("Guild Name: ", getGuildNameFromID(guildID)); + switch (msg.cmd) { + // Check the status of the bot + case "Status": + log.INFO("Status command run from IPC"); + + status({guildID: guildID, callback: (statusObj) => { + log.DEBUG("Status Object string: ", statusObj); + if (!statusObj.voiceConnection) return process.send(new HPOB("Status", "VDISCONN")); + }}); + break; + + // Check the params for a server ID and if so join the server + case "Join": + log.INFO("Join command run from IPC"); + + join({guildID: guildID, guildObj: client.guilds.cache.get(guildID), channelID: msg.params.channelID, callback: () => { + process.send(new HPOB("Join", "AIDS")); + }}) + break; + + // Check to see if the bot is in a server and if so leave + case "Leave": + log.INFO("Leave command run from IPC"); + + leave({guildID: guildID, callback: (response) => { + process.send(new HPOB("Leave", response)); + }}); + break; + + default: + // Command doesn't exist + log.INFO("Unknown command run from IPC"); + break; + } +}) + +// When the client is connected and ready +client.on('ready', () =>{ + log.INFO(`${client.user.tag} is ready`) + process.send({'msg': "INIT READY"}); +}); + +/* +* Saved For later +client.on('messageCreate', (message) => { + log.DEBUG(`Message Sent by: ${message.author.tag}\n\t'${message.content}'`); +}); +*/ + +// When a command is sent +client.on('interactionCreate', (interaction) => { + if (interaction.isChatInputCommand()){ + switch (interaction.commandName) { + case "ping": + ping(interaction); + break; + case "join": + join({ interaction: interaction }); + break; + case "leave": + leave({ interaction: interaction }); + break; + case "status": + status({ interaction: interaction }); + break; + default: + interaction.reply({ content: 'Command not found, try one that exists', fetchReply: true }) + .then((message) => log.DEBUG(`Reply sent with content ${message.content}`)) + .catch((err) => log.ERROR(err)); + } + } +}) + +function loginBot(){ + client.login(getTOKEN()); +} + +function getGuilds() { + return client.guilds.cache.map(guild => guild.id) +} + +function getGuildNameFromID(guildID) { + return client.guilds.cache.map((guild) => { + if (guild.id === guildID) return guild.name; + })[0] +} + +function main(){ + registerCommands(() => { + loginBot(); + }); +} + +main(); +//module.exports = client; \ No newline at end of file diff --git a/Server/discord-admin-bot/commands/ping.js b/Server/discord-admin-bot/commands/ping.js new file mode 100644 index 0000000..69c2bf4 --- /dev/null +++ b/Server/discord-admin-bot/commands/ping.js @@ -0,0 +1,6 @@ +// Utilities +import { replyToInteraction } from '../utilities/messageHandler.js.js'; + +export default function ping(interaction) { + return replyToInteraction(interaction, "Pong! I have Aids and now you do too!"); +} \ No newline at end of file diff --git a/Server/discord-admin-bot/config/botConfig.json b/Server/discord-admin-bot/config/botConfig.json new file mode 100644 index 0000000..ff3e722 --- /dev/null +++ b/Server/discord-admin-bot/config/botConfig.json @@ -0,0 +1,7 @@ +{ + "TOKEN": "OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY", + "ApplicationID": "943742040255115304", + "GuildID": "367396189529833472", + "DeviceID": "5", + "DeviceName": "VoiceMeeter Aux Output (VB-Audi" +} \ No newline at end of file diff --git a/Server/routes/admin.js b/Server/routes/admin.js index cf461b6..918a24d 100644 --- a/Server/routes/admin.js +++ b/Server/routes/admin.js @@ -4,17 +4,16 @@ const log = new DebugBuilder("server", "admin"); // Modules var express = require('express'); var router = express.Router(); +var adminController = require("../controllers/adminController"); /* GET */ -router.get('/', (req, res) => { - res.send('GET request to the admin') -}) +router.get('/presets', adminController.getAvailablePresets); /* POST */ -router.post('/', (req, res) => { - log.DEBUG(req.body); - res.send('POST request to the post') -}) +router.post('/join', adminController.joinPreset); + +/* POST */ +router.post('/leave', adminController.leaveServer); module.exports = router; diff --git a/Server/utilities/httpRequests.js b/Server/utilities/httpRequests.js new file mode 100644 index 0000000..4fef264 --- /dev/null +++ b/Server/utilities/httpRequests.js @@ -0,0 +1,68 @@ +// Debug +const { DebugBuilder } = require("../utilities/debugBuilder.js"); +const log = new DebugBuilder("server", "httpRequests"); +// Modules +const http = require("http"); + +exports.requestOptions = class requestOptions { + /** + * Construct an HTTP request + * @param {*} method Method of request to use [GET, POST] + * @param {*} headers Headers of the request, not required but will get filled with default values if not set + * @param {*} hostname The destination of the request + * @param {*} port The port for the destination, will use 3001 by default + */ + constructor(path, method, hostname, port = 3001, headers = undefined, timeout = undefined) { + this.hostname = hostname; + this.path = path; + this.port = port; + this.method = method; + this.timeout = timeout; + if (method === "POST"){ + this.headers = headers ?? { + 'Content-Type': 'application/json', + } + } + } +} + +/** + * Send the HTTP request to the server + * @param requestOptions + * @param data + * @param callback + */ +exports.sendHttpRequest = function sendHttpRequest(requestOptions, data, callback){ + log.DEBUG("Sending a request to: ", requestOptions.hostname, requestOptions.port) + // Create the request + const req = http.request(requestOptions, res => { + res.on('data', (data) => { + const responseObject = { + "statusCode": res.statusCode, + "body": data + }; + + try { + responseObject.body = JSON.parse(responseObject.body) + } + catch (err) { + } + + log.DEBUG("Response Object: ", responseObject); + callback(responseObject); + }) + }).on('error', err => { + log.ERROR('Error: ', err.message) + // TODO need to handle if the server is down + }) + + if (requestOptions.timeout) { + req.setTimeout(requestOptions.timeout, () => { + callback(false); + }); + } + + // Write the data to the request and send it + req.write(data) + req.end() +} \ No newline at end of file diff --git a/Server/utilities/mysqlHandler.js b/Server/utilities/mysqlHandler.js index b53a54b..7a1225b 100644 --- a/Server/utilities/mysqlHandler.js +++ b/Server/utilities/mysqlHandler.js @@ -13,7 +13,9 @@ const nodesTable = `${databaseConfig.database_database}.nodes`; connection.connect() -// Get all nodes the server knows about regardless of status +/** Get all nodes the server knows about regardless of status + * @param {*} callback Callback function + */ exports.getAllNodes = (callback) => { const sqlQuery = `SELECT * FROM ${nodesTable}` runSQL(sqlQuery, (rows) => { @@ -21,7 +23,9 @@ exports.getAllNodes = (callback) => { }) } -// Get all nodes that have the online status set true (are online) +/** Get all nodes that have the online status set true (are online) + * @param callback Callback function + */ exports.getOnlineNodes = (callback) => { const sqlQuery = `SELECT * FROM ${nodesTable} WHERE online = 1;` runSQL(sqlQuery, (rows) => { @@ -29,7 +33,10 @@ exports.getOnlineNodes = (callback) => { }) } -// Get info on a node based on ID +/** Get info on a node based on ID + * @param nodeId The ID of the node + * @param callback Callback function + */ exports.getNodeInfoFromId = (nodeId, callback) => { const sqlQuery = `SELECT * FROM ${nodesTable} WHERE id = ${nodeId}` runSQL(sqlQuery, (rows) => { @@ -39,7 +46,10 @@ exports.getNodeInfoFromId = (nodeId, callback) => { }) } -// Add a new node to the DB +/** Add a new node to the DB + * @param nodeObject Node information object + * @param callback Callback function + */ exports.addNewNode = (nodeObject, callback) => { if (!nodeObject.name) throw new Error("No name provided"); const name = nodeObject.name, @@ -55,7 +65,10 @@ exports.addNewNode = (nodeObject, callback) => { }) } -// Update the known info on a node +/** Update the known info on a node + * @param nodeObject Node information object + * @param callback Callback function + */ exports.updateNodeInfo = (nodeObject, callback) => { const name = nodeObject.name, ip = nodeObject.ip, diff --git a/Server/utilities/utils.js b/Server/utilities/utils.js index 852e537..640c3fe 100644 --- a/Server/utilities/utils.js +++ b/Server/utilities/utils.js @@ -6,4 +6,14 @@ exports.JsonToBuffer = (jsonObject) => { // Convert a buffer from the DB to JSON object exports.BufferToJson = (buffer) => { return JSON.parse(buffer.toString()); -} \ No newline at end of file +} + +/** Find a key in an object by its value + * + * @param {*} object The object to search + * @param {*} value The value to search the arrays in the object for + * @returns The key of the object that contains the value + */ +exports.getKeyByArrayValue = (object, value) => { + return Object.keys(object).find(key => object[key].includes(value)); + } \ No newline at end of file