Initial move

This commit is contained in:
Logan Cusano
2024-05-12 12:54:20 -04:00
parent 8ab949f15c
commit 4023a7fc2c
24 changed files with 4278 additions and 0 deletions

301
.gitignore vendored Normal file
View File

@@ -0,0 +1,301 @@
# ---> Node
# Logs
logs
*.log
npm-debug.log*
yarn-debug.log*
yarn-error.log*
lerna-debug.log*
.pnpm-debug.log*
# Diagnostic reports (https://nodejs.org/api/report.html)
report.[0-9]*.[0-9]*.[0-9]*.[0-9]*.json
# Runtime data
pids
*.pid
*.seed
*.pid.lock
# Directory for instrumented libs generated by jscoverage/JSCover
lib-cov
# Coverage directory used by tools like istanbul
coverage
*.lcov
# nyc test coverage
.nyc_output
# Grunt intermediate storage (https://gruntjs.com/creating-plugins#storing-task-files)
.grunt
# Bower dependency directory (https://bower.io/)
bower_components
# node-waf configuration
.lock-wscript
# Compiled binary addons (https://nodejs.org/api/addons.html)
build/Release
# Dependency directories
node_modules/
jspm_packages/
# Snowpack dependency directory (https://snowpack.dev/)
web_modules/
# TypeScript cache
*.tsbuildinfo
# Optional npm cache directory
.npm
# Optional eslint cache
.eslintcache
# Optional stylelint cache
.stylelintcache
# Microbundle cache
.rpt2_cache/
.rts2_cache_cjs/
.rts2_cache_es/
.rts2_cache_umd/
# Optional REPL history
.node_repl_history
# Output of 'npm pack'
*.tgz
# Yarn Integrity file
.yarn-integrity
# dotenv environment variable files
.env
.env.development.local
.env.test.local
.env.production.local
.env.local
# parcel-bundler cache (https://parceljs.org/)
.cache
.parcel-cache
# Next.js build output
.next
out
# Nuxt.js build / generate output
.nuxt
dist
# Gatsby files
.cache/
# Comment in the public line in if your project uses Gatsby and not Next.js
# https://nextjs.org/blog/next-9-1#public-directory-support
# public
# vuepress build output
.vuepress/dist
# vuepress v2.x temp and cache directory
.temp
.cache
# Docusaurus cache and generated files
.docusaurus
# Serverless directories
.serverless/
# FuseBox cache
.fusebox/
# DynamoDB Local files
.dynamodb/
# TernJS port file
.tern-port
# Stores VSCode versions used for testing VSCode extensions
.vscode-test
# yarn v2
.yarn/cache
.yarn/unplugged
.yarn/build-state.yml
.yarn/install-state.gz
.pnp.*
# ---> Python
# Byte-compiled / optimized / DLL files
__pycache__/
*.py[cod]
*$py.class
# C extensions
*.so
# Distribution / packaging
.Python
build/
develop-eggs/
dist/
downloads/
eggs/
.eggs/
lib/
lib64/
parts/
sdist/
var/
wheels/
share/python-wheels/
*.egg-info/
.installed.cfg
*.egg
MANIFEST
# PyInstaller
# Usually these files are written by a python script from a template
# before PyInstaller builds the exe, so as to inject date/other infos into it.
*.manifest
*.spec
# Installer logs
pip-log.txt
pip-delete-this-directory.txt
# Unit test / coverage reports
htmlcov/
.tox/
.nox/
.coverage
.coverage.*
.cache
nosetests.xml
coverage.xml
*.cover
*.py,cover
.hypothesis/
.pytest_cache/
cover/
# Translations
*.mo
*.pot
# Django stuff:
*.log
local_settings.py
db.sqlite3
db.sqlite3-journal
# Flask stuff:
instance/
.webassets-cache
# Scrapy stuff:
.scrapy
# Sphinx documentation
docs/_build/
# PyBuilder
.pybuilder/
target/
# Jupyter Notebook
.ipynb_checkpoints
# IPython
profile_default/
ipython_config.py
# pyenv
# For a library or package, you might want to ignore these files since the code is
# intended to run in multiple environments; otherwise, check them in:
# .python-version
# pipenv
# According to pypa/pipenv#598, it is recommended to include Pipfile.lock in version control.
# However, in case of collaboration, if having platform-specific dependencies or dependencies
# having no cross-platform support, pipenv may install dependencies that don't work, or not
# install all needed dependencies.
#Pipfile.lock
# poetry
# Similar to Pipfile.lock, it is generally recommended to include poetry.lock in version control.
# This is especially recommended for binary packages to ensure reproducibility, and is more
# commonly ignored for libraries.
# https://python-poetry.org/docs/basic-usage/#commit-your-poetrylock-file-to-version-control
#poetry.lock
# pdm
# Similar to Pipfile.lock, it is generally recommended to include pdm.lock in version control.
#pdm.lock
# pdm stores project-wide configurations in .pdm.toml, but it is recommended to not include it
# in version control.
# https://pdm.fming.dev/#use-with-ide
.pdm.toml
# PEP 582; used by e.g. github.com/David-OConnor/pyflow and github.com/pdm-project/pdm
__pypackages__/
# Celery stuff
celerybeat-schedule
celerybeat.pid
# SageMath parsed files
*.sage.py
# Environments
.env
.venv
env/
venv/
ENV/
env.bak/
venv.bak/
# Spyder project settings
.spyderproject
.spyproject
# Rope project settings
.ropeproject
# mkdocs documentation
/site
# mypy
.mypy_cache/
.dmypy.json
dmypy.json
# Pyre type checker
.pyre/
# pytype static type analyzer
.pytype/
# Cython debug symbols
cython_debug/
# PyCharm
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
# and can be added to the global gitignore or merged into this file. For a more nuclear
# option (not recommended) you can uncomment the following to ignore the entire idea folder.
#.idea/
/.vscode
# Ignore the config dirs
config/
# Ignore the OP25 directory we will create
op25/

