Initial move
This commit is contained in:
301
.gitignore
vendored
Normal file
301
.gitignore
vendored
Normal 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
21
Dockerfile
Normal 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", "."]
|
||||
7
addons/example/config.json
Normal file
7
addons/example/config.json
Normal file
@@ -0,0 +1,7 @@
|
||||
{
|
||||
"name": "Addon 1",
|
||||
"enabled": false,
|
||||
"options": {
|
||||
"eventName": "connection"
|
||||
}
|
||||
}
|
||||
17
addons/example/index.js
Normal file
17
addons/example/index.js
Normal 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);
|
||||
});
|
||||
}
|
||||
154
discordBot/commands/join.mjs
Normal file
154
discordBot/commands/join.mjs
Normal 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());
|
||||
}
|
||||
}
|
||||
56
discordBot/commands/leave.mjs
Normal file
56
discordBot/commands/leave.mjs
Normal 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());
|
||||
}
|
||||
}
|
||||
43
discordBot/commands/ping.mjs
Normal file
43
discordBot/commands/ping.mjs
Normal 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());
|
||||
}
|
||||
}
|
||||
35
discordBot/commands/update.mjs
Normal file
35
discordBot/commands/update.mjs
Normal 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
92
discordBot/discordBot.mjs
Normal 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);
|
||||
32
discordBot/events/interactionCreate.mjs
Normal file
32
discordBot/events/interactionCreate.mjs
Normal 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);
|
||||
}
|
||||
83
discordBot/modules/registerCommands.mjs
Normal file
83
discordBot/modules/registerCommands.mjs
Normal 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;
|
||||
}
|
||||
48
discordBot/modules/wrappers.mjs
Normal file
48
discordBot/modules/wrappers.mjs
Normal 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
25
makefile
Normal 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
31
modules/addonManager.mjs
Normal 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.`);
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
90
modules/mongoDiscordIDWrappers.mjs
Normal file
90
modules/mongoDiscordIDWrappers.mjs
Normal 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
53
modules/mongoHandler.mjs
Normal 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();
|
||||
}
|
||||
};
|
||||
75
modules/mongoNodesWrappers.mjs
Normal file
75
modules/mongoNodesWrappers.mjs
Normal 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();
|
||||
}
|
||||
};
|
||||
111
modules/mongoSystemsWrappers.mjs
Normal file
111
modules/mongoSystemsWrappers.mjs
Normal 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
39
modules/socketServer.mjs
Normal 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);
|
||||
});
|
||||
|
||||
});
|
||||
315
modules/socketServerWrappers.mjs
Normal file
315
modules/socketServerWrappers.mjs
Normal 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
2316
package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
26
package.json
Normal file
26
package.json
Normal 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
18
server.js
Normal 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);
|
||||
290
test/socketServerWrappers.test.js
Normal file
290
test/socketServerWrappers.test.js
Normal 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
|
||||
});
|
||||
})
|
||||
});
|
||||
Reference in New Issue
Block a user