diff --git a/client/client.js b/client/client.js index 447adc3..81b329f 100644 --- a/client/client.js +++ b/client/client.js @@ -1,7 +1,7 @@ import { generateUniqueID } from './modules/baseUtils.mjs'; import { updateId } from './modules/updateConfig.mjs'; import { ClientNodeConfig } from './modules/clientObjectDefinitions.mjs'; -import { initSocketConnection, initSocketListeners, sendNodeUpdate } from './modules/socketClient.mjs'; +import { initSocketConnection } from './modules/socketClient.mjs'; import dotenv from 'dotenv'; dotenv.config() @@ -9,7 +9,7 @@ dotenv.config() var localNodeConfig = new ClientNodeConfig({}) async function boot() { - if (localNodeConfig.node.id === undefined || localNodeConfig.node.id === '' || localNodeConfig.node.id === 0) { + if (localNodeConfig.node.nuid === undefined || localNodeConfig.node.nuid === '' || localNodeConfig.node.nuid === 0) { // Run the first time boot sequence await firstTimeBoot(); } @@ -25,10 +25,10 @@ async function boot() { async function firstTimeBoot() { // Generate a new ID for the node localNodeConfig.node.id = generateUniqueID(); - console.log(`Generated a new unique ID for this node: '${localNodeConfig.node.id}'`); + console.log(`Generated a new unique ID for this node: '${localNodeConfig.node.nuid}'`); // Update the config with the new ID - updateId(localNodeConfig.node.id); + updateId(localNodeConfig.node.nuid); console.log("Updated the config with the new node ID"); // TODO - Create the config file with the ID given and replace the update above // TODO - Check if the system is linux or windows and set the 'type' param in DAB @@ -41,7 +41,5 @@ async function firstTimeBoot() { // Boot the client application boot().then((openSocket) => { - initSocketListeners(openSocket, localNodeConfig); - //console.log(openSocket, "Open socket"); - setTimeout(() => {sendNodeUpdate(openSocket);}, 2500); + console.log(openSocket, "Booted Sucessfully"); }) \ No newline at end of file diff --git a/client/discordAudioBot/dab.mjs b/client/discordAudioBot/dab.mjs index ce7f033..1190a08 100644 --- a/client/discordAudioBot/dab.mjs +++ b/client/discordAudioBot/dab.mjs @@ -78,6 +78,7 @@ export async function connectToChannel(channel) { }); try { await entersState(connection, VoiceConnectionStatus.Ready, 30_000); + await connection.subscribe(player); return connection; } catch (error) { connection.destroy(); @@ -100,6 +101,8 @@ export async function initDiscordBotClient(token, readyCallback){ readyCallback(client); }); + /* on event create + // TODO - Implement methods for discord users to interact directly with the bots for realtime info (last talked ID/TG, etc.) client.on(Events.MessageCreate, async (message) => { if (!message.guild) return; console.log(`New Message:`, message.content); @@ -118,6 +121,7 @@ export async function initDiscordBotClient(token, readyCallback){ } } }); + */ client.login(token); } \ No newline at end of file diff --git a/client/modules/baseUtils.mjs b/client/modules/baseUtils.mjs index 62c0931..0f02f4c 100644 --- a/client/modules/baseUtils.mjs +++ b/client/modules/baseUtils.mjs @@ -1,5 +1,7 @@ import { networkInterfaces } from 'os'; import { createHash, randomBytes } from 'crypto'; +import { promises, constants } from 'fs'; +import { dirname } from "path"; /** * Check to see if the input is a valid JSON string @@ -7,7 +9,7 @@ import { createHash, randomBytes } from 'crypto'; * @param {*} str The string to check for valud JSON * @returns {true|false} */ -export function isJsonString (str) { +export const isJsonString = (str) => { try { JSON.parse(str); } catch (e) { @@ -16,12 +18,25 @@ export function isJsonString (str) { return true; } +/** + * Check to see if a path exists, creating it if it does not + * @returns {undefined} + */ +export const ensureDirectoryExists = async (filePath) => { + const directory = dirname(filePath); + try { + await promises.access(directory, constants.F_OK); + } catch (error) { + await promises.mkdir(directory, { recursive: true }); + } + }; + /** * Generate a unique ID for a given device, this will also be unique on the same device at a different time. * @returns {string} A generated unique ID */ -export function generateUniqueID () { +export const generateUniqueID = () => { const netInterfaces = networkInterfaces(); let macAddress = ''; diff --git a/client/modules/clientObjectDefinitions.mjs b/client/modules/clientObjectDefinitions.mjs index 52a97cf..0d3b541 100644 --- a/client/modules/clientObjectDefinitions.mjs +++ b/client/modules/clientObjectDefinitions.mjs @@ -1,3 +1,5 @@ +import { getAllPresets } from "./radioPresetHandler.mjs"; + import dotenv from 'dotenv'; dotenv.config() @@ -7,33 +9,43 @@ dotenv.config() export class ClientNodeObject { /** * - * @param {string} param0._id The ID of the node (Assigned by the client on first boot) + * @param {string} param0._nuid The ID of the node (Assigned by the client on first boot) * @param {string} param0._name The name of the node (Assigned by the user) - * @param {string} param0._location The physical location of the node (Assigned by the user) - * @param {object} param0._nearbySystems An object array of nearby systems (Assigned by the user) + * @param {string} param0._location The physical location of the node (Assigned by the user) + * @param {object} param0._capabilities The capabilities of this node (Assigned by the user, and determined by the hardware) */ - constructor({ _id = undefined, _name = undefined, _location = undefined, _nearbySystems = undefined}) { - this.id = _id; + constructor({ _nuid = undefined, _name = undefined, _location = undefined, _capabilities = undefined }) { + this.nuid = _nuid; this.name = _name; - this.location = _location; - this.nearbySystems = _nearbySystems; + this.location = _location; + this.capabilities = _capabilities } } /** The configuration object for the node */ export class ClientNodeConfig { + /** + * + * @param {string} param0._nuid The ID of the node (Assigned by the client on first boot) + * @param {string} param0._name The name of the node (Assigned by the user) + * @param {string} param0._location The physical location of the node (Assigned by the user) + * @param {object} param0._nearbySystems An object array of nearby systems (Assigned by the user) + * @param {object} param0._capabilities The capabilities of this node (Assigned by the user, and determined by the hardware) + */ constructor({ - _id = process.env.CLIENT_ID, - _name = process.env.CLIENT_NAME, - _location = process.env.CLIENT_LOCATION, - _nearbySystems = undefined, // TODO - Get the presets when loading the config instead of having to do it after the fact - _serverIp = process.env.SERVER_IP, - _serverPort = process.env.SERVER_PORT, + _nuid = process.env.CLIENT_NUID, + _name = process.env.CLIENT_NAME, + _location = process.env.CLIENT_LOCATION, + _nearbySystems = getAllPresets(), + _capabilities = process.env.CLIENT_CAPABILITIES.split(", "), + _serverIp = process.env.SERVER_IP, + _serverPort = process.env.SERVER_PORT, }) { this.node = new ClientNodeObject({ - _id:_id, _name:_name, _location:_location, _nearbySystems:_nearbySystems + _nuid: _nuid, _name: _name, _location: _location, _capabilities: _capabilities }); + this.nearbySystems = _nearbySystems; this.serverIp = _serverIp; this.serverPort = _serverPort; } diff --git a/client/modules/radioPresetHandler.js b/client/modules/radioPresetHandler.mjs similarity index 61% rename from client/modules/radioPresetHandler.js rename to client/modules/radioPresetHandler.mjs index cab19f1..cd093f6 100644 --- a/client/modules/radioPresetHandler.js +++ b/client/modules/radioPresetHandler.mjs @@ -1,22 +1,27 @@ -// Debug -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"); +import { writeFile, existsSync, readFileSync } from 'fs'; +import { resolve } from "path"; +import { ensureDirectoryExists } from "./baseUtils.mjs"; +import convert_units from "convert-units"; +const { converter } = convert_units; + +import dotenv from 'dotenv'; +dotenv.config() + +const configFilePath = process.env.CONFIG_PATH; /** * Write the given presets to the JSON file * @param presets The preset or presets to be written * @param {function} callback The function to be called when this wrapper completes */ -function writePresets(presets, callback = undefined) { - log.DEBUG(`${__dirname}`); - fs.writeFile("./config/radioPresets.json", JSON.stringify(presets), (err) => { +const writePresets = async (presets, callback = undefined) => { + console.log(`${__dirname}`); + await ensureDirectoryExists(configFilePath); + writeFile(configFilePath, JSON.stringify(presets), (err) => { // Error checking if (err) throw err; - log.DEBUG("Write Complete"); + console.log("Write Complete"); if (callback) callback(); else return }); } @@ -26,14 +31,14 @@ function writePresets(presets, callback = undefined) { * @param frequenciesArray * @returns {*[]} */ -function sanitizeFrequencies(frequenciesArray) { +const sanitizeFrequencies = async (frequenciesArray) => { let sanitizedFrequencyArray = []; for (const freq of frequenciesArray) { sanitizedFrequencyArray.push(convertFrequencyToHertz(freq)); } - log.DEBUG("Sanitized Frequency Array", sanitizedFrequencyArray); + console.log("Sanitized Frequency Array", sanitizedFrequencyArray); return sanitizedFrequencyArray; } @@ -42,23 +47,23 @@ function sanitizeFrequencies(frequenciesArray) { * @param frequency Could be a string, number or float, * @returns {number|number|*} Return the value to be saved in Hz format ("154.875"MHz format = "154875000") */ -function convertFrequencyToHertz(frequency){ +const convertFrequencyToHertz = async (frequency) => { // check if the passed value is a number - if(typeof frequency == 'number' && !isNaN(frequency)){ + if (typeof frequency == 'number' && !isNaN(frequency)) { if (Number.isInteger(frequency)) { - log.DEBUG(`${frequency} is an integer.`); + console.log(`${frequency} is an integer.`); // Check to see if the frequency has the correct length if (frequency >= 1000000) return frequency if (frequency >= 100 && frequency <= 999) return frequency * 1000000 - log.WARN("Frequency hasn't matched filters: ", frequency); + console.log("Frequency hasn't matched filters: ", frequency); } else { - log.DEBUG(`${frequency} is a float value.`); + console.log(`${frequency} is a float value.`); // Convert to a string to remove the decimal in place and then correct the length return parseInt(converter(frequency).from("MHz").to("Hz")); } } else { - log.DEBUG(`${frequency} is not a number`); + console.log(`${frequency} is not a number`); frequency = convertFrequencyToHertz(parseFloat(frequency)); return parseInt(frequency) @@ -69,10 +74,10 @@ function convertFrequencyToHertz(frequency){ * Gets the saved presets and returns a preset object * @returns {any} The object containing the different systems the bot is near */ -function getPresets() { - const presetDir = path.resolve("./config/radioPresets.json"); - log.DEBUG(`Getting presets from directory: '${presetDir}'`); - if (fs.existsSync(presetDir)) return JSON.parse(fs.readFileSync(presetDir)); +export const getAllPresets = () => { + const presetDir = resolve(configFilePath); + console.log(`Getting presets from directory: '${presetDir}'`); + if (existsSync(presetDir)) return JSON.parse(readFileSync(presetDir)); else return {}; } @@ -86,15 +91,15 @@ function getPresets() { * @param {string} trunkFile The file that contains all trunking information (if applicable to the selected listening mode) * @param {string} whitelistFile The file that contains the whitelisted talkgroups [optional] */ -function addNewPreset(systemName, frequencies, mode, callback, trunkFile = undefined, whitelistFile = undefined) { +export const addNewPreset = (systemName, frequencies, mode, callback, trunkFile = undefined, whitelistFile = undefined) => { const presets = this.getPresets(); // Create the preset for the new system presets[systemName] = { - "frequencies": sanitizeFrequencies(frequencies), - "mode": mode, - "trunkFile": trunkFile ?? "none", - "whitelistFile": whitelistFile ?? "none" - } + "frequencies": sanitizeFrequencies(frequencies), + "mode": mode, + "trunkFile": trunkFile ?? "none", + "whitelistFile": whitelistFile ?? "none" + } // Write the changes to the preset config file writePresets(presets, callback); } @@ -109,15 +114,15 @@ function addNewPreset(systemName, frequencies, mode, callback, trunkFile = undef * @param {string} trunkFile The file that contains all trunking information (if applicable to the selected listening mode) * @param {string} whitelistFile The file that contains the whitelisted talkgroups [optional] */ -function updatePreset(systemName, callback, { frequencies = undefined, mode = undefined, trunkFile = undefined, whitelistFile = undefined }) { +export const updatePreset = (systemName, callback, { frequencies = undefined, mode = undefined, trunkFile = undefined, whitelistFile = undefined }) => { const presets = this.getPresets(); // Check if a system name was passed if (systemName in presets) { // System name exists, checking to see if the keys are different - if(frequencies && sanitizeFrequencies(frequencies) !== presets[systemName].frequencies) presets[systemName].frequencies = sanitizeFrequencies(frequencies); - if(mode && mode !== presets[systemName].mode) presets[systemName].mode = mode; - if(trunkFile && trunkFile !== presets[systemName].trunkFile || trunkFile === "") presets[systemName].trunkFile = trunkFile ?? "none"; - if(whitelistFile && whitelistFile !== presets[systemName].whitelistFile || whitelistFile === "") presets[systemName].whitelistFile = whitelistFile ?? "none"; + if (frequencies && sanitizeFrequencies(frequencies) !== presets[systemName].frequencies) presets[systemName].frequencies = sanitizeFrequencies(frequencies); + if (mode && mode !== presets[systemName].mode) presets[systemName].mode = mode; + if (trunkFile && trunkFile !== presets[systemName].trunkFile || trunkFile === "") presets[systemName].trunkFile = trunkFile ?? "none"; + if (whitelistFile && whitelistFile !== presets[systemName].whitelistFile || whitelistFile === "") presets[systemName].whitelistFile = whitelistFile ?? "none"; // Write the changes writePresets(presets, callback); } @@ -129,14 +134,11 @@ function updatePreset(systemName, callback, { frequencies = undefined, mode = un * @param {string} systemName The name of the system being modified * @param {function} callback The callback function to be called when the function completes */ -function removePreset(systemName, callback) { +export const removePreset = (systemName, callback) => { const presets = this.getPresets(); // Check if a system name was passed if (systemName in presets) { delete presets[systemName]; writePresets(presets, callback); } -} - - - +} \ No newline at end of file diff --git a/client/modules/socketClient.mjs b/client/modules/socketClient.mjs index 51d8f42..05b06cc 100644 --- a/client/modules/socketClient.mjs +++ b/client/modules/socketClient.mjs @@ -1,34 +1,28 @@ import { io } from "socket.io-client"; import { connectToChannel, initDiscordBotClient, getVoiceChannelFromID } from '../discordAudioBot/dab.mjs'; +import { logIntoServerWrapper, sendNodeUpdateWrapper } from "./socketClientWrappers.mjs"; -export function initSocketConnection(localNodeConfig) { +export const initSocketConnection = async (localNodeConfig) => { const serverEndpoint = `http://${localNodeConfig.serverIp}:${localNodeConfig.serverPort}` || 'http://localhost:3000'; // Adjust the server endpoint const socket = io.connect(serverEndpoint); - return socket; -} - -export function initSocketListeners(socket, localNodeConfig) { - socket.on('connect', () => { + socket.on('connect', async () => { console.log('Connected to the server'); - logIntoServer(socket, localNodeConfig.node); + await logIntoServerWrapper(socket, localNodeConfig); }); socket.on('node-join', async (joinData) => { console.log("Join requested: ", joinData) - // TODO - Implement logic to control OP25 for the requested channel + // TODO - Implement logic to control OP25 for the requested channel/system // Join the requested channel with the requested ID - initDiscordBotClient(joinData.clientID, client => { - console.log("Client:", client); + initDiscordBotClient(joinData.clientID, client => { getVoiceChannelFromID(client, joinData.channelID).then(vc => { - console.log("Voice Channel:", vc); - console.log("Voice Channel Guild:", vc.Guild); const connection = connectToChannel(vc); + console.log("Bot Connected to VC"); }) - }); - console.log("All done?"); + }); }); socket.on('node-leave', () => { @@ -39,12 +33,5 @@ export function initSocketListeners(socket, localNodeConfig) { console.log('Disconnected from the server'); }); -} - -export function logIntoServer(socket, nodeData) { - socket.emit("node-login", nodeData); -} - -export function sendNodeUpdate(socket, nodeData) { - socket.emit('node-update', nodeData); + return socket; } \ No newline at end of file diff --git a/client/modules/socketClientWrappers.mjs b/client/modules/socketClientWrappers.mjs new file mode 100644 index 0000000..107a9fc --- /dev/null +++ b/client/modules/socketClientWrappers.mjs @@ -0,0 +1,11 @@ +export const logIntoServerWrapper = async (socket, localNodeConfig) => { + socket.emit("node-login", localNodeConfig.node); + sendNodeUpdateWrapper(socket, localNodeConfig); +} + +export const sendNodeUpdateWrapper = async (socket, localNodeConfig) => { + socket.emit('node-update', { + 'node': localNodeConfig.node, + 'nearbySystems': localNodeConfig.nearbySystems + }); +} \ No newline at end of file diff --git a/client/modules/updateConfig.mjs b/client/modules/updateConfig.mjs index a9a4640..46e54b0 100644 --- a/client/modules/updateConfig.mjs +++ b/client/modules/updateConfig.mjs @@ -17,8 +17,8 @@ class Options { * @param updatedId The updated ID assigned to the node */ export function updateId (updatedId) { - updateConfig('CLIENT_ID', updatedId); - process.env.CLIENT_ID = updatedId; + updateConfig('CLIENT_NUID', updatedId); + process.env.CLIENT_NUID = updatedId; console.log("Updated ID to: ", updatedId); } @@ -41,7 +41,7 @@ export function updateClientConfig (runningConfig, newConfigObject) { if (configKeys.includes("id")) { if (runningConfig.id != newConfigObject.id) { this.updateId(newConfigObject.id); - updatedKeys.push({ 'CLIENT_ID': newConfigObject.id }); + updatedKeys.push({ 'CLIENT_NUID': newConfigObject.id }); } } if (configKeys.includes("name")) { diff --git a/client/package-lock.json b/client/package-lock.json index a29bf5a..116ea47 100644 --- a/client/package-lock.json +++ b/client/package-lock.json @@ -10,6 +10,7 @@ "license": "ISC", "dependencies": { "@discordjs/voice": "^0.16.1", + "convert-units": "^2.3.4", "discord.js": "^14.14.1", "dotenv": "^16.3.1", "libsodium-wrappers": "^0.7.13", @@ -321,6 +322,15 @@ "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==" }, + "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/debug": { "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", @@ -522,11 +532,165 @@ "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" }, + "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.snakecase": { "version": "4.1.1", "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" }, + "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/magic-bytes.js": { "version": "1.7.0", "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.7.0.tgz", diff --git a/client/package.json b/client/package.json index 207687b..1f2478f 100644 --- a/client/package.json +++ b/client/package.json @@ -12,6 +12,7 @@ "type": "module", "dependencies": { "@discordjs/voice": "^0.16.1", + "convert-units": "^2.3.4", "discord.js": "^14.14.1", "dotenv": "^16.3.1", "libsodium-wrappers": "^0.7.13", diff --git a/server/discordBot/commands/join.mjs b/server/discordBot/commands/join.mjs new file mode 100644 index 0000000..7632a67 --- /dev/null +++ b/server/discordBot/commands/join.mjs @@ -0,0 +1,25 @@ +import { SlashCommandBuilder } from 'discord.js'; + +// Exporting data property +export const data = new SlashCommandBuilder() + .setName('join') + .setDescription('Replies with your input!'); + +// Exporting other properties +export const example = "/join"; +export const deferInitialReply = false; + +// Exporting execute function +export async function execute(nodeIo, interaction) { + try { + const sockets = await nodeIo.allSockets(); + console.log("All open sockets: ",sockets); + console.log("Open Socket: ", sockets.values(), nodeIo.sockets.sockets.get(sockets[0])) + //await interaction.reply(`**Online Sockets: '${sockets}'**`); + await interaction.reply('**Pong.**'); + //await interaction.channel.send('**Pong.**'); + } catch (err) { + console.error(err); + // await interaction.reply(err.toString()); + } +} \ No newline at end of file diff --git a/server/discordBot/modules/discordBotUtils.mjs b/server/discordBot/discordBot.mjs similarity index 66% rename from server/discordBot/modules/discordBotUtils.mjs rename to server/discordBot/discordBot.mjs index 9e1bb72..3069b8d 100644 --- a/server/discordBot/modules/discordBotUtils.mjs +++ b/server/discordBot/discordBot.mjs @@ -1,29 +1,32 @@ +import { Client, GatewayIntentBits, Collection } from 'discord.js'; +import { registerActiveCommands, unregisterAllCommands } from './modules/registerCommands.mjs' import { join, dirname } from 'path'; import { readdirSync } from 'fs'; import { fileURLToPath } from 'url'; -import { Collection } from 'discord.js'; const __filename = fileURLToPath(import.meta.url); const __dirname = dirname(__filename); +import dotenv from 'dotenv'; +dotenv.config() + /** * Add the enabled commands to the bot to be used by users in discord * (commands that end in '.mjs' will be enabled, to disable just remove the extension or replace with '.mjs.disabled') * @param {any} serverClient - * @param {any} nodeIo * @param {any} _commandsPath="./commands" * @returns {any} */ -export function addEnabledCommands(serverClient, nodeIo, _commandsPath = "../commands") { +export const addEnabledCommands = async (serverClient, _commandsPath = "./commands") => { // Setup commands for the Discord bot serverClient.commands = new Collection(); const commandsPath = join(__dirname, _commandsPath); const commandFiles = readdirSync(commandsPath).filter(file => file.endsWith('.mjs')); for (const file of commandFiles) { - const filePath = join(commandsPath, file); + const filePath = await join(commandsPath, file); console.log(`Adding enabled command: ${filePath}`); - import(`file://${filePath}`).then(command => { + await import(`file://${filePath}`).then(command => { if (command.data instanceof Promise) { command.data.then(async (builder) => { command.data = builder; @@ -40,17 +43,19 @@ export function addEnabledCommands(serverClient, nodeIo, _commandsPath = "../com } }) } + + // Register the commands currently in use by the bot + await registerActiveCommands(serverClient); } /** * Add the enabled event listeners to the bot * (events that end in '.mjs' will be enabled, to disable just remove the extension or replace with '.mjs.disabled') * @param {any} serverClient - * @param {any} nodeIo * @param {any} _eventsPath="./events" * @returns {any} */ -export function addEnabledEventListeners(serverClient, nodeIo, _eventsPath = "../events") { +export function addEnabledEventListeners(serverClient, _eventsPath = "./events") { const eventsPath = join(__dirname, _eventsPath); const eventFiles = readdirSync(eventsPath).filter(file => file.endsWith('.mjs')); @@ -60,10 +65,28 @@ export function addEnabledEventListeners(serverClient, nodeIo, _eventsPath = ".. import(`file://${filePath}`).then(event => { console.log("Adding event: ", event); if (event.once) { - serverClient.once(event.name, (...args) => event.execute(nodeIo, ...args)); + serverClient.once(event.name, (...args) => event.execute(serverClient.nodeIo, ...args)); } else { - serverClient.on(event.name, (...args) => event.execute(nodeIo, ...args)); + serverClient.on(event.name, (...args) => event.execute(serverClient.nodeIo, ...args)); } }) } -} \ No newline at end of file +} + +// The discord client +export const serverClient = new Client({ intents: [GatewayIntentBits.Guilds] }); + +// Run when the bot is ready +serverClient.on('ready', async () => { + console.log(`Logged in as ${serverClient.user.tag}!`); + + // Add and register commands + await addEnabledCommands(serverClient); + + // Config the discord bot with events + await addEnabledEventListeners(serverClient); +}); + +// Startup the discord bot +console.log(`Logging into discord with ID: ${process.env.DISCORD_TOKEN}`); +serverClient.login(process.env.DISCORD_TOKEN); diff --git a/server/discordBot/modules/registerCommands.mjs b/server/discordBot/modules/registerCommands.mjs new file mode 100644 index 0000000..cfadb0e --- /dev/null +++ b/server/discordBot/modules/registerCommands.mjs @@ -0,0 +1,83 @@ +import { REST, Routes } from 'discord.js'; + +import dotenv from 'dotenv'; +dotenv.config() + +const discordToken = process.env.DISCORD_TOKEN; + +export const registerActiveCommands = async (serverClient) => { + const guildIDs = serverClient.guilds.cache; + const clientId = serverClient.user.id; + const commands = await serverClient.commands.map(command => command = command.data.toJSON()); + + // Construct and prepare an instance of the REST module + const rest = new REST({ version: '10' }).setToken(discordToken); + + // and deploy your commands! + guildIDs.forEach(guild => { + console.log("Deploying commands for: ", guild.id); + console.log("Commands", commands); + (async () => { + try { + console.log(`Started refreshing application (/) commands for guild ID: ${guild.id}.`); + // The put method is used to fully refresh all commands in the guild with the current set + const data = await rest.put( + Routes.applicationGuildCommands(clientId, guild.id), + { body: commands }, + ); + + console.log(`Successfully reloaded ${data.length} application (/) commands for guild ID: ${guild.id}.`); + } catch (error) { + // And of course, make sure you catch and log any errors! + console.log("ERROR Deploying commands: ", error, "Body from error: ", commands); + } + })() + }) +}; + +/** + * Remove all commands for a given bot in a given guild + * + * @param {any} serverClient The discord bot client + */ +export const unregisterAllCommands = async (serverClient) => { + const guildIDs = serverClient.guilds.cache; + const clientId = serverClient.user.id; + commands = []; + + const rest = new REST({ version: '10' }).setToken(discordToken); + guildIDs.forEach(guild => { + console.log("Removing commands for: ", clientId, guild.id); + (async () => { + try { + console.log(`Started removal of ${commands.length} application (/) commands for guild ID: ${guild.id}.`); + // The put method is used to fully refresh all commands in the guild with the current set + const data = await rest.put( + Routes.applicationGuildCommands(clientId, guild.id), + { body: commands }, + ); + + console.log(`Successfully removed ${data.length} application (/) commands for guild ID: ${guild.id}.`); + } catch (error) { + // And of course, make sure you catch and log any errors! + console.log("ERROR removing commands: ", error, "Body from error: ", commands); + } + })() + }) + +} + +/** + * This named wrapper will remove all commands and then re-add the commands back, effectively refreshing them + * @param {any} serverClient The discord bot client object + * @returns {any} + */ +export const refreshActiveCommandsWrapper = async (serverClient) => { + // Remove all commands + console.log("Removing/Unregistering all commands from all connected servers/guilds"); + await unregisterAllCommands(serverClient); + // Deploy the active commands + console.log("Adding commands to all connected servers/guilds"); + await registerActiveCommands(serverClient); + return; +} \ No newline at end of file diff --git a/server/modules/discordBot.mjs b/server/modules/discordBot.mjs deleted file mode 100644 index b9fd1c4..0000000 --- a/server/modules/discordBot.mjs +++ /dev/null @@ -1,11 +0,0 @@ -import { Client, GatewayIntentBits } from 'discord.js'; -//import { deployActiveCommands } from '../discordBot/modules/deployCommands.mjs' - -import dotenv from 'dotenv'; -dotenv.config() - -export const serverClient = new Client({ intents: [GatewayIntentBits.Guilds] }); - -serverClient.on('ready', () => { - console.log(`Logged in as ${serverClient.user.tag}!`); -}); \ No newline at end of file diff --git a/server/modules/mongoHandler.mjs b/server/modules/mongoHandler.mjs new file mode 100644 index 0000000..de5be26 --- /dev/null +++ b/server/modules/mongoHandler.mjs @@ -0,0 +1,53 @@ +// Import necessary modules +import { MongoClient } from 'mongodb'; + +import dotenv from 'dotenv'; +dotenv.config() + +// MongoDB connection URI +const uri = process.env.MONGO_URL; + +// Function to connect to the database +export const connectToDatabase = async () => { + try { + const client = await MongoClient.connect(uri); + return client; + } catch (error) { + console.error('Error connecting to the database:', error); + throw error; + } +}; + +// Function to insert a document into the collection +export const insertDocument = async (collectionName, document) => { + const db = await connectToDatabase(); + try { + const collection = db.db().collection(collectionName); + const result = await collection.insertOne(document); + console.log('Document inserted:', result.insertedId); + return result.insertedId; + } catch (error) { + console.error('Error inserting document:', error); + throw error; + } finally { + // Close the connection + await db.close(); + } +}; + +// Function to retrieve documents from the collection +export const getDocuments = async (collectionName) => { + const db = await connectToDatabase(); + try { + const collection = db.db().collection(collectionName); + const documents = await collection.find({}).toArray(); + console.log('Documents retrieved:', documents); + return documents; + } catch (error) { + console.error('Error retrieving documents:', error); + throw error; + } finally { + // Close the connection + await db.close(); + } +}; diff --git a/server/modules/mongoNodesWrappers.mjs b/server/modules/mongoNodesWrappers.mjs new file mode 100644 index 0000000..f12fbbf --- /dev/null +++ b/server/modules/mongoNodesWrappers.mjs @@ -0,0 +1,75 @@ +import { insertDocument, getDocuments, connectToDatabase } from "./mongoHandler.mjs"; + +const collectionName = 'nodes'; + +// Wrapper for inserting a node +export const createNode = async (node) => { + try { + const insertedId = await insertDocument(collectionName, node); + return insertedId; + } catch (error) { + console.error('Error creating node:', error); + throw error; + } +}; + +// Wrapper for retrieving all nodes +export const getAllNodes = async () => { + try { + const nodes = await getDocuments(collectionName); + return nodes; + } catch (error) { + console.error('Error getting all nodes:', error); + throw error; + } +}; + +// Wrapper for retrieving a node by NUID +export const getNodeByNuid = async (nuid) => { + const db = await connectToDatabase(); + try { + const collection = db.db().collection(collectionName); + const node = await collection.findOne({ nuid }); + return node; + } catch (error) { + console.error('Error getting node by NUID:', error); + throw error; + } finally { + // Close the connection + await db.close(); + } +}; + +// Wrapper for updating a node by NUID +export const updateNodeByNuid = async (nuid, updatedFields) => { + const db = await connectToDatabase(); + try { + const collection = db.db().collection(collectionName); + const result = await collection.updateOne({ nuid }, { $set: updatedFields }); + console.log('Node updated:', result.modifiedCount); + return result.modifiedCount; + } catch (error) { + console.error('Error updating node by NUID:', error); + throw error; + } finally { + // Close the connection + await db.close(); + } +}; + +// Wrapper for deleting a node by NUID +export const deleteNodeByNuid = async (nuid) => { + const db = await connectToDatabase(); + try { + const collection = db.db().collection(collectionName); + const result = await collection.deleteOne({ nuid }); + console.log('Node deleted:', result.deletedCount); + return result.deletedCount; + } catch (error) { + console.error('Error deleting node by NUID:', error); + throw error; + } finally { + // Close the connection + await db.close(); + } +}; \ No newline at end of file diff --git a/server/modules/mongoSystemsWrappers.mjs b/server/modules/mongoSystemsWrappers.mjs new file mode 100644 index 0000000..f353073 --- /dev/null +++ b/server/modules/mongoSystemsWrappers.mjs @@ -0,0 +1,111 @@ +import { insertDocument, getDocuments, connectToDatabase } from "./mongoHandler.mjs"; + +const collectionName = 'radio-systems'; + +// Local wrapper to remove any local files from radio systems +const removeLocalFilesFromsystem = async (system) => { + if (system.trunkFile) delete system.trunkFile; + if (system.whitelistFile) delete system.whitelistFile; +} + + +// Wrapper for inserting a system +export const createSystem = async (name, system, nuid) => { + try { + // Remove any local files + await removeLocalFilesFromsystem(system); + // Add the NUID of the node that created this system + system.nodes = [nuid]; + // Add the name of the system + system.name = name + const insertedId = await insertDocument(collectionName, system); + return insertedId; + } catch (error) { + console.error('Error creating system:', error); + throw error; + } +}; + +// Wrapper for retrieving all systems +export const getAllSystems = async () => { + try { + const systems = await getDocuments(collectionName); + return systems; + } catch (error) { + console.error('Error getting all systems:', error); + throw error; + } +}; + +// Wrapper for retrieving a system by name +export const getSystemByName = async (name) => { + const db = await connectToDatabase(); + try { + const collection = db.db().collection(collectionName); + const system = await collection.findOne({ name }); + return system; + } catch (error) { + console.error('Error getting system by name:', error); + throw error; + } finally { + // Close the connection + await db.close(); + } +}; + +// Wrapper to get all systems from a given node +export const getSystemsByNuid = async (nuid) => { + const db = await connectToDatabase(); + try { + const collection = db.db().collection(collectionName); + + // Query for documents where the 'nodes' array contains the given nodeID + const query = { nodes: nuid }; + const systems = await collection.find(query).toArray(); + + return systems; + } catch (error) { + console.error('Error finding entries:', error); + throw error; + } finally { + // Close the connection + await db.close(); + } +}; + +// Wrapper for updating a system by name +export const updateSystemByName = async (name, updatedSystem) => { + // Remove any local files + await removeLocalFilesFromsystem(updatedSystem); + + const db = await connectToDatabase(); + try { + const collection = db.db().collection(collectionName); + const result = await collection.updateOne({ name }, { $set: updatedSystem }); + console.log('System updated:', result.modifiedCount); + return result.modifiedCount; + } catch (error) { + console.error('Error updating system by name:', error); + throw error; + } finally { + // Close the connection + await db.close(); + } +}; + +// Wrapper for deleting a system by name +export const deleteSystemByName = async (name) => { + const db = await connectToDatabase(); + try { + const collection = db.db().collection(collectionName); + const result = await collection.deleteOne({ name }); + console.log('System deleted:', result.deletedCount); + return result.deletedCount; + } catch (error) { + console.error('Error deleting system by name:', error); + throw error; + } finally { + // Close the connection + await db.close(); + } +}; \ No newline at end of file diff --git a/server/modules/socketServer.mjs b/server/modules/socketServer.mjs index 964de0e..9c6ee3b 100644 --- a/server/modules/socketServer.mjs +++ b/server/modules/socketServer.mjs @@ -2,6 +2,7 @@ import express from 'express'; import { createServer } from 'node:http'; import { Server } from 'socket.io'; import morgan from 'morgan'; +import { nodeLoginWrapper, nodeUpdateWrapper, nodeDisconnectWrapper, nearbySystemsUpdateWraper } from "./socketServerWrappers.mjs"; export const app = express(); export const server = createServer(app); @@ -17,48 +18,21 @@ nodeIo.on('connection', (socket) => { console.log('a user connected', socket.id); socket.on('node-login', (data) => { - nodeLoginWrapper(data); + nodeLoginWrapper(data, socket); }) socket.on('node-update', (data) => { - updateNodeData(data); + nodeUpdateWrapper(data.node); + nearbySystemsUpdateWraper(data.node.nuid, data.nearbySystems) }) socket.on('disconnect', () => { - console.log('user disconnected'); + nodeDisconnectWrapper(socket.id); }); - // Test commands - setTimeout(() => { - const joinData = { - 'clientID': "MTE5NjAwNTM2ODYzNjExMjk3Nw.GuCMXg.24iNNofNNumq46FIj68zMe9RmQgugAgfrvelEA", - 'channelID': "367396189529833476", - 'preset': "" - } - sendNodeCommand(socket, "node-join", joinData); - }, 2500) - //setTimeout(() => { sendNodeCommand(socket, "node-leave", {}); }, 3500) }); -function sendNodeCommand(socket, command, data) { - // TODO - Check to see if the command exists - // TODO - Check to see if the socket is alive? - // TODO - Validate the given data - socket.emit(command, data); -} - -function loginNode() { - -} - -function registerNode() { - -} - -function updateNodeData(data) { - console.log("Data update sent by node: ", data); -} - -function nodeLoginWrapper(data) { - console.log(`Login requested from node: ${data.id}`, data); -} \ No newline at end of file +// Startup the node server +server.listen(3000, () => { + console.log('server running at http://localhost:3000'); +}); \ No newline at end of file diff --git a/server/modules/socketServerWrappers.mjs b/server/modules/socketServerWrappers.mjs new file mode 100644 index 0000000..af0e6af --- /dev/null +++ b/server/modules/socketServerWrappers.mjs @@ -0,0 +1,167 @@ +import { createNode, getNodeByNuid, updateNodeByNuid } from "./mongoNodesWrappers.mjs" +import { createSystem, getSystemByName, updateSystemByName, getSystemsByNuid, deleteSystemByName } from "./mongoSystemsWrappers.mjs" + +/** + * Description + * @param {any} socket + * @param {any} command + * @param {any} data + * @returns {any} + */ +const sendNodeCommand = async (socket, command, data) => { + // TODO - Check to see if the command exists + // TODO - Check to see if the socket is alive? + // TODO - Validate the given data + socket.emit(command, data); +} + +/** + * Log the node into the network + * @param {object} data The data sent from the node + * @param {any} socket The socket the node is connected from + * @returns {any} + */ +export const nodeLoginWrapper = async (data, socket) => { + console.log(`Login requested from node: ${data.nuid}`, data); + // Check to see if node exists + var node = await getNodeByNuid(data.nuid); + console.log("After grabbing", node); + if (!node) { + const insertedId = await createNode(data); + node = await getNodeByNuid(data.nuid); + console.log("Added new node to the database:", insertedId); + } + // Check for updates if so + // Check for System updates + + // Add the socket/node connection + socket.node = node; + socket.id = node.nuid; + + return; +} + +/** + * Disconnect the client from the server + * @param {string} socketId The socket ID that was disconnected + * @returns {any} + */ +export const nodeDisconnectWrapper = async (socketId) => { + return; +} + +/** + * Update node data in the database + * @param {object} nodeData The data object sent from the node + * @returns {any} + */ +export const nodeUpdateWrapper = async (nodeData) => { + console.log("Data update sent by node: ", nodeData); + const updateResults = await updateNodeByNuid(nodeData.nuid, nodeData); + return; +} + +/** + * Wrapper to update the systems from the nearbySystems object passed from clients + * @param {string} nuid The NUID of the node that sent the update + * @param {object} nearbySystems The nearby systems object passed from the node to be updated + * @returns {any} + */ +export const nearbySystemsUpdateWraper = async (nuid, nearbySystems) => { + console.log("System updates sent by node: ", nuid, nearbySystems); + // Check to see if the node removed any systems + const existingSystems = await getSystemsByNuid(nuid); + console.log("Existing systems:", existingSystems); + if (existingSystems !== nearbySystems) { + for (const existingSystem of existingSystems) { + if (existingSystem.name in nearbySystems) { + // Skip this system if it's in the given systems update + continue; + } + + console.log("System exists that was not given by node", existingSystem); + // Check if this node was the only node on this system + if (existingSystem.nodes.filter(node => node !== nuid).length === 0) { + // Remove the system if so + console.log("Given node was the only node on this system, removing the system..."); + await deleteSystemByName(existingSystem.name); + } else { + // Remove the node from the array if there are other nodes with this system + console.log("Other nodes found on this system, removing the given NUID"); + existingSystem.nodes = existingSystem.nodes.filter(node => node !== nuid); + console.log(existingSystem); + await updateSystemByName(existingSystem.name, existingSystem); + } + } + } + + // Add and update the given systems + for (const nearbySystem in nearbySystems) { + // Check if the system exists already on another node + const existingSystem = await getSystemByName(nearbySystem); + if (existingSystem) { + // Verify the frequencies match (to make sure the name isn't just the same) + console.log(existingSystem.frequencies, nearbySystems[nearbySystem].frequencies, (JSON.stringify(existingSystem.frequencies) === JSON.stringify(nearbySystems[nearbySystem].frequencies))); + + if (JSON.stringify(existingSystem.frequencies) === JSON.stringify(nearbySystems[nearbySystem].frequencies)) { + // The systems are the same + + // Check if the current node is listed in the nodes, if not add it + if (!existingSystem.nodes.includes(nuid)) { + existingSystem.nodes.push(nuid); + // Update the system with the added node + const updateResults = await updateSystemByName(nearbySystem, existingSystem); + if (updateResults) console.log("System updated", nearbySystem); + } + } else { + // The systems are not the same + // TODO - Implement logic to handle if system names match, but they are for different frequencies or have additional freqs + + // Check if the current node is listed in the nodes, if not add it + if (!existingSystem.nodes.includes(nuid)) { + existingSystem.nodes.push(nuid); + nearbySystems[nearbySystem].nodes = existingSystem.nodes; + } + + // Update the system with the added node + const updateResults = await updateSystemByName(nearbySystem, nearbySystems[nearbySystem]); + if (updateResults) console.log("System updated", nearbySystem); + } + } + else { + // Create a new system + const newSystem = await createSystem(nearbySystem, nearbySystems[nearbySystem], nuid); + console.log("New system created", nearbySystem, newSystem); + } + } + return; +} + + +/** + * Get the open socket connection ID for a node from the NUID + * @param {string} nuid The NUID to find within the open sockets + * @returns {string|null} Will return the open socket ID or NULL + */ +const getSocketIdByNuid = async (nuid) => { + for (const openSocket in openSockets) { + if (openSockets[openSocket] == nuid) + return openSocket; + } + return null; +} + + + +const requestNodeJoinPreset = async () => { + // Check for System updates + // Test commands + setTimeout(() => { + const joinData = { + 'clientID': "MTE5NjAwNTM2ODYzNjExMjk3Nw.GuCMXg.24iNNofNNumq46FIj68zMe9RmQgugAgfrvelEA", + 'channelID': "367396189529833476", + 'preset': "" + } + sendNodeCommand(socket, "node-join", joinData); + }, 2500) +} \ No newline at end of file diff --git a/server/package-lock.json b/server/package-lock.json index 25324b2..6ea3250 100644 --- a/server/package-lock.json +++ b/server/package-lock.json @@ -12,6 +12,7 @@ "discord.js": "^14.14.1", "dotenv": "^16.3.1", "express": "^4.18.2", + "mongodb": "^6.3.0", "morgan": "^1.10.0", "socket.io": "^4.7.2" } @@ -142,6 +143,14 @@ "node": ">=14" } }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.4.tgz", + "integrity": "sha512-8zJ8N1x51xo9hwPh6AWnKdLGEC5N3lDa6kms1YHmFBoRhTpJR6HG8wWk0td1MVCu9cD4YBrvjZEtd5Obw0Fbnw==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, "node_modules/@sapphire/async-queue": { "version": "1.5.1", "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.1.tgz", @@ -198,6 +207,19 @@ "undici-types": "~5.26.4" } }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.4.tgz", + "integrity": "sha512-lXCmTWSHJvf0TRSO58nm978b8HJ/EdsSsEKLd3ODHFjo+3VGAyyTp4v50nWvwtzBxSMQrVOK7tcuN0zGPLICMw==", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, "node_modules/@types/ws": { "version": "8.5.9", "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.9.tgz", @@ -279,6 +301,14 @@ "npm": "1.2.8000 || >= 1.4.16" } }, + "node_modules/bson": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.2.0.tgz", + "integrity": "sha512-ID1cI+7bazPDyL9wYy9GaQ8gEEohWvcUl/Yf0dIdutJxnmInEEyCsb4awy/OiBfall7zBA179Pahi3vCdFze3Q==", + "engines": { + "node": ">=16.20.1" + } + }, "node_modules/bytes": { "version": "3.1.2", "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", @@ -743,6 +773,11 @@ "node": ">= 0.6" } }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, "node_modules/merge-descriptors": { "version": "1.0.1", "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", @@ -786,6 +821,60 @@ "node": ">= 0.6" } }, + "node_modules/mongodb": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.3.0.tgz", + "integrity": "sha512-tt0KuGjGtLUhLoU263+xvQmPHEGTw5LbcNC73EoFRYgSHwZt5tsoJC110hDyO1kjQzpgNrpdcSza9PknWN4LrA==", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.0", + "bson": "^6.2.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.0.tgz", + "integrity": "sha512-t1Vf+m1I5hC2M5RJx/7AtxgABy1cZmIPQRMXw+gEIPn/cZNF3Oiy+l0UIypUwVB5trcWHq3crg2g3uAR9aAwsQ==", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, "node_modules/morgan": { "version": "1.10.0", "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", @@ -885,6 +974,14 @@ "node": ">= 0.10" } }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, "node_modules/qs": { "version": "6.11.0", "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", @@ -1098,6 +1195,14 @@ "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, "node_modules/statuses": { "version": "2.0.1", "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", @@ -1114,6 +1219,17 @@ "node": ">=0.6" } }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, "node_modules/ts-mixer": { "version": "6.0.3", "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.3.tgz", @@ -1176,6 +1292,26 @@ "node": ">= 0.8" } }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + }, "node_modules/ws": { "version": "8.11.0", "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", diff --git a/server/package.json b/server/package.json index a0c25c5..358c0c9 100644 --- a/server/package.json +++ b/server/package.json @@ -14,6 +14,7 @@ "discord.js": "^14.14.1", "dotenv": "^16.3.1", "express": "^4.18.2", + "mongodb": "^6.3.0", "morgan": "^1.10.0", "socket.io": "^4.7.2" } diff --git a/server/server.js b/server/server.js index b448c47..c76cf6b 100644 --- a/server/server.js +++ b/server/server.js @@ -1,19 +1,9 @@ import { nodeIo, app, server } from './modules/socketServer.mjs'; -import { addEnabledCommands, addEnabledEventListeners } from './discordBot/modules/discordBotUtils.mjs' -import { serverClient } from './modules/discordBot.mjs'; +import { serverClient, addEnabledEventListeners } from './discordBot/discordBot.mjs'; import dotenv from 'dotenv'; dotenv.config() -// Startup the node server -server.listen(3000, () => { - console.log('server running at http://localhost:3000'); -}); - -// Config the discord bot with commands and events -await addEnabledEventListeners(serverClient, nodeIo); -await addEnabledCommands(serverClient, nodeIo); - -// Startup the discord bot -console.log(`Logging into discord with ID: ${process.env.DISCORD_TOKEN}`); -serverClient.login(process.env.DISCORD_TOKEN); +// Add objects to the others +serverClient.nodeIo = nodeIo; +nodeIo.serverClient = serverClient; \ No newline at end of file