21
Dockerfile Normal file
View File

@@ -0,0 +1,21 @@
# Use the official Node.js image as the base image
FROM node:20
# Set the working directory inside the container
WORKDIR /server
# Copy package.json and package-lock.json (if available) to the working directory
COPY package*.json ./
# Install dependencies
RUN npm install -g node-gyp
RUN npm install
# Copy the rest of the application code to the working directory
COPY . .
# Expose the port on which your Node.js application will run
EXPOSE 3000
# Command to run the Node.js application
CMD ["node", "."]

View File

@@ -0,0 +1,7 @@
{
"name": "Addon 1",
"enabled": false,
"options": {
"eventName": "connection"
}
}

17
addons/example/index.js Normal file
View File

@@ -0,0 +1,17 @@
// addons/addon1/index.js
// Function called by the main application to initialize the addon
export function initialize(nodeIo, config) {
console.log(`Initializing ${config.name}`);
// Call other functions within the addon module
registerSocketEvents(nodeIo, config);
// Call additional initialization functions if needed
}
// Function to register Socket.IO event handlers
function registerSocketEvents(nodeIo, config) {
nodeIo.on(config.options.eventName, (data) => {
console.log(`Received event "${config.options.eventName}" from client:`, data);
});
}

View File

@@ -0,0 +1,154 @@
import { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js';
import { requestNodeJoinSystem, checkIfNodeIsConnectedToVC, checkIfNodeHasOpenDiscordClient, getNodeCurrentListeningSystem } from '../../modules/socketServerWrappers.mjs';
import { getSystemsByNuid, getAllSystems, getSystemByName } from '../../modules/mongoSystemsWrappers.mjs';
import { getAvailableTokensInGuild } from '../modules/wrappers.mjs';
// Exporting data property
export const data = new SlashCommandBuilder()
.setName('join')
.setDescription('Listen to the selected radio system in your channel')
.addStringOption(system =>
system.setName('system')
.setDescription('The radio system you would like to listen to')
.setRequired(true)
.setAutocomplete(true));
// Exporting other properties
export const example = "/join";
export const deferInitialReply = true;
/**
* Function to give the user auto-reply suggestions
* @param {any} nodeIo The nodeIO server for manipulation of sockets
* @param {any} interaction The interaction object
*/
export async function autocomplete(nodeIo, interaction) {
const focusedValue = interaction.options.getFocused();
const choices = await getAllSystems();
const filtered = choices.filter(choice => choice.name.startsWith(focusedValue));
console.log(focusedValue, choices, filtered);
await interaction.respond(
filtered.map(choice => ({ name: choice.name, value: choice.name })),
);
}
/**
* The function to run when the command is called by a discord user
* @param {any} nodeIo The nodeIO server for manipulation of sockets
* @param {any} interaction The interaction object
*/
export async function execute(nodeIo, interaction) {
// Check if the user is in a VC
if (!interaction.member.voice.channel) { return await interaction.editReply({ content: `<@${interaction.member.id}>, you need to enter a voice channel before you use this command`, ephemeral: true }) }
// Grab the channel if the user is connected to VC
const channelToJoin = interaction.member.voice.channel;
console.log(`The user '${interaction.member.id}' is in the voice channel '${channelToJoin}'`);
// Get the selected system option from the command interaction
const selectedSystem = interaction.options.getString('system');
try {
// Get the selected system object from the DB
const system = await getSystemByName(selectedSystem);
// Function wrapper to request the selected/only node to join the selected system
const joinSelectedNode = async (selectedNodeSocketId) => {
const openSocket = await nodeIo.sockets.sockets.get(selectedNodeSocketId);
// Get the open ID for this connection\
const ss = await getAvailableTokensInGuild(nodeIo, interaction.guild.id);
console.log("Available discord tokens: ", discordTokens);
if (discordTokens.length >= 1) {
// TODO - Implement a method to have preferred tokens (bot users) for specific systems
console.log("Joining selected open socket:", selectedNodeSocketId, system.name, channelToJoin.id, openSocket.node.name, discordTokens[0].token);
// Ask the node to join the selected channel and system
await requestNodeJoinSystem(openSocket, system.name, channelToJoin.id, discordTokens[0].token);
}
else {
return await interaction.editReply({ content: `<@${interaction.member.id}>, there are no free bots. Free up or create a new bot ID (discord app) to listen to this system.`, ephemeral: true })
}
}
// Get all open socket nodes
const openSockets = [...await nodeIo.allSockets()]; // TODO - Filter the returned nodes to only nodes that have the radio capability
console.log("All open sockets: ", openSockets);
var availableNodes = [];
// Check each open socket to see if the node has the requested system
await Promise.all(openSockets.map(async openSocket => {
openSocket = await nodeIo.sockets.sockets.get(openSocket);
// Check if the node has an existing open client (meaning the radio is already being listened to)
const hasOpenClient = await checkIfNodeHasOpenDiscordClient(openSocket);
if (hasOpenClient) {
let currentSystem = await getNodeCurrentListeningSystem(openSocket);
if (currentSystem != system.name) {
console.log("Node is listening to a different system than requested", openSocket.node.name);
return;
}
}
// Check if the bot has an open voice connection in the requested server already
const connected = await checkIfNodeIsConnectedToVC(nodeIo, interaction.guild.id, openSocket.node.nuid);
console.log("Connected:", connected);
if (!connected) {
// Check if this node has the requested system, if so add it to the availble array
if (system.nodes.includes(openSocket.node.nuid)) {
availableNodes.push(openSocket);
}
}
}));
console.log("Availble nodes:", availableNodes.map(socket => socket.node.name));
// If there are no available nodes, let the user know there are none available
if (availableNodes.length == 0) {
// There are no nodes availble for the requested system
return await interaction.editReply(`<@${interaction.member.id}>, the selected system has no available nodes`);
} else if (availableNodes.length == 1) {
// There is only one node available for the requested system
// Request the node to join
await joinSelectedNode(availableNodes[0].id);
// Let the user know
await interaction.editReply({ content: `Ok <@${interaction.member.id}>, a bot will join your channel listening to *'${system.name}'* shortly`, components: [] });
} else if (availableNodes.length > 1) {
// There is more than one node availble for the requested system
const nodeSelectionButtons = []
// Create a button for each available node
for (const availableNode of availableNodes) {
nodeSelectionButtons.push(new ButtonBuilder().setCustomId(availableNode.id).setLabel(availableNode.node.name).setStyle(ButtonStyle.Primary));
}
const actionRow = new ActionRowBuilder().addComponents(nodeSelectionButtons);
// Reply to the user with the button prompts
const response = await interaction.editReply({
content: `<@${interaction.member.id}>, Please select the Node you would like to join with this system`,
components: [actionRow]
});
// Make sure the responding selection is from the user who initiated the command
const collectorFilter = i => i.user.id === interaction.user.id;
// Wait for the confirmation from the user on which node to join
try {
const selectedNode = await response.awaitMessageComponent({ filter: collectorFilter, time: 60_000 });
// Run the local wrapper to listen to the selected node
await joinSelectedNode(selectedNode.customId);
// Let the user know
await selectedNodeConfirmation.update({ content: `Ok <@${interaction.member.id}>, a bot will join your channel listening to *'${system.name}'*`, components: [] });
} catch (e) {
console.error(e);
// Timeout the prompt if the user doesn't interact with it
await interaction.editReply({ content: 'Confirmation not received within 1 minute, cancelling', components: [] });
}
}
} catch (err) {
console.error(err);
// await interaction.reply(err.toString());
}
}

View File

@@ -0,0 +1,56 @@
import { SlashCommandBuilder } from 'discord.js';
import { requestBotLeaveServer, getSocketIdByNuid } from '../../modules/socketServerWrappers.mjs';
import { checkOnlineBotsInGuild } from '../modules/wrappers.mjs'
// Exporting data property
export const data = new SlashCommandBuilder()
.setName('leave')
.setDescription('Disconnect a bot from the server')
.addStringOption(system =>
system.setName('bot')
.setDescription('The bot you would like to disconnect')
.setRequired(true)
.setAutocomplete(true));;
// Exporting other properties
export const example = "/leave *{Bot Name}*";
export const deferInitialReply = true;
/**
* Function to give the user auto-reply suggestions
* @param {any} nodeIo The nodeIO server for manipulation of sockets
* @param {any} interaction The interaction object
*/
export async function autocomplete(nodeIo, interaction) {
const focusedValue = interaction.options.getFocused();
const choices = (await checkOnlineBotsInGuild(nodeIo, interaction.guild.id));
console.log(choices);
const filtered = choices.filter(choice => choice.name.startsWith(focusedValue)).map(choice => choice = {name: choice.name, value: choice.nuid});
console.log(focusedValue, choices, filtered);
await interaction.respond(filtered);
}
/**
* The function to run when the command is called by a discord user
* @param {any} nodeIo The nodeIO server for manipulation of sockets
* @param {any} interaction The interaction object
*/
export async function execute(nodeIo, interaction) {
try {
// Get the requested bot
const selectedNode = interaction.options.getString('bot');
const socket = await getSocketIdByNuid(nodeIo, selectedNode);
console.log("All open sockets:", socket, selectedNode);
await requestBotLeaveServer(socket, interaction.guild.id);
//await interaction.reply(`**Online Sockets: '${sockets}'**`);
await interaction.editReply(`Ok <@${interaction.member.id}>, the bot is leaving shortly`);
//await interaction.channel.send('**Pong.**');
} catch (err) {
console.error(err);
// await interaction.reply(err.toString());
}
}

View File

@@ -0,0 +1,43 @@
import { SlashCommandBuilder } from 'discord.js';
// Exporting data property that contains the command structure for discord including any params
export const data = new SlashCommandBuilder()
.setName('ping')
.setDescription('Replies with your input!');
// Exporting other properties
export const example = "/ping"; // An example of how the command would be run in discord chat, this will be used for the help command
export const deferInitialReply = false; // If we the initial reply in discord should be deferred. This gives extra time to respond, however the method of replying is different.
/**
* Function to give the user auto-reply suggestions
* @param {any} nodeIo The nodeIO server for manipulation of sockets
* @param {any} interaction The interaction object
*/
/*
export async function autocomplete(nodeIo, interaction) {
const focusedValue = interaction.options.getFocused();
const choices = [];
const filtered = choices.filter(choice => choice.name.startsWith(focusedValue));
console.log(focusedValue, choices, filtered);
await interaction.respond(filtered);
}
*/
/**
* The function to run when the command is called by a discord user
* @param {any} nodeIo The nodeIO server for manipulation of sockets
* @param {any} interaction The interaction object
*/
export const execute = async (nodeIo, interaction) => {
try {
const sockets = await nodeIo.allSockets();
console.log("All open sockets: ",sockets);
//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());
}
}

View File

@@ -0,0 +1,35 @@
import { SlashCommandBuilder } from 'discord.js';
import { requestNodeUpdate } from '../../modules/socketServerWrappers.mjs';
// Exporting data property that contains the command structure for discord including any params
export const data = new SlashCommandBuilder()
.setName('update')
.setDescription('Updates all nodes currently logged on');
// Exporting other properties
export const example = "/update"; // An example of how the command would be run in discord chat, this will be used for the help command
export const deferInitialReply = false; // If we the initial reply in discord should be deferred. This gives extra time to respond, however the method of replying is different.
/**
* The function to run when the command is called by a discord user
* @param {any} nodeIo The nodeIO server for manipulation of sockets
* @param {any} interaction The interaction object
*/
export const execute = async (nodeIo, interaction) => {
try {
const openSockets = [...await nodeIo.allSockets()]; // TODO - Filter the returned nodes to only nodes that have the radio capability
console.log("All open sockets: ", openSockets);
// Check each open socket to see if the node has the requested system
await Promise.all(openSockets.map(openSocket => {
openSocket = nodeIo.sockets.sockets.get(openSocket);
requestNodeUpdate(openSocket);
}));
//await interaction.reply(`**Online Sockets: '${sockets}'**`);
await interaction.reply('All nodes have been requested to update');
//await interaction.channel.send('**Pong.**');
} catch (err) {
console.error(err);
// await interaction.reply(err.toString());
}
}

92
discordBot/discordBot.mjs Normal file
View File

@@ -0,0 +1,92 @@
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';
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} _commandsPath="./commands"
* @returns {any}
*/
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 = await join(commandsPath, file);
console.log(`Adding enabled command: ${filePath}`);
await import(`file://${filePath}`).then(command => {
if (command.data instanceof Promise) {
command.data.then(async (builder) => {
command.data = builder;
console.log("Importing command: ", command.data.name, command);
// Set a new item in the Collection
// With the key as the command name and the value as the exported module
serverClient.commands.set(command.data.name, command);
});
} else {
console.log("Importing command: ", command.data.name, command);
// Set a new item in the Collection
// With the key as the command name and the value as the exported module
serverClient.commands.set(command.data.name, command);
}
})
}
// 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} _eventsPath="./events"
* @returns {any}
*/
export function addEnabledEventListeners(serverClient, _eventsPath = "./events") {
const eventsPath = join(__dirname, _eventsPath);
const eventFiles = readdirSync(eventsPath).filter(file => file.endsWith('.mjs'));
for (const file of eventFiles) {
const filePath = join(eventsPath, file);
console.log(`Adding enabled event listener: ${filePath}`);
import(`file://${filePath}`).then(event => {
console.log("Adding event: ", event);
if (event.once) {
serverClient.once(event.name, (...args) => event.execute(serverClient.nodeIo, ...args));
} else {
serverClient.on(event.name, (...args) => event.execute(serverClient.nodeIo, ...args));
}
})
}
}
// The discord client
export const serverClient = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates] });
// 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);

View File

@@ -0,0 +1,32 @@
import { Events } from 'discord.js';
export const name = Events.InteractionCreate;
export async function execute(nodeIo, interaction) {
const command = interaction.client.commands.get(interaction.commandName);
console.log("Interaction created for command: ", command);
// Execute autocomplete if the user is checking autocomplete
if (interaction.isAutocomplete()) {
console.log("Running autocomplete for command: ", command.data.name);
return await command.autocomplete(nodeIo, interaction);
}
// Check if the interaction is a command
if (!interaction.isChatInputCommand()) return;
if (!command) {
console.error(`No command matching ${interaction.commandName} was found.`);
return;
}
console.log(`${interaction.member.user} is running '${interaction.commandName}'`);
// Defer the initial reply if the command has the parameter set
if (command.deferInitialReply) {
await interaction.deferReply();
}
// Execute the command
command.execute(nodeIo, interaction);
}

View File

@@ -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;
}

View File

@@ -0,0 +1,48 @@
import { checkIfNodeIsConnectedToVC, getNodeDiscordID, getNodeDiscordUsername } from '../../modules/socketServerWrappers.mjs';
import { getAllDiscordIDs } from '../../modules/mongoDiscordIDWrappers.mjs'
export const checkOnlineBotsInGuild = async (nodeIo, guildId) => {
let onlineBots = [];
const openSockets = [...await nodeIo.allSockets()];
await Promise.all(openSockets.map(async openSocket => {
openSocket = await nodeIo.sockets.sockets.get(openSocket);
const connected = await checkIfNodeIsConnectedToVC(nodeIo, guildId, openSocket.node.nuid);
console.log("Connected:", connected);
if (connected) {
const username = await getNodeDiscordUsername(openSocket, guildId);
const discordID = await getNodeDiscordID(openSocket);
onlineBots.push({
name: username,
discord_id: discordID,
nuid: openSocket.node.nuid
});
}
}));
return onlineBots;
}
export const getAvailableTokensInGuild = async (nodeIo, guildId) => {
try {
// Execute both asynchronous functions concurrently
const [discordIDs, onlineBots] = await Promise.all([
getAllDiscordIDs(), // Fetch all Discord IDs
checkOnlineBotsInGuild(nodeIo, guildId) // Check online bots in the guild
]);
// Use the results of both promises here
console.log("Available Discord IDs:", discordIDs);
console.log("Online bots in the guild:", onlineBots);
// Filter any discordIDs that are not active
const availableDiscordIDs = discordIDs.filter(discordID => discordID.active == true).filter(discordID => !onlineBots.some(bot => Number(bot.discord_id) == discordID.discord_id));
// Return the unavailable discordIDs
return availableDiscordIDs;
} catch (error) {
console.error('Error getting available tokens in guild:', error);
throw error;
}
};

25
makefile Normal file
View File

@@ -0,0 +1,25 @@
# Define variables
DOCKER_IMAGE_NAME := drb-server
# Define targets and rules
.PHONY: clean build run
clean:
@echo "Cleaning existing Docker images, containers, and builds..."
docker stop drb || true
docker rm drb || true
docker rmi $(DOCKER_IMAGE_NAME) || true
build:
@echo "Building Docker image..."
docker build -t $(DOCKER_IMAGE_NAME) .
run:
@echo "Running Docker container..."
docker run -d --rm -e NODE_ENV=${NODE_ENV} \
-e SERVER_PORT=${SERVER_PORT} \
-e MONGO_URL=${MONGO_URL} \
-e DISCORD_TOKEN=${DISCORD_TOKEN} \
-p ${SERVER_PORT}:${SERVER_PORT} \
--name=drb \
$(DOCKER_IMAGE_NAME)

31
modules/addonManager.mjs Normal file
View File

@@ -0,0 +1,31 @@
import { fileURLToPath } from 'url';
import fs from 'fs';
import path from 'path';
// Function to load addons from the addons directory
export const loadAddons = async (nodeIo) => {
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const addonsDir = path.join(__dirname, '../addons');
// Read the directory containing addon modules
const addonDirectories = await fs.readdirSync(addonsDir, { withFileTypes: true });
addonDirectories.forEach(addonDir => {
if (addonDir.isDirectory()) {
const addonConfigPath = path.join(addonsDir, addonDir.name, 'config.json');
if (fs.existsSync(addonConfigPath)) {
const addonConfig = JSON.parse(fs.readFileSync(addonConfigPath, 'utf-8'));
if (addonConfig.enabled) {
const addonIndexPath = path.join(addonsDir, addonDir.name, 'index.js');
import(`file://${addonIndexPath}`).then(addonModule => {
console.log("Loading addon: ", addonModule);
addonModule.initialize(nodeIo, addonConfig);
console.log(`Addon ${addonConfig.name} loaded.`);
});
}
}
}
});
}

View File

@@ -0,0 +1,90 @@
import { insertDocument, getDocuments, connectToDatabase } from "./mongoHandler.mjs";
const collectionName = 'discord-ids';
// Wrapper for inserting a Discord ID
export const createDiscordID = async (discordID) => {
try {
const insertedId = await insertDocument(collectionName, discordID);
return insertedId;
} catch (error) {
console.error('Error creating Discord ID:', error);
throw error;
}
};
// Wrapper for retrieving all Discord IDs
export const getAllDiscordIDs = async () => {
try {
const discordIDs = await getDocuments(collectionName);
return discordIDs;
} catch (error) {
console.error('Error getting all Discord IDs:', error);
throw error;
}
};
// Wrapper for retrieving a Discord ID by name or discord_id
export const getDiscordID = async (identifier) => {
const db = await connectToDatabase();
try {
const collection = db.db().collection(collectionName);
const discordID = await collection.findOne({
$or: [
{ name: identifier },
{ discord_id: identifier }
]
});
return discordID;
} catch (error) {
console.error('Error getting Discord ID:', error);
throw error;
} finally {
// Close the connection
await db.close();
}
};
// Wrapper for updating a Discord ID by name or discord_id
export const updateDiscordID = async (identifier, updatedFields) => {
const db = await connectToDatabase();
try {
const collection = db.db().collection(collectionName);
const result = await collection.updateOne({
$or: [
{ name: identifier },
{ discord_id: identifier }
]
}, { $set: updatedFields });
console.log('Discord ID updated:', result.modifiedCount);
return result.modifiedCount;
} catch (error) {
console.error('Error updating Discord ID:', error);
throw error;
} finally {
// Close the connection
await db.close();
}
};
// Wrapper for deleting a Discord ID by name or discord_id
export const deleteDiscordID = async (identifier) => {
const db = await connectToDatabase();
try {
const collection = db.db().collection(collectionName);
const result = await collection.deleteOne({
$or: [
{ name: identifier },
{ discord_id: identifier }
]
});
console.log('Discord ID deleted:', result.deletedCount);
return result.deletedCount;
} catch (error) {
console.error('Error deleting Discord ID:', error);
throw error;
} finally {
// Close the connection
await db.close();
}
};

53
modules/mongoHandler.mjs Normal file
View File

@@ -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();
}
};

View File

@@ -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();
}
};

View File

@@ -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();
}
};

39
modules/socketServer.mjs Normal file
View File

@@ -0,0 +1,39 @@
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);
export const nodeIo = new Server(server);
app.use(morgan('tiny'));
app.get('/', (req, res) => {
res.send('<h1>Hello world</h1>');
});
nodeIo.on('connection', (socket) => {
console.log('a user connected', socket.id);
socket.on('node-login', async (data) => {
await nodeLoginWrapper(data, socket);
await socket.emit('node-login-successful');
})
socket.on('node-update', async (data) => {
let tempPromises = [];
tempPromises.push(nodeUpdateWrapper(data.node));
tempPromises.push(nearbySystemsUpdateWraper(data.node.nuid, data.nearbySystems));
await Promise.all(tempPromises);
await socket.emit('node-update-successful')
})
socket.on('disconnect', () => {
nodeDisconnectWrapper(socket.id);
});
});

View File

@@ -0,0 +1,315 @@
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);
console.log("Added new node to the database:", insertedId);
} else {
// Check for updates
const updatedNode = await updateNodeByNuid(data.nuid, data)
console.log("Updated node:", updatedNode);
}
node = await getNodeByNuid(data.nuid);
// Add the socket/node connection
socket.node = node;
return;
}
/**
* Disconnect the client from the server
* @param {string} socketId The socket ID that was disconnected
* @returns {any}
*/
export const nodeDisconnectWrapper = async (socketId) => {
// TODO - Let any server know that a bot has disconnected if the bot was joined to vc? might not be worth cpu lol
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
*/
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)
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
*/
export const getSocketIdByNuid = async (nodeIo, nuid) => {
const openSockets = await nodeIo.allSockets();
for (const openSocketId of openSockets) {
console.log(openSockets)
const openSocket = await nodeIo.sockets.sockets.get(openSocketId);
if (openSocket.node.nuid == nuid)
return openSocket;
}
return null;
}
/**
* Get all nodes that are connected to a voice channel
* @param {any} nodeIo The nodeIo object that contains the IO server
* @param {string} guildId The guild ID string for the guild we are looking in
* @returns {Array} The sockets connected to VC in a given server
*/
export const getAllSocketsConnectedToVC = async (nodeIo, guildId) => {
// Get all open socket nodes
// TODO - require a server guild to filter the results, ie this would be able to check what server the VCs the nodes are connected are in
const openSockets = [...await nodeIo.allSockets()]; // TODO - Filter the returned nodes to only nodes that have the radio capability
// Check each open socket to see if the node has the requested system
const socketsConnectedToVC = []
await Promise.all(openSockets.map(async openSocket => {
openSocket = await nodeIo.sockets.sockets.get(openSocket);
await new Promise((res) => {
openSocket.emit('node-check-connected-status', guildId, (status) => {
if (status) {
console.log("Socket is connected to VC:", openSocket.node.name, status);
socketsConnectedToVC.push(openSocket);
} else {
console.log("Socket is NOT connected to VC:", openSocket.node.name);
}
res();
})
});
}));
return socketsConnectedToVC;
}
/**
* Check if the given node has an open discord client
* @param {any} openSocket The open socket connection with the node to check
* @returns {boolean} If the given node has an open discord client or not
*/
export const checkIfNodeHasOpenDiscordClient = async (openSocket) => {
// Check the open socket to see if the node has an open discord client
let hasOpenDiscordClient = false;
await new Promise((res) => {
openSocket.emit('node-check-discord-open-client', (status) => {
if (status) {
console.log("Socket has an open discord client:", openSocket.node.name, status);
hasOpenDiscordClient = true;
} else {
console.log("Socket does NOT have an open discord client:", openSocket.node.name);
}
res();
})
});
return hasOpenDiscordClient;
}
export const getNodeCurrentListeningSystem = async (openSocket) => {
const hasOpenClient = checkIfNodeHasOpenDiscordClient(openSocket);
if (!hasOpenClient) return undefined;
// check what system the socket is listening to
let currentSystem = undefined;
await new Promise((res) => {
openSocket.emit('node-check-current-system', (system) => {
if (system) {
console.log("Socket is listening to system:", openSocket.node.name, system);
currentSystem = system;
} else {
console.log("Socket is not currently listening to a system:", openSocket.node.name);
}
res();
})
});
return currentSystem;
}
/**
* Wrapper to check if the given NUID is connected to a VC
* @param {any} nodeIo The nodeIo object that contains the IO server
* @param {string} nuid The NUID string that we would like to find in the open socket connections
* @returns {boolean} If the node is connected to VC in the given server
*/
export const checkIfNodeIsConnectedToVC = async (nodeIo, guildId, nuid) => {
const socketsConnectedToVC = await getAllSocketsConnectedToVC(nodeIo, guildId);
for (const socket of socketsConnectedToVC) {
if (socket.node.nuid === nuid) {
return true;
}
}
return false;
}
/**
* Get the discord username from a given socket
* @param {any} socket The socket object of the node to check the username of
* * @param {string} guildId The guild ID to check the username in
* @returns {string} The username of the bot in the requested server
*/
export const getNodeDiscordUsername = async (socket, guildId) => {
return await new Promise((res) => {
socket.emit('node-get-discord-username', guildId, (username) => {
res(username);
});
});
}
/**
* Get the discord ID from a given socket
* @param {any} socket The socket object of the node to check the ID of
* @returns {string} The ID of the bot
*/
export const getNodeDiscordID = async (socket) => {
return await new Promise((res) => {
socket.emit('node-get-discord-id', (discordID) => {
res(discordID);
});
});
}
/**
* Request a given socket node to join a given voice channel
* @param {any} socket The socket object of the node the request should be sent to
* @param {any} systemName The system preset name that we would like to listen to
* @param {string} discordChanelId The Discord channel ID to join the listening bot to
*/
export const requestNodeJoinSystem = async (socket, systemName, discordChanelId, discordToken = "MTE5NjAwNTM2ODYzNjExMjk3Nw.GuCMXg.24iNNofNNumq46FIj68zMe9RmQgugAgfrvelEA") => {
// Join the system
const joinData = {
'clientID': discordToken,
'channelID': discordChanelId,
'system': systemName
}
// Send the command to the node
await sendNodeCommand(socket, "node-join", joinData);
}
/**
* Request a given socket node to leave VC in a given server
* @param {any} socket The socket object of the node the request should be sent to
* @param {string} guildId The guild ID to disconnect the socket node from
*/
export const requestBotLeaveServer = async (socket, guildId) => {
// Send the command to the node
await sendNodeCommand(socket, "node-leave", guildId);
}
/**
* Requset a given socket node to update themselves
* @param {any} socket The socket object of the node to request to update
*/
export const requestNodeUpdate = async (socket) => {
await sendNodeCommand(socket, 'node-update', (status) => {
if (status) {
console.log("Node is out of date, updating now", socket.node.name);
} else {
console.log("Node is up to date", socket.node.name);
}
});
}

2316
package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

26
package.json Normal file
View File

@@ -0,0 +1,26 @@
{
"name": "drb-server",
"version": "3.0.0",
"description": "",
"main": "server.js",
"scripts": {
"test": "mocha --timeout 5000",
"start": "node server.js"
},
"author": "Logan Cusano",
"license": "ISC",
"type": "module",
"devDependencies": {
"chai": "^5.1.0",
"mocha": "^10.4.0",
"socket.io-client": "^4.7.5"
},
"dependencies": {
"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"
}
}

18
server.js Normal file
View File

@@ -0,0 +1,18 @@
import { nodeIo, app, server } from './modules/socketServer.mjs';
import { loadAddons } from './modules/addonManager.mjs';
import { serverClient, addEnabledEventListeners } from './discordBot/discordBot.mjs';
import dotenv from 'dotenv';
dotenv.config()
// Startup the node server
server.listen(process.env.SERVER_PORT || 3000, () => {
console.log(`server running at http://localhost:${process.env.SERVER_PORT}`);
});
// Add objects to the others
serverClient.nodeIo = nodeIo;
nodeIo.serverClient = serverClient;
// Load the addons
loadAddons(nodeIo);

View File

@@ -0,0 +1,290 @@
// Import necessary modules for testing
import { expect } from 'chai';
import ioClient from 'socket.io-client';
import { deleteNodeByNuid, getNodeByNuid } from '../modules/mongoNodesWrappers.mjs';
import { deleteSystemByName, getSystemByName } from '../modules/mongoSystemsWrappers.mjs';
import { nodeIo } from '../modules/socketServer.mjs';
import dotenv from 'dotenv';
dotenv.config()
process.env.SERVER_PORT = 6000
// Define necessary variables for testing, such as mocked database connections or socket instances
const localNodeConfig = {
serverIp: 'localhost',
serverPort: process.env.SERVER_PORT,
node: {
nuid: "4f29a6340901a12affc87047c0ac16b01b92496c460c880a2459abe8c7928374",
name: "testyv7",
location: "china",
capabilities: ["radio"]
},
nearbySystems: {
"Testing P25 System Name": {
"frequencies": [
155344000,
155444000,
155555000,
155588550
],
"mode": "p25",
"trunkFile": "trunk.tsv",
"whitelistFile": "whitelist.tsv"
}
}
};
const updatedLocalNodeConfig = {
node: {
nuid: localNodeConfig.node.nuid,
name: "updatedName",
location: "updatedLocation",
capabilities: ["radio", "weather"] // Updated capabilities
},
nearbySystems: {
"Testing P25 System Name": {
"frequencies": [
155444000,
155555000,
155500000
],
"mode": "p25",
"trunkFile": "trunk2.tsv",
"whitelistFile": "whitelist2.tsv"
}
}
};
// Start the Socket.IO server before running tests
let clientSocket; // The socket client
let serverClientSocket // The open client socket on the server
before(done => {
// Startup the node server
nodeIo.listen(process.env.SERVER_PORT || 3000, () => {
console.log(`server running at http://localhost:${process.env.SERVER_PORT}`);
});
// Connect a client socket to the server
clientSocket = ioClient.connect(`http://localhost:${process.env.SERVER_PORT}`);
nodeIo.on('connection', (socket) => {
serverClientSocket = socket;
done();
})
});
// Close the Socket.IO server after running tests
after(async () => {
// Disconnect client socket
clientSocket.disconnect();
// Close the server
nodeIo.close();
// Remove the test data
deleteNodeByNuid(localNodeConfig.node.nuid); // Delete the user
deleteSystemByName(Object.keys(localNodeConfig.nearbySystems)[0])
});
describe('Node Core Server Tests', () => {
// Test Node Login functionality
describe('Node Login', () => {
it('Should add a new node if it does not exist', async () => {
// Simulate a node login request
// Use the getNodeByNuid mock function to simulate checking if node exists
const existingNode = await getNodeByNuid(localNodeConfig.node.nuid);
// Assert that existingNode is null before node login
expect(existingNode).to.be.null;
// Wait for the update
const node_login = new Promise(res => {
clientSocket.on('node-login-successful', async () => {
res();
});
});
// Emit the login command
clientSocket.emit("node-login", localNodeConfig.node);
// Wait for the successful login event
await node_login;
// Now we need to check if the node is added to the database
// We can use getNodeByNuid again to verify if the node was added correctly
const addedNode = await getNodeByNuid(localNodeConfig.node.nuid);
console.log("Added Node:", addedNode);
// Assert that the node is added correctly
expect(addedNode).to.have.property('_id'); // Check if _id property exists
expect(addedNode).to.have.property('nuid', localNodeConfig.node.nuid);
expect(addedNode).to.have.property('name', localNodeConfig.node.name);
expect(addedNode).to.have.property('location', localNodeConfig.node.location);
expect(addedNode).to.have.deep.property('capabilities', localNodeConfig.node.capabilities);
})
it('Should update a node if it exists', async () => {
// Simulate a node login request
// Use the getNodeByNuid mock function to simulate checking if node exists
const existingNode = await getNodeByNuid(localNodeConfig.node.nuid);
// Assert that existingNode is matches the existing data before logging in
expect(existingNode).to.have.property('_id'); // Check if _id property exists
expect(existingNode).to.have.property('nuid', localNodeConfig.node.nuid);
expect(existingNode).to.have.property('name', localNodeConfig.node.name);
expect(existingNode).to.have.property('location', localNodeConfig.node.location);
expect(existingNode).to.have.deep.property('capabilities', localNodeConfig.node.capabilities);
// Wait for the update
const node_login = new Promise(res => {
clientSocket.on('node-login-successful', async () => {
res();
});
});
// Emit the login command
clientSocket.emit("node-login", updatedLocalNodeConfig.node);
// Wait for the successful login event
await node_login;
// Now we need to check if the node is added to the database
// We can use getNodeByNuid again to verify if the node was added correctly
const updatedNode = await getNodeByNuid(localNodeConfig.node.nuid);
console.log("Updated Node:", updatedNode);
// Assert that the node is added correctly
expect(updatedNode).to.have.property('_id'); // Check if _id property exists
expect(updatedNode).to.have.property('nuid', updatedLocalNodeConfig.node.nuid);
expect(updatedNode).to.have.property('name', updatedLocalNodeConfig.node.name);
expect(updatedNode).to.have.property('location', updatedLocalNodeConfig.node.location);
expect(updatedNode).to.have.deep.property('capabilities', updatedLocalNodeConfig.node.capabilities);
})
});
// Test Node Update functionality
describe('Node Update', () => {
it('Should add a node\'s nearby systems', async () => {
// Simulate an update request sent from the client to the server
// Get the existing node in the database
const existingNode = await getNodeByNuid(localNodeConfig.node.nuid);
// Assert that existingNode matches the updatedLocalNodeConfig
expect(existingNode).to.have.property('_id'); // Check if _id property exists
expect(existingNode).to.have.property('nuid', updatedLocalNodeConfig.node.nuid);
expect(existingNode).to.have.property('name', updatedLocalNodeConfig.node.name);
expect(existingNode).to.have.property('location', updatedLocalNodeConfig.node.location);
expect(existingNode).to.have.deep.property('capabilities', updatedLocalNodeConfig.node.capabilities);
// Get the system from the DB
const existsingSystem = await getSystemByName("Testing P25 System Name");
// Assert that there is no existing system in the DB
expect(existsingSystem).to.be.null;
// Wait for the update
const node_system_update = new Promise(res => {
clientSocket.on('node-update-successful', async () => {
res();
});
});
// Emit the update command
clientSocket.emit("node-update", updatedLocalNodeConfig);
// Wait for the successful update event
await node_system_update;
// Now we need to check if the system is added to the database
// We can use getNodeByNuid again to verify if the node was added correctly
const updatedNode = await getNodeByNuid(localNodeConfig.node.nuid);
console.log("Updated Node:", updatedNode);
// Assert that the node is added correctly
expect(updatedNode).to.have.property('_id'); // Check if _id property exists
expect(updatedNode).to.have.property('nuid', updatedLocalNodeConfig.node.nuid);
expect(updatedNode).to.have.property('name', updatedLocalNodeConfig.node.name);
expect(updatedNode).to.have.property('location', updatedLocalNodeConfig.node.location);
expect(updatedNode).to.have.deep.property('capabilities', updatedLocalNodeConfig.node.capabilities);
// Get the updated system
const addedSystem = await getSystemByName("Testing P25 System Name");
console.log("Added system:", addedSystem);
expect(addedSystem).to.have.property('_id'); // Check if _id property exists
expect(addedSystem).to.have.property('nodes'); // Check if nodes property exists
expect(addedSystem.nodes).to.include(updatedLocalNodeConfig.node.nuid) // Check if this node ID is in the nodes array
expect(addedSystem).to.have.deep.property('frequencies', updatedLocalNodeConfig.nearbySystems['Testing P25 System Name'].frequencies);
expect(addedSystem).to.have.property('mode', updatedLocalNodeConfig.nearbySystems['Testing P25 System Name'].mode);
});
it('Should update a node and its nearby systems', async () => {
// Get the existing node in the database
const existingNode = await getNodeByNuid(localNodeConfig.node.nuid);
// Assert that existingNode matches the updatedLocalNodeConfig
expect(existingNode).to.have.property('_id'); // Check if _id property exists
expect(existingNode).to.have.property('nuid', updatedLocalNodeConfig.node.nuid);
expect(existingNode).to.have.property('name', updatedLocalNodeConfig.node.name);
expect(existingNode).to.have.property('location', updatedLocalNodeConfig.node.location);
expect(existingNode).to.have.deep.property('capabilities', updatedLocalNodeConfig.node.capabilities);
// Get the updated system
const existingSystem = await getSystemByName("Testing P25 System Name");
expect(existingSystem).to.have.property('_id'); // Check if _id property exists
expect(existingSystem).to.have.property('nodes'); // Check if nodes property exists
expect(existingSystem.nodes).to.include(updatedLocalNodeConfig.node.nuid) // Check if this node ID is in the nodes array
expect(existingSystem).to.have.deep.property('frequencies', updatedLocalNodeConfig.nearbySystems['Testing P25 System Name'].frequencies);
expect(existingSystem).to.have.property('mode', updatedLocalNodeConfig.nearbySystems['Testing P25 System Name'].mode);
// Wait for the update
const node_update = new Promise(res => {
clientSocket.on('node-update-successful', async () => {
res();
});
});
// Emit the update command
clientSocket.emit("node-update", localNodeConfig);
// Wait for the successful update event
await node_update;
const updatedNode = await getNodeByNuid(localNodeConfig.node.nuid);
console.log("Updated Node:", updatedNode);
// Assert that the node is added correctly
expect(updatedNode).to.have.property('_id'); // Check if _id property exists
expect(updatedNode).to.have.property('nuid', localNodeConfig.node.nuid);
expect(updatedNode).to.have.property('name', localNodeConfig.node.name);
expect(updatedNode).to.have.property('location', localNodeConfig.node.location);
expect(updatedNode).to.have.deep.property('capabilities', localNodeConfig.node.capabilities);
// Get the updated system
const updatedSystem = await getSystemByName("Testing P25 System Name");
console.log("Updated system:", updatedSystem);
expect(updatedSystem).to.have.property('_id'); // Check if _id property exists
expect(updatedSystem).to.have.property('nodes'); // Check if nodes property exists
expect(updatedSystem.nodes).include(localNodeConfig.node.nuid) // Check if this node ID is in the nodes array
expect(updatedSystem).to.have.deep.property('frequencies', localNodeConfig.nearbySystems['Testing P25 System Name'].frequencies);
expect(updatedSystem).to.have.property('mode', localNodeConfig.nearbySystems['Testing P25 System Name'].mode);
});
});
describe('Node Disconnect', () => {
it('Should trigger cleanup actions upon socket disconnection', async () => {
// Write test code to simulate a socket disconnection
// Check if the appropriate cleanup actions are triggered
});
})
});