From 4023a7fc2cd12c97fac7e58b292e5a0cd079eed9 Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Sun, 12 May 2024 12:54:20 -0400 Subject: [PATCH] Initial move --- .gitignore | 301 +++ Dockerfile | 21 + addons/example/config.json | 7 + addons/example/index.js | 17 + discordBot/commands/join.mjs | 154 ++ discordBot/commands/leave.mjs | 56 + discordBot/commands/ping.mjs | 43 + discordBot/commands/update.mjs | 35 + discordBot/discordBot.mjs | 92 + discordBot/events/interactionCreate.mjs | 32 + discordBot/modules/registerCommands.mjs | 83 + discordBot/modules/wrappers.mjs | 48 + makefile | 25 + modules/addonManager.mjs | 31 + modules/mongoDiscordIDWrappers.mjs | 90 + modules/mongoHandler.mjs | 53 + modules/mongoNodesWrappers.mjs | 75 + modules/mongoSystemsWrappers.mjs | 111 ++ modules/socketServer.mjs | 39 + modules/socketServerWrappers.mjs | 315 +++ package-lock.json | 2316 +++++++++++++++++++++++ package.json | 26 + server.js | 18 + test/socketServerWrappers.test.js | 290 +++ 24 files changed, 4278 insertions(+) create mode 100644 .gitignore create mode 100644 Dockerfile create mode 100644 addons/example/config.json create mode 100644 addons/example/index.js create mode 100644 discordBot/commands/join.mjs create mode 100644 discordBot/commands/leave.mjs create mode 100644 discordBot/commands/ping.mjs create mode 100644 discordBot/commands/update.mjs create mode 100644 discordBot/discordBot.mjs create mode 100644 discordBot/events/interactionCreate.mjs create mode 100644 discordBot/modules/registerCommands.mjs create mode 100644 discordBot/modules/wrappers.mjs create mode 100644 makefile create mode 100644 modules/addonManager.mjs create mode 100644 modules/mongoDiscordIDWrappers.mjs create mode 100644 modules/mongoHandler.mjs create mode 100644 modules/mongoNodesWrappers.mjs create mode 100644 modules/mongoSystemsWrappers.mjs create mode 100644 modules/socketServer.mjs create mode 100644 modules/socketServerWrappers.mjs create mode 100644 package-lock.json create mode 100644 package.json create mode 100644 server.js create mode 100644 test/socketServerWrappers.test.js diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..2ef0782 --- /dev/null +++ b/.gitignore @@ -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/ diff --git a/Dockerfile b/Dockerfile new file mode 100644 index 0000000..dd3de19 --- /dev/null +++ b/Dockerfile @@ -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", "."] diff --git a/addons/example/config.json b/addons/example/config.json new file mode 100644 index 0000000..f6cff28 --- /dev/null +++ b/addons/example/config.json @@ -0,0 +1,7 @@ +{ + "name": "Addon 1", + "enabled": false, + "options": { + "eventName": "connection" + } + } \ No newline at end of file diff --git a/addons/example/index.js b/addons/example/index.js new file mode 100644 index 0000000..2ba6e20 --- /dev/null +++ b/addons/example/index.js @@ -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); + }); +} diff --git a/discordBot/commands/join.mjs b/discordBot/commands/join.mjs new file mode 100644 index 0000000..9c28cd5 --- /dev/null +++ b/discordBot/commands/join.mjs @@ -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()); + } +} \ No newline at end of file diff --git a/discordBot/commands/leave.mjs b/discordBot/commands/leave.mjs new file mode 100644 index 0000000..2539175 --- /dev/null +++ b/discordBot/commands/leave.mjs @@ -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()); + } +} \ No newline at end of file diff --git a/discordBot/commands/ping.mjs b/discordBot/commands/ping.mjs new file mode 100644 index 0000000..6b4cc2e --- /dev/null +++ b/discordBot/commands/ping.mjs @@ -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()); + } +} \ No newline at end of file diff --git a/discordBot/commands/update.mjs b/discordBot/commands/update.mjs new file mode 100644 index 0000000..b9f4150 --- /dev/null +++ b/discordBot/commands/update.mjs @@ -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()); + } +} \ No newline at end of file diff --git a/discordBot/discordBot.mjs b/discordBot/discordBot.mjs new file mode 100644 index 0000000..d09feeb --- /dev/null +++ b/discordBot/discordBot.mjs @@ -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); diff --git a/discordBot/events/interactionCreate.mjs b/discordBot/events/interactionCreate.mjs new file mode 100644 index 0000000..0aa7482 --- /dev/null +++ b/discordBot/events/interactionCreate.mjs @@ -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); +} \ No newline at end of file diff --git a/discordBot/modules/registerCommands.mjs b/discordBot/modules/registerCommands.mjs new file mode 100644 index 0000000..cfadb0e --- /dev/null +++ b/discordBot/modules/registerCommands.mjs @@ -0,0 +1,83 @@ +import { REST, Routes } from 'discord.js'; + +import dotenv from 'dotenv'; +dotenv.config() + +const discordToken = process.env.DISCORD_TOKEN; + +export const registerActiveCommands = async (serverClient) => { + const guildIDs = serverClient.guilds.cache; + const clientId = serverClient.user.id; + const commands = await serverClient.commands.map(command => command = command.data.toJSON()); + + // Construct and prepare an instance of the REST module + const rest = new REST({ version: '10' }).setToken(discordToken); + + // and deploy your commands! + guildIDs.forEach(guild => { + console.log("Deploying commands for: ", guild.id); + console.log("Commands", commands); + (async () => { + try { + console.log(`Started refreshing application (/) commands for guild ID: ${guild.id}.`); + // The put method is used to fully refresh all commands in the guild with the current set + const data = await rest.put( + Routes.applicationGuildCommands(clientId, guild.id), + { body: commands }, + ); + + console.log(`Successfully reloaded ${data.length} application (/) commands for guild ID: ${guild.id}.`); + } catch (error) { + // And of course, make sure you catch and log any errors! + console.log("ERROR Deploying commands: ", error, "Body from error: ", commands); + } + })() + }) +}; + +/** + * Remove all commands for a given bot in a given guild + * + * @param {any} serverClient The discord bot client + */ +export const unregisterAllCommands = async (serverClient) => { + const guildIDs = serverClient.guilds.cache; + const clientId = serverClient.user.id; + commands = []; + + const rest = new REST({ version: '10' }).setToken(discordToken); + guildIDs.forEach(guild => { + console.log("Removing commands for: ", clientId, guild.id); + (async () => { + try { + console.log(`Started removal of ${commands.length} application (/) commands for guild ID: ${guild.id}.`); + // The put method is used to fully refresh all commands in the guild with the current set + const data = await rest.put( + Routes.applicationGuildCommands(clientId, guild.id), + { body: commands }, + ); + + console.log(`Successfully removed ${data.length} application (/) commands for guild ID: ${guild.id}.`); + } catch (error) { + // And of course, make sure you catch and log any errors! + console.log("ERROR removing commands: ", error, "Body from error: ", commands); + } + })() + }) + +} + +/** + * This named wrapper will remove all commands and then re-add the commands back, effectively refreshing them + * @param {any} serverClient The discord bot client object + * @returns {any} + */ +export const refreshActiveCommandsWrapper = async (serverClient) => { + // Remove all commands + console.log("Removing/Unregistering all commands from all connected servers/guilds"); + await unregisterAllCommands(serverClient); + // Deploy the active commands + console.log("Adding commands to all connected servers/guilds"); + await registerActiveCommands(serverClient); + return; +} \ No newline at end of file diff --git a/discordBot/modules/wrappers.mjs b/discordBot/modules/wrappers.mjs new file mode 100644 index 0000000..eec1bbb --- /dev/null +++ b/discordBot/modules/wrappers.mjs @@ -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; + } +}; \ No newline at end of file diff --git a/makefile b/makefile new file mode 100644 index 0000000..375217c --- /dev/null +++ b/makefile @@ -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) \ No newline at end of file diff --git a/modules/addonManager.mjs b/modules/addonManager.mjs new file mode 100644 index 0000000..ca63c51 --- /dev/null +++ b/modules/addonManager.mjs @@ -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.`); + }); + } + } + } + }); +} diff --git a/modules/mongoDiscordIDWrappers.mjs b/modules/mongoDiscordIDWrappers.mjs new file mode 100644 index 0000000..1249a62 --- /dev/null +++ b/modules/mongoDiscordIDWrappers.mjs @@ -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(); + } +}; \ No newline at end of file diff --git a/modules/mongoHandler.mjs b/modules/mongoHandler.mjs new file mode 100644 index 0000000..de5be26 --- /dev/null +++ b/modules/mongoHandler.mjs @@ -0,0 +1,53 @@ +// Import necessary modules +import { MongoClient } from 'mongodb'; + +import dotenv from 'dotenv'; +dotenv.config() + +// MongoDB connection URI +const uri = process.env.MONGO_URL; + +// Function to connect to the database +export const connectToDatabase = async () => { + try { + const client = await MongoClient.connect(uri); + return client; + } catch (error) { + console.error('Error connecting to the database:', error); + throw error; + } +}; + +// Function to insert a document into the collection +export const insertDocument = async (collectionName, document) => { + const db = await connectToDatabase(); + try { + const collection = db.db().collection(collectionName); + const result = await collection.insertOne(document); + console.log('Document inserted:', result.insertedId); + return result.insertedId; + } catch (error) { + console.error('Error inserting document:', error); + throw error; + } finally { + // Close the connection + await db.close(); + } +}; + +// Function to retrieve documents from the collection +export const getDocuments = async (collectionName) => { + const db = await connectToDatabase(); + try { + const collection = db.db().collection(collectionName); + const documents = await collection.find({}).toArray(); + console.log('Documents retrieved:', documents); + return documents; + } catch (error) { + console.error('Error retrieving documents:', error); + throw error; + } finally { + // Close the connection + await db.close(); + } +}; diff --git a/modules/mongoNodesWrappers.mjs b/modules/mongoNodesWrappers.mjs new file mode 100644 index 0000000..f12fbbf --- /dev/null +++ b/modules/mongoNodesWrappers.mjs @@ -0,0 +1,75 @@ +import { insertDocument, getDocuments, connectToDatabase } from "./mongoHandler.mjs"; + +const collectionName = 'nodes'; + +// Wrapper for inserting a node +export const createNode = async (node) => { + try { + const insertedId = await insertDocument(collectionName, node); + return insertedId; + } catch (error) { + console.error('Error creating node:', error); + throw error; + } +}; + +// Wrapper for retrieving all nodes +export const getAllNodes = async () => { + try { + const nodes = await getDocuments(collectionName); + return nodes; + } catch (error) { + console.error('Error getting all nodes:', error); + throw error; + } +}; + +// Wrapper for retrieving a node by NUID +export const getNodeByNuid = async (nuid) => { + const db = await connectToDatabase(); + try { + const collection = db.db().collection(collectionName); + const node = await collection.findOne({ nuid }); + return node; + } catch (error) { + console.error('Error getting node by NUID:', error); + throw error; + } finally { + // Close the connection + await db.close(); + } +}; + +// Wrapper for updating a node by NUID +export const updateNodeByNuid = async (nuid, updatedFields) => { + const db = await connectToDatabase(); + try { + const collection = db.db().collection(collectionName); + const result = await collection.updateOne({ nuid }, { $set: updatedFields }); + console.log('Node updated:', result.modifiedCount); + return result.modifiedCount; + } catch (error) { + console.error('Error updating node by NUID:', error); + throw error; + } finally { + // Close the connection + await db.close(); + } +}; + +// Wrapper for deleting a node by NUID +export const deleteNodeByNuid = async (nuid) => { + const db = await connectToDatabase(); + try { + const collection = db.db().collection(collectionName); + const result = await collection.deleteOne({ nuid }); + console.log('Node deleted:', result.deletedCount); + return result.deletedCount; + } catch (error) { + console.error('Error deleting node by NUID:', error); + throw error; + } finally { + // Close the connection + await db.close(); + } +}; \ No newline at end of file diff --git a/modules/mongoSystemsWrappers.mjs b/modules/mongoSystemsWrappers.mjs new file mode 100644 index 0000000..f353073 --- /dev/null +++ b/modules/mongoSystemsWrappers.mjs @@ -0,0 +1,111 @@ +import { insertDocument, getDocuments, connectToDatabase } from "./mongoHandler.mjs"; + +const collectionName = 'radio-systems'; + +// Local wrapper to remove any local files from radio systems +const removeLocalFilesFromsystem = async (system) => { + if (system.trunkFile) delete system.trunkFile; + if (system.whitelistFile) delete system.whitelistFile; +} + + +// Wrapper for inserting a system +export const createSystem = async (name, system, nuid) => { + try { + // Remove any local files + await removeLocalFilesFromsystem(system); + // Add the NUID of the node that created this system + system.nodes = [nuid]; + // Add the name of the system + system.name = name + const insertedId = await insertDocument(collectionName, system); + return insertedId; + } catch (error) { + console.error('Error creating system:', error); + throw error; + } +}; + +// Wrapper for retrieving all systems +export const getAllSystems = async () => { + try { + const systems = await getDocuments(collectionName); + return systems; + } catch (error) { + console.error('Error getting all systems:', error); + throw error; + } +}; + +// Wrapper for retrieving a system by name +export const getSystemByName = async (name) => { + const db = await connectToDatabase(); + try { + const collection = db.db().collection(collectionName); + const system = await collection.findOne({ name }); + return system; + } catch (error) { + console.error('Error getting system by name:', error); + throw error; + } finally { + // Close the connection + await db.close(); + } +}; + +// Wrapper to get all systems from a given node +export const getSystemsByNuid = async (nuid) => { + const db = await connectToDatabase(); + try { + const collection = db.db().collection(collectionName); + + // Query for documents where the 'nodes' array contains the given nodeID + const query = { nodes: nuid }; + const systems = await collection.find(query).toArray(); + + return systems; + } catch (error) { + console.error('Error finding entries:', error); + throw error; + } finally { + // Close the connection + await db.close(); + } +}; + +// Wrapper for updating a system by name +export const updateSystemByName = async (name, updatedSystem) => { + // Remove any local files + await removeLocalFilesFromsystem(updatedSystem); + + const db = await connectToDatabase(); + try { + const collection = db.db().collection(collectionName); + const result = await collection.updateOne({ name }, { $set: updatedSystem }); + console.log('System updated:', result.modifiedCount); + return result.modifiedCount; + } catch (error) { + console.error('Error updating system by name:', error); + throw error; + } finally { + // Close the connection + await db.close(); + } +}; + +// Wrapper for deleting a system by name +export const deleteSystemByName = async (name) => { + const db = await connectToDatabase(); + try { + const collection = db.db().collection(collectionName); + const result = await collection.deleteOne({ name }); + console.log('System deleted:', result.deletedCount); + return result.deletedCount; + } catch (error) { + console.error('Error deleting system by name:', error); + throw error; + } finally { + // Close the connection + await db.close(); + } +}; \ No newline at end of file diff --git a/modules/socketServer.mjs b/modules/socketServer.mjs new file mode 100644 index 0000000..848ad9f --- /dev/null +++ b/modules/socketServer.mjs @@ -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('

Hello world

'); +}); + +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); + }); + +}); \ No newline at end of file diff --git a/modules/socketServerWrappers.mjs b/modules/socketServerWrappers.mjs new file mode 100644 index 0000000..799be18 --- /dev/null +++ b/modules/socketServerWrappers.mjs @@ -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); + } + }); +} \ No newline at end of file diff --git a/package-lock.json b/package-lock.json new file mode 100644 index 0000000..f8609bb --- /dev/null +++ b/package-lock.json @@ -0,0 +1,2316 @@ +{ + "name": "drb-server", + "version": "3.0.0", + "lockfileVersion": 3, + "requires": true, + "packages": { + "": { + "name": "drb-server", + "version": "3.0.0", + "license": "ISC", + "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" + }, + "devDependencies": { + "chai": "^5.1.0", + "mocha": "^10.4.0", + "socket.io-client": "^4.7.5" + } + }, + "node_modules/@discordjs/builders": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.7.0.tgz", + "integrity": "sha512-GDtbKMkg433cOZur8Dv6c25EHxduNIBsxeHrsRoIM8+AwmEZ8r0tEpckx/sHwTLwQPOF3e2JWloZh9ofCaMfAw==", + "dependencies": { + "@discordjs/formatters": "^0.3.3", + "@discordjs/util": "^1.0.2", + "@sapphire/shapeshift": "^3.9.3", + "discord-api-types": "0.37.61", + "fast-deep-equal": "^3.1.3", + "ts-mixer": "^6.0.3", + "tslib": "^2.6.2" + }, + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/collection": { + "version": "1.5.3", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz", + "integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/formatters": { + "version": "0.3.3", + "resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.3.3.tgz", + "integrity": "sha512-wTcI1Q5cps1eSGhl6+6AzzZkBBlVrBdc9IUhJbijRgVjCNIIIZPgqnUj3ntFODsHrdbGU8BEG9XmDQmgEEYn3w==", + "dependencies": { + "discord-api-types": "0.37.61" + }, + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/rest": { + "version": "2.2.0", + "resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.2.0.tgz", + "integrity": "sha512-nXm9wT8oqrYFRMEqTXQx9DUTeEtXUDMmnUKIhZn6O2EeDY9VCdwj23XCPq7fkqMPKdF7ldAfeVKyxxFdbZl59A==", + "dependencies": { + "@discordjs/collection": "^2.0.0", + "@discordjs/util": "^1.0.2", + "@sapphire/async-queue": "^1.5.0", + "@sapphire/snowflake": "^3.5.1", + "@vladfrangu/async_event_emitter": "^2.2.2", + "discord-api-types": "0.37.61", + "magic-bytes.js": "^1.5.0", + "tslib": "^2.6.2", + "undici": "5.27.2" + }, + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/rest/node_modules/@discordjs/collection": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.0.0.tgz", + "integrity": "sha512-YTWIXLrf5FsrLMycpMM9Q6vnZoR/lN2AWX23/Cuo8uOOtS8eHB2dyQaaGnaF8aZPYnttf2bkLMcXn/j6JUOi3w==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@discordjs/util": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.0.2.tgz", + "integrity": "sha512-IRNbimrmfb75GMNEjyznqM1tkI7HrZOf14njX7tCAAUetyZM1Pr8hX/EK2lxBCOgWDRmigbp24fD1hdMfQK5lw==", + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/ws": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.0.2.tgz", + "integrity": "sha512-+XI82Rm2hKnFwAySXEep4A7Kfoowt6weO6381jgW+wVdTpMS/56qCvoXyFRY0slcv7c/U8My2PwIB2/wEaAh7Q==", + "dependencies": { + "@discordjs/collection": "^2.0.0", + "@discordjs/rest": "^2.1.0", + "@discordjs/util": "^1.0.2", + "@sapphire/async-queue": "^1.5.0", + "@types/ws": "^8.5.9", + "@vladfrangu/async_event_emitter": "^2.2.2", + "discord-api-types": "0.37.61", + "tslib": "^2.6.2", + "ws": "^8.14.2" + }, + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/@discordjs/ws/node_modules/@discordjs/collection": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.0.0.tgz", + "integrity": "sha512-YTWIXLrf5FsrLMycpMM9Q6vnZoR/lN2AWX23/Cuo8uOOtS8eHB2dyQaaGnaF8aZPYnttf2bkLMcXn/j6JUOi3w==", + "engines": { + "node": ">=18" + } + }, + "node_modules/@discordjs/ws/node_modules/ws": { + "version": "8.16.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.16.0.tgz", + "integrity": "sha512-HS0c//TP7Ina87TfiPUz1rQzMhHrl/SG2guqRcTOIUYD2q8uhUdNHZYJUaQ8aTGPzCh+c6oawMKW35nFl1dxyQ==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/@fastify/busboy": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/@fastify/busboy/-/busboy-2.1.0.tgz", + "integrity": "sha512-+KpH+QxZU7O4675t3mnkQKcZZg56u+K/Ct2K+N2AZYNVK8kyeo/bI18tI8aPm3tvNNRyTWfj6s5tnGNlcbQRsA==", + "engines": { + "node": ">=14" + } + }, + "node_modules/@mongodb-js/saslprep": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/@mongodb-js/saslprep/-/saslprep-1.1.4.tgz", + "integrity": "sha512-8zJ8N1x51xo9hwPh6AWnKdLGEC5N3lDa6kms1YHmFBoRhTpJR6HG8wWk0td1MVCu9cD4YBrvjZEtd5Obw0Fbnw==", + "dependencies": { + "sparse-bitfield": "^3.0.3" + } + }, + "node_modules/@sapphire/async-queue": { + "version": "1.5.1", + "resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.1.tgz", + "integrity": "sha512-1RdpsmDQR/aWfp8oJzPtn4dNQrbpqSL5PIA0uAB/XwerPXUf994Ug1au1e7uGcD7ei8/F63UDjr5GWps1g/HxQ==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@sapphire/shapeshift": { + "version": "3.9.5", + "resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-3.9.5.tgz", + "integrity": "sha512-AGdHe+51gF7D3W8hBfuSFLBocURDCXVQczScTHXDS3RpNjNgrktIx/amlz5y8nHhm8SAdFt/X8EF8ZSfjJ0tnA==", + "dependencies": { + "fast-deep-equal": "^3.1.3", + "lodash": "^4.17.21" + }, + "engines": { + "node": ">=v18" + } + }, + "node_modules/@sapphire/snowflake": { + "version": "3.5.1", + "resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.1.tgz", + "integrity": "sha512-BxcYGzgEsdlG0dKAyOm0ehLGm2CafIrfQTZGWgkfKYbj+pNNsorZ7EotuZukc2MT70E0UbppVbtpBrqpzVzjNA==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/@socket.io/component-emitter": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/@socket.io/component-emitter/-/component-emitter-3.1.0.tgz", + "integrity": "sha512-+9jVqKhRSpsc591z5vX+X5Yyw+he/HCB4iQ/RYxw35CEPaY1gnsNE43nf9n9AaYjAQrTiI/mOwKUKdUs9vf7Xg==" + }, + "node_modules/@types/cookie": { + "version": "0.4.1", + "resolved": "https://registry.npmjs.org/@types/cookie/-/cookie-0.4.1.tgz", + "integrity": "sha512-XW/Aa8APYr6jSVVA1y/DEIZX0/GMKLEVekNG727R8cs56ahETkRAy/3DR7+fJyh7oUgGwNQaRfXCun0+KbWY7Q==" + }, + "node_modules/@types/cors": { + "version": "2.8.17", + "resolved": "https://registry.npmjs.org/@types/cors/-/cors-2.8.17.tgz", + "integrity": "sha512-8CGDvrBj1zgo2qE+oS3pOCyYNqCPryMWY2bGfwA0dcfopWGgxs+78df0Rs3rc9THP4JkOhLsAa+15VdpAqkcUA==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@types/node": { + "version": "20.10.6", + "resolved": "https://registry.npmjs.org/@types/node/-/node-20.10.6.tgz", + "integrity": "sha512-Vac8H+NlRNNlAmDfGUP7b5h/KA+AtWIzuXy0E6OyP8f1tCLYAtPvKRRDJjAPqhpCb0t6U2j7/xqAuLEebW2kiw==", + "dependencies": { + "undici-types": "~5.26.4" + } + }, + "node_modules/@types/webidl-conversions": { + "version": "7.0.3", + "resolved": "https://registry.npmjs.org/@types/webidl-conversions/-/webidl-conversions-7.0.3.tgz", + "integrity": "sha512-CiJJvcRtIgzadHCYXw7dqEnMNRjhGZlYK05Mj9OyktqV8uVT8fD2BFOB7S1uwBE3Kj2Z+4UyPmFw/Ixgw/LAlA==" + }, + "node_modules/@types/whatwg-url": { + "version": "11.0.4", + "resolved": "https://registry.npmjs.org/@types/whatwg-url/-/whatwg-url-11.0.4.tgz", + "integrity": "sha512-lXCmTWSHJvf0TRSO58nm978b8HJ/EdsSsEKLd3ODHFjo+3VGAyyTp4v50nWvwtzBxSMQrVOK7tcuN0zGPLICMw==", + "dependencies": { + "@types/webidl-conversions": "*" + } + }, + "node_modules/@types/ws": { + "version": "8.5.9", + "resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.5.9.tgz", + "integrity": "sha512-jbdrY0a8lxfdTp/+r7Z4CkycbOFN8WX+IOchLJr3juT/xzbJ8URyTVSJ/hvNdadTgM1mnedb47n+Y31GsFnQlg==", + "dependencies": { + "@types/node": "*" + } + }, + "node_modules/@vladfrangu/async_event_emitter": { + "version": "2.2.4", + "resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.2.4.tgz", + "integrity": "sha512-ButUPz9E9cXMLgvAW8aLAKKJJsPu1dY1/l/E8xzLFuysowXygs6GBcyunK9rnGC4zTsnIc2mQo71rGw9U+Ykug==", + "engines": { + "node": ">=v14.0.0", + "npm": ">=7.0.0" + } + }, + "node_modules/accepts": { + "version": "1.3.8", + "resolved": "https://registry.npmjs.org/accepts/-/accepts-1.3.8.tgz", + "integrity": "sha512-PYAthTa2m2VKxuvSD3DPC/Gy+U+sOA1LAuT8mkmRuvw+NACSaeXEQ+NHcVF7rONl6qcaxV3Uuemwawk+7+SJLw==", + "dependencies": { + "mime-types": "~2.1.34", + "negotiator": "0.6.3" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/ansi-colors": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/ansi-colors/-/ansi-colors-4.1.1.tgz", + "integrity": "sha512-JoX0apGbHaUJBNl6yF+p6JAFYZ666/hhCGKN5t9QFjbJQKUU/g8MNbFDbvfrgKXvI1QpZplPOnwIo99lX/AAmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/ansi-regex": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/ansi-regex/-/ansi-regex-5.0.1.tgz", + "integrity": "sha512-quJQXlTSUGL2LH9SUXo8VwsY4soanhgo6LNSm84E1LBcE8s3O0wpdiRzyR9z/ZZJMlMWv37qOOb9pdJlMUEKFQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/ansi-styles": { + "version": "4.3.0", + "resolved": "https://registry.npmjs.org/ansi-styles/-/ansi-styles-4.3.0.tgz", + "integrity": "sha512-zbB9rCJAT1rbjiVDb2hqKFHNYLxgtk8NURxZ3IZwD3F6NtxbXZQCnnSi1Lkx+IDohdPlFp222wVALIheZJQSEg==", + "dev": true, + "dependencies": { + "color-convert": "^2.0.1" + }, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/chalk/ansi-styles?sponsor=1" + } + }, + "node_modules/anymatch": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/anymatch/-/anymatch-3.1.3.tgz", + "integrity": "sha512-KMReFUr0B4t+D+OBkjR3KYqvocp2XaSzO55UcB6mgQMd3KbcE+mWTyvVV7D/zsdEbNnV6acZUutkiHQXvTr1Rw==", + "dev": true, + "dependencies": { + "normalize-path": "^3.0.0", + "picomatch": "^2.0.4" + }, + "engines": { + "node": ">= 8" + } + }, + "node_modules/argparse": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/argparse/-/argparse-2.0.1.tgz", + "integrity": "sha512-8+9WqebbFzpX9OR+Wa6O29asIogeRMzcGtAINdpMHHyAg10f05aSFVBbcEqGf/PXw1EjAZ+q2/bEBg3DvurK3Q==", + "dev": true + }, + "node_modules/array-flatten": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/array-flatten/-/array-flatten-1.1.1.tgz", + "integrity": "sha512-PCVAQswWemu6UdxsDFFX/+gVeYqKAod3D3UVm91jHwynguOwAvYPhx8nNlM++NqRcK6CxxpUafjmhIdKiHibqg==" + }, + "node_modules/assertion-error": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/assertion-error/-/assertion-error-2.0.1.tgz", + "integrity": "sha512-Izi8RQcffqCeNVgFigKli1ssklIbpHnCYc6AknXGYoB6grJqyeby7jv12JUQgmTAnIDnbck1uxksT4dzN3PWBA==", + "dev": true, + "engines": { + "node": ">=12" + } + }, + "node_modules/balanced-match": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/balanced-match/-/balanced-match-1.0.2.tgz", + "integrity": "sha512-3oSeUO0TMV67hN1AmbXsK4yaqU7tjiHlbxRDZOpH0KW9+CeX4bRAaX0Anxt0tx2MrpRpWwQaPwIlISEJhYU5Pw==", + "dev": true + }, + "node_modules/base64id": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/base64id/-/base64id-2.0.0.tgz", + "integrity": "sha512-lGe34o6EHj9y3Kts9R4ZYs/Gr+6N7MCaMlIFA3F1R2O5/m7K06AxfSeO5530PEERE6/WyEg3lsuyw4GHlPZHog==", + "engines": { + "node": "^4.5.0 || >= 5.9" + } + }, + "node_modules/basic-auth": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/basic-auth/-/basic-auth-2.0.1.tgz", + "integrity": "sha512-NF+epuEdnUYVlGuhaxbbq+dvJttwLnGY+YixlXlME5KpQ5W3CnXA5cVTneY3SPbPDRkcjMbifrwmFYcClgOZeg==", + "dependencies": { + "safe-buffer": "5.1.2" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/basic-auth/node_modules/safe-buffer": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz", + "integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g==" + }, + "node_modules/binary-extensions": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/binary-extensions/-/binary-extensions-2.3.0.tgz", + "integrity": "sha512-Ceh+7ox5qe7LJuLHoY0feh3pHuUDHAcRUeyL2VYghZwfpkNIy/+8Ocg0a3UuSoYzavmylwuLWQOf3hl0jjMMIw==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/body-parser": { + "version": "1.20.1", + "resolved": "https://registry.npmjs.org/body-parser/-/body-parser-1.20.1.tgz", + "integrity": "sha512-jWi7abTbYwajOytWCQc37VulmWiRae5RyTpaCyDcS5/lMdtwSz5lOpDE67srw/HYe35f1z3fDQw+3txg7gNtWw==", + "dependencies": { + "bytes": "3.1.2", + "content-type": "~1.0.4", + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "on-finished": "2.4.1", + "qs": "6.11.0", + "raw-body": "2.5.1", + "type-is": "~1.6.18", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/brace-expansion": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/brace-expansion/-/brace-expansion-2.0.1.tgz", + "integrity": "sha512-XnAIvQ8eM+kC6aULx6wuQiwVsnzsi9d3WxzV3FpWTGA19F621kwdbsAcFKXgKUHZWsy+mY6iL1sHTxWEFCytDA==", + "dev": true, + "dependencies": { + "balanced-match": "^1.0.0" + } + }, + "node_modules/braces": { + "version": "3.0.2", + "resolved": "https://registry.npmjs.org/braces/-/braces-3.0.2.tgz", + "integrity": "sha512-b8um+L1RzM3WDSzvhm6gIz1yfTbBt6YTlcEKAvsmqCZZFw46z626lVj9j1yEPW33H5H+lBQpZMP1k8l+78Ha0A==", + "dev": true, + "dependencies": { + "fill-range": "^7.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/browser-stdout": { + "version": "1.3.1", + "resolved": "https://registry.npmjs.org/browser-stdout/-/browser-stdout-1.3.1.tgz", + "integrity": "sha512-qhAVI1+Av2X7qelOfAIYwXONood6XlZE/fXaBSmW/T5SzLAmCgzi+eiWE7fUvbHaeNBQH13UftjpXxsfLkMpgw==", + "dev": true + }, + "node_modules/bson": { + "version": "6.2.0", + "resolved": "https://registry.npmjs.org/bson/-/bson-6.2.0.tgz", + "integrity": "sha512-ID1cI+7bazPDyL9wYy9GaQ8gEEohWvcUl/Yf0dIdutJxnmInEEyCsb4awy/OiBfall7zBA179Pahi3vCdFze3Q==", + "engines": { + "node": ">=16.20.1" + } + }, + "node_modules/bytes": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/bytes/-/bytes-3.1.2.tgz", + "integrity": "sha512-/Nf7TyzTx6S3yRJObOAV7956r8cr2+Oj8AC5dt8wSP3BQAoeX58NoHyCU8P8zGkNXStjTSi6fzO6F0pBdcYbEg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/call-bind": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/call-bind/-/call-bind-1.0.5.tgz", + "integrity": "sha512-C3nQxfFZxFRVoJoGKKI8y3MOEo129NQ+FgQ08iye+Mk4zNZZGdjfs06bVTr+DBSlA66Q2VEcMki/cUCP4SercQ==", + "dependencies": { + "function-bind": "^1.1.2", + "get-intrinsic": "^1.2.1", + "set-function-length": "^1.1.1" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/camelcase": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/camelcase/-/camelcase-6.3.0.tgz", + "integrity": "sha512-Gmy6FhYlCY7uOElZUSbxo2UCDH8owEk996gkbrpsgGtrJLM3J7jGxl9Ic7Qwwj4ivOE5AWZWRMecDdF7hqGjFA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/chai": { + "version": "5.1.0", + "resolved": "https://registry.npmjs.org/chai/-/chai-5.1.0.tgz", + "integrity": "sha512-kDZ7MZyM6Q1DhR9jy7dalKohXQ2yrlXkk59CR52aRKxJrobmlBNqnFQxX9xOX8w+4mz8SYlKJa/7D7ddltFXCw==", + "dev": true, + "dependencies": { + "assertion-error": "^2.0.1", + "check-error": "^2.0.0", + "deep-eql": "^5.0.1", + "loupe": "^3.1.0", + "pathval": "^2.0.0" + }, + "engines": { + "node": ">=12" + } + }, + "node_modules/chalk": { + "version": "4.1.2", + "resolved": "https://registry.npmjs.org/chalk/-/chalk-4.1.2.tgz", + "integrity": "sha512-oKnbhFyRIXpUuez8iBMmyEa4nbj4IOQyuhc/wy9kY7/WVPcwIO9VA668Pu8RkO7+0G76SLROeyw9CpQ061i4mA==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.1.0", + "supports-color": "^7.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/chalk?sponsor=1" + } + }, + "node_modules/chalk/node_modules/supports-color": { + "version": "7.2.0", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-7.2.0.tgz", + "integrity": "sha512-qpCAvRl9stuOHveKsn7HncJRvv501qIacKzQlO/+Lwxc9+0q2wLyv4Dfvt80/DPn2pqOBsJdDiogXGR9+OvwRw==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/check-error": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/check-error/-/check-error-2.0.0.tgz", + "integrity": "sha512-tjLAOBHKVxtPoHe/SA7kNOMvhCRdCJ3vETdeY0RuAc9popf+hyaSV6ZEg9hr4cpWF7jmo/JSWEnLDrnijS9Tog==", + "dev": true, + "engines": { + "node": ">= 16" + } + }, + "node_modules/chokidar": { + "version": "3.5.3", + "resolved": "https://registry.npmjs.org/chokidar/-/chokidar-3.5.3.tgz", + "integrity": "sha512-Dr3sfKRP6oTcjf2JmUmFJfeVMvXBdegxB0iVQ5eb2V10uFJUCAS8OByZdVAyVb8xXNz3GjjTgj9kLWsZTqE6kw==", + "dev": true, + "funding": [ + { + "type": "individual", + "url": "https://paulmillr.com/funding/" + } + ], + "dependencies": { + "anymatch": "~3.1.2", + "braces": "~3.0.2", + "glob-parent": "~5.1.2", + "is-binary-path": "~2.1.0", + "is-glob": "~4.0.1", + "normalize-path": "~3.0.0", + "readdirp": "~3.6.0" + }, + "engines": { + "node": ">= 8.10.0" + }, + "optionalDependencies": { + "fsevents": "~2.3.2" + } + }, + "node_modules/cliui": { + "version": "7.0.4", + "resolved": "https://registry.npmjs.org/cliui/-/cliui-7.0.4.tgz", + "integrity": "sha512-OcRE68cOsVMXp1Yvonl/fzkQOyjLSu/8bhPDfQt0e0/Eb283TKP20Fs2MqoPsr9SwA595rRCA+QMzYc9nBP+JQ==", + "dev": true, + "dependencies": { + "string-width": "^4.2.0", + "strip-ansi": "^6.0.0", + "wrap-ansi": "^7.0.0" + } + }, + "node_modules/color-convert": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/color-convert/-/color-convert-2.0.1.tgz", + "integrity": "sha512-RRECPsj7iu/xb5oKYcsFHSppFNnsj/52OVTRKb4zP5onXwVF3zVmmToNcOfGC+CRDpfK/U584fMg38ZHCaElKQ==", + "dev": true, + "dependencies": { + "color-name": "~1.1.4" + }, + "engines": { + "node": ">=7.0.0" + } + }, + "node_modules/color-name": { + "version": "1.1.4", + "resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz", + "integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA==", + "dev": true + }, + "node_modules/content-disposition": { + "version": "0.5.4", + "resolved": "https://registry.npmjs.org/content-disposition/-/content-disposition-0.5.4.tgz", + "integrity": "sha512-FveZTNuGw04cxlAiWbzi6zTAL/lhehaWbTtgluJh4/E95DqMwTmha3KZN1aAWA8cFIhHzMZUvLevkw5Rqk+tSQ==", + "dependencies": { + "safe-buffer": "5.2.1" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/content-type": { + "version": "1.0.5", + "resolved": "https://registry.npmjs.org/content-type/-/content-type-1.0.5.tgz", + "integrity": "sha512-nTjqfcBFEipKdXCv4YDQWCfmcLZKm81ldF0pAopTvyrFGVbcR6P/VAAd5G7N+0tTr8QqiU0tFadD6FK4NtJwOA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie": { + "version": "0.5.0", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.5.0.tgz", + "integrity": "sha512-YZ3GUyn/o8gfKJlnlX7g7xq4gyO6OSuhGPKaaGssGB2qgDUS0gPgtTvoyZLTt9Ab6dC4hfc9dV5arkvc/OCmrw==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/cookie-signature": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz", + "integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ==" + }, + "node_modules/cors": { + "version": "2.8.5", + "resolved": "https://registry.npmjs.org/cors/-/cors-2.8.5.tgz", + "integrity": "sha512-KIHbLJqu73RGr/hnbrO9uBeixNGuvSQjul/jdFvS/KFSIH1hWVd1ng7zOHx+YrEfInLG7q4n6GHQ9cDtxv/P6g==", + "dependencies": { + "object-assign": "^4", + "vary": "^1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/debug": { + "version": "2.6.9", + "resolved": "https://registry.npmjs.org/debug/-/debug-2.6.9.tgz", + "integrity": "sha512-bC7ElrdJaJnPbAP+1EotYvqZsb3ecl5wi6Bfi6BJTUcNowp6cvspg0jXznRTKDjm/E7AdgFBVeAPVMNcKGsHMA==", + "dependencies": { + "ms": "2.0.0" + } + }, + "node_modules/decamelize": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/decamelize/-/decamelize-4.0.0.tgz", + "integrity": "sha512-9iE1PgSik9HeIIw2JO94IidnE3eBoQrFJ3w7sFuzSX4DpmZ3v5sZpUiV5Swcf6mQEF+Y0ru8Neo+p+nyh2J+hQ==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/deep-eql": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/deep-eql/-/deep-eql-5.0.1.tgz", + "integrity": "sha512-nwQCf6ne2gez3o1MxWifqkciwt0zhl0LO1/UwVu4uMBuPmflWM4oQ70XMqHqnBJA+nhzncaqL9HVL6KkHJ28lw==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/define-data-property": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/define-data-property/-/define-data-property-1.1.1.tgz", + "integrity": "sha512-E7uGkTzkk1d0ByLeSc6ZsFS79Axg+m1P/VsgYsxHgiuc3tFSj+MjMIwe90FC4lOAZzNBdY7kkO2P2wKdsQ1vgQ==", + "dependencies": { + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/depd": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz", + "integrity": "sha512-g7nH6P6dyDioJogAAGprGpCtVImJhpPk/roCzdb3fIh61/s/nPsfR6onyMwkCAR/OlC3yBC0lESvUoQEAssIrw==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/destroy": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/destroy/-/destroy-1.2.0.tgz", + "integrity": "sha512-2sJGJTaXIIaR1w4iJSNoN0hnMY7Gpc/n8D4qSCJw8QqFWXf7cuAgnEHxBpweaVcPevC2l3KpjYCx3NypQQgaJg==", + "engines": { + "node": ">= 0.8", + "npm": "1.2.8000 || >= 1.4.16" + } + }, + "node_modules/diff": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/diff/-/diff-5.0.0.tgz", + "integrity": "sha512-/VTCrvm5Z0JGty/BWHljh+BAiw3IK+2j87NGMu8Nwc/f48WoDAC395uomO9ZD117ZOBaHmkX1oyLvkVM/aIT3w==", + "dev": true, + "engines": { + "node": ">=0.3.1" + } + }, + "node_modules/discord-api-types": { + "version": "0.37.61", + "resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.37.61.tgz", + "integrity": "sha512-o/dXNFfhBpYHpQFdT6FWzeO7pKc838QeeZ9d91CfVAtpr5XLK4B/zYxQbYgPdoMiTDvJfzcsLW5naXgmHGDNXw==" + }, + "node_modules/discord.js": { + "version": "14.14.1", + "resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.14.1.tgz", + "integrity": "sha512-/hUVzkIerxKHyRKopJy5xejp4MYKDPTszAnpYxzVVv4qJYf+Tkt+jnT2N29PIPschicaEEpXwF2ARrTYHYwQ5w==", + "dependencies": { + "@discordjs/builders": "^1.7.0", + "@discordjs/collection": "1.5.3", + "@discordjs/formatters": "^0.3.3", + "@discordjs/rest": "^2.1.0", + "@discordjs/util": "^1.0.2", + "@discordjs/ws": "^1.0.2", + "@sapphire/snowflake": "3.5.1", + "@types/ws": "8.5.9", + "discord-api-types": "0.37.61", + "fast-deep-equal": "3.1.3", + "lodash.snakecase": "4.1.1", + "tslib": "2.6.2", + "undici": "5.27.2", + "ws": "8.14.2" + }, + "engines": { + "node": ">=16.11.0" + } + }, + "node_modules/discord.js/node_modules/ws": { + "version": "8.14.2", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.14.2.tgz", + "integrity": "sha512-wEBG1ftX4jcglPxgFCMJmZ2PLtSbJ2Peg6TmpJFTbe9GZYOQCDPdMYu/Tm0/bGZkw8paZnJY45J4K2PZrLYq8g==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": ">=5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/dotenv": { + "version": "16.3.1", + "resolved": "https://registry.npmjs.org/dotenv/-/dotenv-16.3.1.tgz", + "integrity": "sha512-IPzF4w4/Rd94bA9imS68tZBaYyBWSCE47V1RGuMrB94iyTOIEwRmVL2x/4An+6mETpLrKJ5hQkB8W4kFAadeIQ==", + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/motdotla/dotenv?sponsor=1" + } + }, + "node_modules/ee-first": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/ee-first/-/ee-first-1.1.1.tgz", + "integrity": "sha512-WMwm9LhRUo+WUaRN+vRuETqG89IgZphVSNkdFgeb6sS/E4OrDIN7t48CAewSHXc6C8lefD8KKfr5vY61brQlow==" + }, + "node_modules/emoji-regex": { + "version": "8.0.0", + "resolved": "https://registry.npmjs.org/emoji-regex/-/emoji-regex-8.0.0.tgz", + "integrity": "sha512-MSjYzcWNOA0ewAHpz0MxpYFvwg6yjy1NG3xteoqz644VCo/RPgnr1/GGt+ic3iJTzQ8Eu3TdM14SawnVUmGE6A==", + "dev": true + }, + "node_modules/encodeurl": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/encodeurl/-/encodeurl-1.0.2.tgz", + "integrity": "sha512-TPJXq8JqFaVYm2CWmPvnP2Iyo4ZSM7/QKcSmuMLDObfpH5fi7RUGmd/rTDf+rut/saiDiQEeVTNgAmJEdAOx0w==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/engine.io": { + "version": "6.5.4", + "resolved": "https://registry.npmjs.org/engine.io/-/engine.io-6.5.4.tgz", + "integrity": "sha512-KdVSDKhVKyOi+r5uEabrDLZw2qXStVvCsEB/LN3mw4WFi6Gx50jTyuxYVCwAAC0U46FdnzP/ScKRBTXb/NiEOg==", + "dependencies": { + "@types/cookie": "^0.4.1", + "@types/cors": "^2.8.12", + "@types/node": ">=10.0.0", + "accepts": "~1.3.4", + "base64id": "2.0.0", + "cookie": "~0.4.1", + "cors": "~2.8.5", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/engine.io-client": { + "version": "6.5.3", + "resolved": "https://registry.npmjs.org/engine.io-client/-/engine.io-client-6.5.3.tgz", + "integrity": "sha512-9Z0qLB0NIisTRt1DZ/8U2k12RJn8yls/nXMZLn+/N8hANT3TcYjKFKcwbw5zFQiN4NTde3TSY9zb79e1ij6j9Q==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1", + "engine.io-parser": "~5.2.1", + "ws": "~8.11.0", + "xmlhttprequest-ssl": "~2.0.0" + } + }, + "node_modules/engine.io-client/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io-client/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/engine.io-parser": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/engine.io-parser/-/engine.io-parser-5.2.1.tgz", + "integrity": "sha512-9JktcM3u18nU9N2Lz3bWeBgxVgOKpw7yhRaoxQA3FUDZzzw+9WlA6p4G4u0RixNkg14fH7EfEc/RhpurtiROTQ==", + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/engine.io/node_modules/cookie": { + "version": "0.4.2", + "resolved": "https://registry.npmjs.org/cookie/-/cookie-0.4.2.tgz", + "integrity": "sha512-aSWTXFzaKWkvHO1Ny/s+ePFpvKsPnjc551iI41v3ny/ow6tBG5Vd+FuqGNhh1LxOmVzOlGUriIlOaokOvhaStA==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/engine.io/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/engine.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/escalade": { + "version": "3.1.2", + "resolved": "https://registry.npmjs.org/escalade/-/escalade-3.1.2.tgz", + "integrity": "sha512-ErCHMCae19vR8vQGe50xIsVomy19rg6gFu3+r3jkEO46suLMWBksvVyoGgQV+jOfl84ZSOSlmv6Gxa89PmTGmA==", + "dev": true, + "engines": { + "node": ">=6" + } + }, + "node_modules/escape-html": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/escape-html/-/escape-html-1.0.3.tgz", + "integrity": "sha512-NiSupZ4OeuGwr68lGIeym/ksIZMJodUGOSCZ/FSnTxcrekbvqrgdUxlJOMpijaKZVjAJrWrGs/6Jy8OMuyj9ow==" + }, + "node_modules/escape-string-regexp": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/escape-string-regexp/-/escape-string-regexp-4.0.0.tgz", + "integrity": "sha512-TtpcNJ3XAzx3Gq8sWRzJaVajRs0uVxA2YAkdb1jm2YkPz4G6egUFAyA3n5vtEIZefPk5Wa4UXbKuS5fKkJWdgA==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/etag": { + "version": "1.8.1", + "resolved": "https://registry.npmjs.org/etag/-/etag-1.8.1.tgz", + "integrity": "sha512-aIL5Fx7mawVa300al2BnEE4iNvo1qETxLrPI/o05L7z6go7fCw1J6EQmbK4FmJ2AS7kgVF/KEZWufBfdClMcPg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/express": { + "version": "4.18.2", + "resolved": "https://registry.npmjs.org/express/-/express-4.18.2.tgz", + "integrity": "sha512-5/PsL6iGPdfQ/lKM1UuielYgv3BUoJfz1aUwU9vHZ+J7gyvwdQXFEBIEIaxeGf0GIcreATNyBExtalisDbuMqQ==", + "dependencies": { + "accepts": "~1.3.8", + "array-flatten": "1.1.1", + "body-parser": "1.20.1", + "content-disposition": "0.5.4", + "content-type": "~1.0.4", + "cookie": "0.5.0", + "cookie-signature": "1.0.6", + "debug": "2.6.9", + "depd": "2.0.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "finalhandler": "1.2.0", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "merge-descriptors": "1.0.1", + "methods": "~1.1.2", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "path-to-regexp": "0.1.7", + "proxy-addr": "~2.0.7", + "qs": "6.11.0", + "range-parser": "~1.2.1", + "safe-buffer": "5.2.1", + "send": "0.18.0", + "serve-static": "1.15.0", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "type-is": "~1.6.18", + "utils-merge": "1.0.1", + "vary": "~1.1.2" + }, + "engines": { + "node": ">= 0.10.0" + } + }, + "node_modules/fast-deep-equal": { + "version": "3.1.3", + "resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz", + "integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==" + }, + "node_modules/fill-range": { + "version": "7.0.1", + "resolved": "https://registry.npmjs.org/fill-range/-/fill-range-7.0.1.tgz", + "integrity": "sha512-qOo9F+dMUmC2Lcb4BbVvnKJxTPjCm+RRpe4gDuGrzkL7mEVl/djYSu2OdQ2Pa302N4oqkSg9ir6jaLWJ2USVpQ==", + "dev": true, + "dependencies": { + "to-regex-range": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/finalhandler": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/finalhandler/-/finalhandler-1.2.0.tgz", + "integrity": "sha512-5uXcUVftlQMFnWC9qu/svkWv3GTd2PfUhK/3PLkYNAe7FbqJMt3515HaxE6eRL74GdsriiwujiawdaB1BpEISg==", + "dependencies": { + "debug": "2.6.9", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "on-finished": "2.4.1", + "parseurl": "~1.3.3", + "statuses": "2.0.1", + "unpipe": "~1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/find-up": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/find-up/-/find-up-5.0.0.tgz", + "integrity": "sha512-78/PXT1wlLLDgTzDs7sjq9hzz0vXD+zn+7wypEe4fXQxCmdmqfGsEPQxmiCSQI3ajFV91bVSsvNtrJRiW6nGng==", + "dev": true, + "dependencies": { + "locate-path": "^6.0.0", + "path-exists": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/flat": { + "version": "5.0.2", + "resolved": "https://registry.npmjs.org/flat/-/flat-5.0.2.tgz", + "integrity": "sha512-b6suED+5/3rTpUBdG1gupIl8MPFCAMA0QXwmljLhvCUKcUvdE4gWky9zpuGCcXHOsz4J9wPGNWq6OKpmIzz3hQ==", + "dev": true, + "bin": { + "flat": "cli.js" + } + }, + "node_modules/forwarded": { + "version": "0.2.0", + "resolved": "https://registry.npmjs.org/forwarded/-/forwarded-0.2.0.tgz", + "integrity": "sha512-buRG0fpBtRHSTCOASe6hD258tEubFoRLb4ZNA6NxMVHNw2gOcwHo9wyablzMzOA5z9xA9L1KNjk/Nt6MT9aYow==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fresh": { + "version": "0.5.2", + "resolved": "https://registry.npmjs.org/fresh/-/fresh-0.5.2.tgz", + "integrity": "sha512-zJ2mQYM18rEFOudeV4GShTGIQ7RbzA7ozbU9I/XBpm7kqgMywgmylMwXHxZJmkVoYkna9d2pVXVXPdYTP9ej8Q==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/fs.realpath": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/fs.realpath/-/fs.realpath-1.0.0.tgz", + "integrity": "sha512-OO0pH2lK6a0hZnAdau5ItzHPI6pUlvI7jMVnxUQRtw4owF2wk8lOSabtGDCTP4Ggrg2MbGnWO9X8K1t4+fGMDw==", + "dev": true + }, + "node_modules/fsevents": { + "version": "2.3.3", + "resolved": "https://registry.npmjs.org/fsevents/-/fsevents-2.3.3.tgz", + "integrity": "sha512-5xoDfX+fL7faATnagmWPpbFtwh/R77WmMMqqHGS65C3vvB0YHrgF+B1YmZ3441tMj5n63k0212XNoJwzlhffQw==", + "dev": true, + "hasInstallScript": true, + "optional": true, + "os": [ + "darwin" + ], + "engines": { + "node": "^8.16.0 || ^10.6.0 || >=11.0.0" + } + }, + "node_modules/function-bind": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.2.tgz", + "integrity": "sha512-7XHNxH7qX9xG5mIwxkhumTox/MIRNcOgDrxWsMt2pAr23WHp6MrRlN7FBSFpCpr+oVO0F744iUgR82nJMfG2SA==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/get-caller-file": { + "version": "2.0.5", + "resolved": "https://registry.npmjs.org/get-caller-file/-/get-caller-file-2.0.5.tgz", + "integrity": "sha512-DyFP3BM/3YHTQOCUL/w0OZHR0lpKeGrxotcHWcqNEdnltqFwXVfhEBQ94eIo34AfQpo0rGki4cyIiftY06h2Fg==", + "dev": true, + "engines": { + "node": "6.* || 8.* || >= 10.*" + } + }, + "node_modules/get-func-name": { + "version": "2.0.2", + "resolved": "https://registry.npmjs.org/get-func-name/-/get-func-name-2.0.2.tgz", + "integrity": "sha512-8vXOvuE167CtIc3OyItco7N/dpRtBbYOsPsXCz7X/PMnlGjYjSGuZJgM1Y7mmew7BKf9BqvLX2tnOVy1BBUsxQ==", + "dev": true, + "engines": { + "node": "*" + } + }, + "node_modules/get-intrinsic": { + "version": "1.2.2", + "resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.2.tgz", + "integrity": "sha512-0gSo4ml/0j98Y3lngkFEot/zhiCeWsbYIlZ+uZOVgzLyLaUw7wxUL+nCTP0XJvJg1AXulJRI3UJi8GsbDuxdGA==", + "dependencies": { + "function-bind": "^1.1.2", + "has-proto": "^1.0.1", + "has-symbols": "^1.0.3", + "hasown": "^2.0.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/glob": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/glob/-/glob-8.1.0.tgz", + "integrity": "sha512-r8hpEjiQEYlF2QU0df3dS+nxxSIreXQS1qRhMJM0Q5NDdR386C7jb7Hwwod8Fgiuex+k0GFjgft18yvxm5XoCQ==", + "dev": true, + "dependencies": { + "fs.realpath": "^1.0.0", + "inflight": "^1.0.4", + "inherits": "2", + "minimatch": "^5.0.1", + "once": "^1.3.0" + }, + "engines": { + "node": ">=12" + }, + "funding": { + "url": "https://github.com/sponsors/isaacs" + } + }, + "node_modules/glob-parent": { + "version": "5.1.2", + "resolved": "https://registry.npmjs.org/glob-parent/-/glob-parent-5.1.2.tgz", + "integrity": "sha512-AOIgSQCepiJYwP3ARnGx+5VnTu2HBYdzbGP45eLw1vr3zB3vZLeyed1sC9hnbcOc9/SrMyM5RPQrkGz4aS9Zow==", + "dev": true, + "dependencies": { + "is-glob": "^4.0.1" + }, + "engines": { + "node": ">= 6" + } + }, + "node_modules/gopd": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/gopd/-/gopd-1.0.1.tgz", + "integrity": "sha512-d65bNlIadxvpb/A2abVdlqKqV563juRnZ1Wtk6s1sIR8uNsXR70xqIzVqxVf1eTqDunwT2MkczEeaezCKTZhwA==", + "dependencies": { + "get-intrinsic": "^1.1.3" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-flag": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/has-flag/-/has-flag-4.0.0.tgz", + "integrity": "sha512-EykJT/Q1KjTWctppgIAgfSO0tKVuZUjhgMr17kqTumMl6Afv3EISleU7qZUzoXDFTAHTDC4NOoG/ZxU3EvlMPQ==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/has-property-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-property-descriptors/-/has-property-descriptors-1.0.1.tgz", + "integrity": "sha512-VsX8eaIewvas0xnvinAe9bw4WfIeODpGYikiWYLH+dma0Jw6KHYqWiWfhQlgOVK8D6PvjubK5Uc4P0iIhIcNVg==", + "dependencies": { + "get-intrinsic": "^1.2.2" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-proto": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/has-proto/-/has-proto-1.0.1.tgz", + "integrity": "sha512-7qE+iP+O+bgF9clE5+UoBFzE65mlBiVj3tKCrlNQ0Ogwm0BjpT/gK4SlLYDMybDh5I3TCTKnPPa0oMG7JDYrhg==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/has-symbols": { + "version": "1.0.3", + "resolved": "https://registry.npmjs.org/has-symbols/-/has-symbols-1.0.3.tgz", + "integrity": "sha512-l3LCuF6MgDNwTDKkdYGEihYjt5pRPbEg46rtlmnSPlUbgmB8LOIrKJbYYFBSbnPaJexMKtiPO8hmeRjRz2Td+A==", + "engines": { + "node": ">= 0.4" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/hasown": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/hasown/-/hasown-2.0.0.tgz", + "integrity": "sha512-vUptKVTpIJhcczKBbgnS+RtcuYMB8+oNzPK2/Hp3hanz8JmpATdmmgLgSaadVREkDm+e2giHwY3ZRkyjSIDDFA==", + "dependencies": { + "function-bind": "^1.1.2" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/he": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/he/-/he-1.2.0.tgz", + "integrity": "sha512-F/1DnUGPopORZi0ni+CvrCgHQ5FyEAHRLSApuYWMmrbSwoN2Mn/7k+Gl38gJnR7yyDZk6WLXwiGod1JOWNDKGw==", + "dev": true, + "bin": { + "he": "bin/he" + } + }, + "node_modules/http-errors": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/http-errors/-/http-errors-2.0.0.tgz", + "integrity": "sha512-FtwrG/euBzaEjYeRqOgly7G0qviiXoJWnvEH2Z1plBdXgbyjv34pHTSb9zoeHMyDy33+DWy5Wt9Wo+TURtOYSQ==", + "dependencies": { + "depd": "2.0.0", + "inherits": "2.0.4", + "setprototypeof": "1.2.0", + "statuses": "2.0.1", + "toidentifier": "1.0.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/iconv-lite": { + "version": "0.4.24", + "resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.4.24.tgz", + "integrity": "sha512-v3MXnZAcvnywkTUEZomIActle7RXXeedOR31wwl7VlyoXO4Qi9arvSenNQWne1TcRwhCL1HwLI21bEqdpj8/rA==", + "dependencies": { + "safer-buffer": ">= 2.1.2 < 3" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/inflight": { + "version": "1.0.6", + "resolved": "https://registry.npmjs.org/inflight/-/inflight-1.0.6.tgz", + "integrity": "sha512-k92I/b08q4wvFscXCLvqfsHCrjrF7yiXsQuIVvVE7N82W3+aqpzuUdBbfhWcy/FZR3/4IgflMgKLOsvPDrGCJA==", + "dev": true, + "dependencies": { + "once": "^1.3.0", + "wrappy": "1" + } + }, + "node_modules/inherits": { + "version": "2.0.4", + "resolved": "https://registry.npmjs.org/inherits/-/inherits-2.0.4.tgz", + "integrity": "sha512-k/vGaX4/Yla3WzyMCvTQOXYeIHvqOKtnqBduzTHpzpQZzAskKMhZ2K+EnBiSM9zGSoIFeMpXKxa4dYeZIQqewQ==" + }, + "node_modules/ipaddr.js": { + "version": "1.9.1", + "resolved": "https://registry.npmjs.org/ipaddr.js/-/ipaddr.js-1.9.1.tgz", + "integrity": "sha512-0KI/607xoxSToH7GjN1FfSbLoU0+btTicjsQSWQlh/hZykN8KpmMf7uYwPW3R+akZ6R/w18ZlXSHBYXiYUPO3g==", + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/is-binary-path": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-binary-path/-/is-binary-path-2.1.0.tgz", + "integrity": "sha512-ZMERYes6pDydyuGidse7OsHxtbI7WVeUEozgR/g7rd0xUimYNlvZRE/K2MgZTjWy725IfelLeVcEM97mmtRGXw==", + "dev": true, + "dependencies": { + "binary-extensions": "^2.0.0" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-extglob": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/is-extglob/-/is-extglob-2.1.1.tgz", + "integrity": "sha512-SbKbANkN603Vi4jEZv49LeVJMn4yGwsbzZworEoyEiutsN3nJYdbO36zfhGJ6QEDpOZIFkDtnq5JRxmvl3jsoQ==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-fullwidth-code-point": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/is-fullwidth-code-point/-/is-fullwidth-code-point-3.0.0.tgz", + "integrity": "sha512-zymm5+u+sCsSWyD9qNaejV3DFvhCKclKdizYaJUuHA83RLjb7nSuGnddCHGv0hk+KY7BMAlsWeK4Ueg6EV6XQg==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-glob": { + "version": "4.0.3", + "resolved": "https://registry.npmjs.org/is-glob/-/is-glob-4.0.3.tgz", + "integrity": "sha512-xelSayHH36ZgE7ZWhli7pW34hNbNl8Ojv5KVmkJD4hBdD3th8Tfk9vYasLM+mXWOZhFkgZfxhLSnrwRr4elSSg==", + "dev": true, + "dependencies": { + "is-extglob": "^2.1.1" + }, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/is-number": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/is-number/-/is-number-7.0.0.tgz", + "integrity": "sha512-41Cifkg6e8TylSpdtTpeLVMqvSBEVzTttHvERD741+pnZ8ANv0004MRL43QKPDlK9cGvNp6NZWZUBlbGXYxxng==", + "dev": true, + "engines": { + "node": ">=0.12.0" + } + }, + "node_modules/is-plain-obj": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/is-plain-obj/-/is-plain-obj-2.1.0.tgz", + "integrity": "sha512-YWnfyRwxL/+SsrWYfOpUtz5b3YD+nyfkHvjbcanzk8zgyO4ASD67uVMRt8k5bM4lLMDnXfriRhOpemw+NfT1eA==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/is-unicode-supported": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/is-unicode-supported/-/is-unicode-supported-0.1.0.tgz", + "integrity": "sha512-knxG2q4UC3u8stRGyAVJCOdxFmv5DZiRcdlIaAQXAbSfJya+OhopNotLQrstBhququ4ZpuKbDc/8S6mgXgPFPw==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/js-yaml": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/js-yaml/-/js-yaml-4.1.0.tgz", + "integrity": "sha512-wpxZs9NoxZaJESJGIZTyDEaYpl0FKSA+FB9aJiyemKhMwkxQg63h4T1KJgUGHpTqPDNRcmmYLugrRjJlBtWvRA==", + "dev": true, + "dependencies": { + "argparse": "^2.0.1" + }, + "bin": { + "js-yaml": "bin/js-yaml.js" + } + }, + "node_modules/locate-path": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/locate-path/-/locate-path-6.0.0.tgz", + "integrity": "sha512-iPZK6eYjbxRu3uB4/WZ3EsEIMJFMqAoopl3R+zuq0UjcAm/MO6KCweDgPfP3elTztoKP3KtnVHxTn2NHBSDVUw==", + "dev": true, + "dependencies": { + "p-locate": "^5.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/lodash": { + "version": "4.17.21", + "resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz", + "integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg==" + }, + "node_modules/lodash.snakecase": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz", + "integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==" + }, + "node_modules/log-symbols": { + "version": "4.1.0", + "resolved": "https://registry.npmjs.org/log-symbols/-/log-symbols-4.1.0.tgz", + "integrity": "sha512-8XPvpAA8uyhfteu8pIvQxpJZ7SYYdpUivZpGy6sFsBuKRY/7rQGavedeB8aK+Zkyq6upMFVL/9AW6vOYzfRyLg==", + "dev": true, + "dependencies": { + "chalk": "^4.1.0", + "is-unicode-supported": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/loupe": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/loupe/-/loupe-3.1.0.tgz", + "integrity": "sha512-qKl+FrLXUhFuHUoDJG7f8P8gEMHq9NFS0c6ghXG1J0rldmZFQZoNVv/vyirE9qwCIhWZDsvEFd1sbFu3GvRQFg==", + "dev": true, + "dependencies": { + "get-func-name": "^2.0.1" + } + }, + "node_modules/magic-bytes.js": { + "version": "1.7.0", + "resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.7.0.tgz", + "integrity": "sha512-YzVU2+/hrjwx8xcgAw+ffNq3jkactpj+f1iSL4LonrFKhvnwDzHSqtFdk/MMRP53y9ScouJ7cKEnqYsJwsHoYA==" + }, + "node_modules/media-typer": { + "version": "0.3.0", + "resolved": "https://registry.npmjs.org/media-typer/-/media-typer-0.3.0.tgz", + "integrity": "sha512-dq+qelQ9akHpcOl/gUVRTxVIOkAJ1wR3QAvb4RsVjS8oVoFjDGTc679wJYmUmknUF5HwMLOgb5O+a3KxfWapPQ==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/memory-pager": { + "version": "1.5.0", + "resolved": "https://registry.npmjs.org/memory-pager/-/memory-pager-1.5.0.tgz", + "integrity": "sha512-ZS4Bp4r/Zoeq6+NLJpP+0Zzm0pR8whtGPf1XExKLJBAczGMnSi3It14OiNCStjQjM6NU1okjQGSxgEZN8eBYKg==" + }, + "node_modules/merge-descriptors": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/merge-descriptors/-/merge-descriptors-1.0.1.tgz", + "integrity": "sha512-cCi6g3/Zr1iqQi6ySbseM1Xvooa98N0w31jzUYrXPX2xqObmFGHJ0tQ5u74H3mVh7wLouTseZyYIq39g8cNp1w==" + }, + "node_modules/methods": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/methods/-/methods-1.1.2.tgz", + "integrity": "sha512-iclAHeNqNm68zFtnZ0e+1L2yUIdvzNoauKU4WBA3VvH/vPFieF7qfRlwUZU+DA9P9bPXIS90ulxoUoCH23sV2w==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime": { + "version": "1.6.0", + "resolved": "https://registry.npmjs.org/mime/-/mime-1.6.0.tgz", + "integrity": "sha512-x0Vn8spI+wuJ1O6S7gnbaQg8Pxh4NNHb7KSINmEWKiPE4RKOplvijn+NkmYmmRgP68mc70j2EbeTFRsrswaQeg==", + "bin": { + "mime": "cli.js" + }, + "engines": { + "node": ">=4" + } + }, + "node_modules/mime-db": { + "version": "1.52.0", + "resolved": "https://registry.npmjs.org/mime-db/-/mime-db-1.52.0.tgz", + "integrity": "sha512-sPU4uV7dYlvtWJxwwxHD0PuihVNiE7TyAbQ5SWxDCB9mUYvOgroQOwYQQOKPJ8CIbE+1ETVlOoK1UC2nU3gYvg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/mime-types": { + "version": "2.1.35", + "resolved": "https://registry.npmjs.org/mime-types/-/mime-types-2.1.35.tgz", + "integrity": "sha512-ZDY+bPm5zTTF+YpCrAU9nK0UgICYPT0QtT1NZWFv4s++TNkcgVaT0g6+4R2uI4MjQjzysHB1zxuWL50hzaeXiw==", + "dependencies": { + "mime-db": "1.52.0" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/minimatch": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/minimatch/-/minimatch-5.0.1.tgz", + "integrity": "sha512-nLDxIFRyhDblz3qMuq+SoRZED4+miJ/G+tdDrjkkkRnjAsBexeGpgjLEQ0blJy7rHhR2b93rhQY4SvyWu9v03g==", + "dev": true, + "dependencies": { + "brace-expansion": "^2.0.1" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/mocha": { + "version": "10.4.0", + "resolved": "https://registry.npmjs.org/mocha/-/mocha-10.4.0.tgz", + "integrity": "sha512-eqhGB8JKapEYcC4ytX/xrzKforgEc3j1pGlAXVy3eRwrtAy5/nIfT1SvgGzfN0XZZxeLq0aQWkOUAmqIJiv+bA==", + "dev": true, + "dependencies": { + "ansi-colors": "4.1.1", + "browser-stdout": "1.3.1", + "chokidar": "3.5.3", + "debug": "4.3.4", + "diff": "5.0.0", + "escape-string-regexp": "4.0.0", + "find-up": "5.0.0", + "glob": "8.1.0", + "he": "1.2.0", + "js-yaml": "4.1.0", + "log-symbols": "4.1.0", + "minimatch": "5.0.1", + "ms": "2.1.3", + "serialize-javascript": "6.0.0", + "strip-json-comments": "3.1.1", + "supports-color": "8.1.1", + "workerpool": "6.2.1", + "yargs": "16.2.0", + "yargs-parser": "20.2.4", + "yargs-unparser": "2.0.0" + }, + "bin": { + "_mocha": "bin/_mocha", + "mocha": "bin/mocha.js" + }, + "engines": { + "node": ">= 14.0.0" + } + }, + "node_modules/mocha/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/mocha/node_modules/debug/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/mocha/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==", + "dev": true + }, + "node_modules/mongodb": { + "version": "6.3.0", + "resolved": "https://registry.npmjs.org/mongodb/-/mongodb-6.3.0.tgz", + "integrity": "sha512-tt0KuGjGtLUhLoU263+xvQmPHEGTw5LbcNC73EoFRYgSHwZt5tsoJC110hDyO1kjQzpgNrpdcSza9PknWN4LrA==", + "dependencies": { + "@mongodb-js/saslprep": "^1.1.0", + "bson": "^6.2.0", + "mongodb-connection-string-url": "^3.0.0" + }, + "engines": { + "node": ">=16.20.1" + }, + "peerDependencies": { + "@aws-sdk/credential-providers": "^3.188.0", + "@mongodb-js/zstd": "^1.1.0", + "gcp-metadata": "^5.2.0", + "kerberos": "^2.0.1", + "mongodb-client-encryption": ">=6.0.0 <7", + "snappy": "^7.2.2", + "socks": "^2.7.1" + }, + "peerDependenciesMeta": { + "@aws-sdk/credential-providers": { + "optional": true + }, + "@mongodb-js/zstd": { + "optional": true + }, + "gcp-metadata": { + "optional": true + }, + "kerberos": { + "optional": true + }, + "mongodb-client-encryption": { + "optional": true + }, + "snappy": { + "optional": true + }, + "socks": { + "optional": true + } + } + }, + "node_modules/mongodb-connection-string-url": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/mongodb-connection-string-url/-/mongodb-connection-string-url-3.0.0.tgz", + "integrity": "sha512-t1Vf+m1I5hC2M5RJx/7AtxgABy1cZmIPQRMXw+gEIPn/cZNF3Oiy+l0UIypUwVB5trcWHq3crg2g3uAR9aAwsQ==", + "dependencies": { + "@types/whatwg-url": "^11.0.2", + "whatwg-url": "^13.0.0" + } + }, + "node_modules/morgan": { + "version": "1.10.0", + "resolved": "https://registry.npmjs.org/morgan/-/morgan-1.10.0.tgz", + "integrity": "sha512-AbegBVI4sh6El+1gNwvD5YIck7nSA36weD7xvIxG4in80j/UoK8AEGaWnnz8v1GxonMCltmlNs5ZKbGvl9b1XQ==", + "dependencies": { + "basic-auth": "~2.0.1", + "debug": "2.6.9", + "depd": "~2.0.0", + "on-finished": "~2.3.0", + "on-headers": "~1.0.2" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/morgan/node_modules/on-finished": { + "version": "2.3.0", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.3.0.tgz", + "integrity": "sha512-ikqdkGAAyf/X/gPhXGvfgAytDZtDbr+bkNUJ0N9h5MI/dmdgCs3l6hoHrcUv41sRKew3jIwrp4qQDXiK99Utww==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/ms": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.0.0.tgz", + "integrity": "sha512-Tpp60P6IUJDTuOq/5Z8cdskzJujfwqfOTkrwIwj7IRISpnkJnT6SyJ4PCPnGMoFjC9ddhal5KVIYtAt97ix05A==" + }, + "node_modules/negotiator": { + "version": "0.6.3", + "resolved": "https://registry.npmjs.org/negotiator/-/negotiator-0.6.3.tgz", + "integrity": "sha512-+EUsqGPLsM+j/zdChZjsnX51g4XrHFOIXwfnCVPGlQk/k5giakcKsuxCObBRu6DSm9opw/O6slWbJdghQM4bBg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/normalize-path": { + "version": "3.0.0", + "resolved": "https://registry.npmjs.org/normalize-path/-/normalize-path-3.0.0.tgz", + "integrity": "sha512-6eZs5Ls3WtCisHWp9S2GUy8dqkpGi4BVSz3GaqiE6ezub0512ESztXUwUB6C6IKbQkY2Pnb/mD4WYojCRwcwLA==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-assign": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/object-assign/-/object-assign-4.1.1.tgz", + "integrity": "sha512-rJgTQnkUnH1sFw8yT6VSU3zD3sWmu6sZhIseY8VX+GRu3P6F7Fu+JNDoXfklElbLJSnc3FUQHVe4cU5hj+BcUg==", + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/object-inspect": { + "version": "1.13.1", + "resolved": "https://registry.npmjs.org/object-inspect/-/object-inspect-1.13.1.tgz", + "integrity": "sha512-5qoj1RUiKOMsCCNLV1CBiPYE10sziTsnmNxkAI/rZhiD63CF7IqdFGC/XzjWjpSgLf0LxXX3bDFIh0E18f6UhQ==", + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/on-finished": { + "version": "2.4.1", + "resolved": "https://registry.npmjs.org/on-finished/-/on-finished-2.4.1.tgz", + "integrity": "sha512-oVlzkg3ENAhCk2zdv7IJwd/QUD4z2RxRwpkcGY8psCVcCYZNq4wYnVWALHM+brtuJjePWiYF/ClmuDr8Ch5+kg==", + "dependencies": { + "ee-first": "1.1.1" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/on-headers": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/on-headers/-/on-headers-1.0.2.tgz", + "integrity": "sha512-pZAE+FJLoyITytdqK0U5s+FIpjN0JP3OzFi/u8Rx+EV5/W+JTWGXG8xFzevE7AjBfDqHv/8vL8qQsIhHnqRkrA==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/once": { + "version": "1.4.0", + "resolved": "https://registry.npmjs.org/once/-/once-1.4.0.tgz", + "integrity": "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w==", + "dev": true, + "dependencies": { + "wrappy": "1" + } + }, + "node_modules/p-limit": { + "version": "3.1.0", + "resolved": "https://registry.npmjs.org/p-limit/-/p-limit-3.1.0.tgz", + "integrity": "sha512-TYOanM3wGwNGsZN2cVTYPArw454xnXj5qmWF1bEoAc4+cU/ol7GVh7odevjp1FNHduHc3KZMcFduxU5Xc6uJRQ==", + "dev": true, + "dependencies": { + "yocto-queue": "^0.1.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/p-locate": { + "version": "5.0.0", + "resolved": "https://registry.npmjs.org/p-locate/-/p-locate-5.0.0.tgz", + "integrity": "sha512-LaNjtRWUBY++zB5nE/NwcaoMylSPk+S+ZHNB1TzdbMJMny6dynpAGt7X/tl/QYq3TIeE6nxHppbo2LGymrG5Pw==", + "dev": true, + "dependencies": { + "p-limit": "^3.0.2" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/parseurl": { + "version": "1.3.3", + "resolved": "https://registry.npmjs.org/parseurl/-/parseurl-1.3.3.tgz", + "integrity": "sha512-CiyeOxFT/JZyN5m0z9PfXw4SCBJ6Sygz1Dpl0wqjlhDEGGBP1GnsUVEL0p63hoG1fcj3fHynXi9NYO4nWOL+qQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/path-exists": { + "version": "4.0.0", + "resolved": "https://registry.npmjs.org/path-exists/-/path-exists-4.0.0.tgz", + "integrity": "sha512-ak9Qy5Q7jYb2Wwcey5Fpvg2KoAc/ZIhLSLOSBmRmygPsGwkVVt0fZa0qrtMz+m6tJTAHfZQ8FnmB4MG4LWy7/w==", + "dev": true, + "engines": { + "node": ">=8" + } + }, + "node_modules/path-to-regexp": { + "version": "0.1.7", + "resolved": "https://registry.npmjs.org/path-to-regexp/-/path-to-regexp-0.1.7.tgz", + "integrity": "sha512-5DFkuoqlv1uYQKxy8omFBeJPQcdoE07Kv2sferDCrAq1ohOU+MSDswDIbnx3YAM60qIOnYa53wBhXW0EbMonrQ==" + }, + "node_modules/pathval": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/pathval/-/pathval-2.0.0.tgz", + "integrity": "sha512-vE7JKRyES09KiunauX7nd2Q9/L7lhok4smP9RZTDeD4MVs72Dp2qNFVz39Nz5a0FVEW0BJR6C0DYrq6unoziZA==", + "dev": true, + "engines": { + "node": ">= 14.16" + } + }, + "node_modules/picomatch": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/picomatch/-/picomatch-2.3.1.tgz", + "integrity": "sha512-JU3teHTNjmE2VCGFzuY8EXzCDVwEqB2a8fsIvwaStHhAWJEeVd1o1QD80CU6+ZdEXXSLbSsuLwJjkCBWqRQUVA==", + "dev": true, + "engines": { + "node": ">=8.6" + }, + "funding": { + "url": "https://github.com/sponsors/jonschlinkert" + } + }, + "node_modules/proxy-addr": { + "version": "2.0.7", + "resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz", + "integrity": "sha512-llQsMLSUDUPT44jdrU/O37qlnifitDP+ZwrmmZcoSKyLKvtZxpyV0n2/bD/N4tBAAZ/gJEdZU7KMraoK1+XYAg==", + "dependencies": { + "forwarded": "0.2.0", + "ipaddr.js": "1.9.1" + }, + "engines": { + "node": ">= 0.10" + } + }, + "node_modules/punycode": { + "version": "2.3.1", + "resolved": "https://registry.npmjs.org/punycode/-/punycode-2.3.1.tgz", + "integrity": "sha512-vYt7UD1U9Wg6138shLtLOvdAu+8DsC/ilFtEVHcH+wydcSpNE20AfSOduf6MkRFahL5FY7X1oU7nKVZFtfq8Fg==", + "engines": { + "node": ">=6" + } + }, + "node_modules/qs": { + "version": "6.11.0", + "resolved": "https://registry.npmjs.org/qs/-/qs-6.11.0.tgz", + "integrity": "sha512-MvjoMCJwEarSbUYk5O+nmoSzSutSsTwF85zcHPQ9OrlFoZOYIjaqBAJIqIXjptyD5vThxGq52Xu/MaJzRkIk4Q==", + "dependencies": { + "side-channel": "^1.0.4" + }, + "engines": { + "node": ">=0.6" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/randombytes": { + "version": "2.1.0", + "resolved": "https://registry.npmjs.org/randombytes/-/randombytes-2.1.0.tgz", + "integrity": "sha512-vYl3iOX+4CKUWuxGi9Ukhie6fsqXqS9FE2Zaic4tNFD2N2QQaXOMFbuKK4QmDHC0JO6B1Zp41J0LpT0oR68amQ==", + "dev": true, + "dependencies": { + "safe-buffer": "^5.1.0" + } + }, + "node_modules/range-parser": { + "version": "1.2.1", + "resolved": "https://registry.npmjs.org/range-parser/-/range-parser-1.2.1.tgz", + "integrity": "sha512-Hrgsx+orqoygnmhFbKaHE6c296J+HTAQXoxEF6gNupROmmGJRoyzfG3ccAveqCBrwr/2yxQ5BVd/GTl5agOwSg==", + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/raw-body": { + "version": "2.5.1", + "resolved": "https://registry.npmjs.org/raw-body/-/raw-body-2.5.1.tgz", + "integrity": "sha512-qqJBtEyVgS0ZmPGdCFPWJ3FreoqvG4MVQln/kCgF7Olq95IbOp0/BWyMwbdtn4VTvkM8Y7khCQ2Xgk/tcrCXig==", + "dependencies": { + "bytes": "3.1.2", + "http-errors": "2.0.0", + "iconv-lite": "0.4.24", + "unpipe": "1.0.0" + }, + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/readdirp": { + "version": "3.6.0", + "resolved": "https://registry.npmjs.org/readdirp/-/readdirp-3.6.0.tgz", + "integrity": "sha512-hOS089on8RduqdbhvQ5Z37A0ESjsqz6qnRcffsMU3495FuTdqSm+7bhJ29JvIOsBDEEnan5DPu9t3To9VRlMzA==", + "dev": true, + "dependencies": { + "picomatch": "^2.2.1" + }, + "engines": { + "node": ">=8.10.0" + } + }, + "node_modules/require-directory": { + "version": "2.1.1", + "resolved": "https://registry.npmjs.org/require-directory/-/require-directory-2.1.1.tgz", + "integrity": "sha512-fGxEI7+wsG9xrvdjsrlmL22OMTTiHRwAMroiEeMgq8gzoLC/PQr7RsRDSTLUg/bZAZtF+TVIkHc6/4RIKrui+Q==", + "dev": true, + "engines": { + "node": ">=0.10.0" + } + }, + "node_modules/safe-buffer": { + "version": "5.2.1", + "resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.2.1.tgz", + "integrity": "sha512-rp3So07KcdmmKbGvgaNxQSJr7bGVSVk5S9Eq1F+ppbRo70+YeaDxkw5Dd8NPN+GD6bjnYm2VuPuCXmpuYvmCXQ==", + "funding": [ + { + "type": "github", + "url": "https://github.com/sponsors/feross" + }, + { + "type": "patreon", + "url": "https://www.patreon.com/feross" + }, + { + "type": "consulting", + "url": "https://feross.org/support" + } + ] + }, + "node_modules/safer-buffer": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/safer-buffer/-/safer-buffer-2.1.2.tgz", + "integrity": "sha512-YZo3K82SD7Riyi0E1EQPojLz7kpepnSQI9IyPbHHg1XXXevb5dJI7tpyN2ADxGcQbHG7vcyRHk0cbwqcQriUtg==" + }, + "node_modules/send": { + "version": "0.18.0", + "resolved": "https://registry.npmjs.org/send/-/send-0.18.0.tgz", + "integrity": "sha512-qqWzuOjSFOuqPjFe4NOsMLafToQQwBSOEpS+FwEt3A2V3vKubTquT3vmLTQpFgMXp8AlFWFuP1qKaJZOtPpVXg==", + "dependencies": { + "debug": "2.6.9", + "depd": "2.0.0", + "destroy": "1.2.0", + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "etag": "~1.8.1", + "fresh": "0.5.2", + "http-errors": "2.0.0", + "mime": "1.6.0", + "ms": "2.1.3", + "on-finished": "2.4.1", + "range-parser": "~1.2.1", + "statuses": "2.0.1" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/send/node_modules/ms": { + "version": "2.1.3", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz", + "integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA==" + }, + "node_modules/serialize-javascript": { + "version": "6.0.0", + "resolved": "https://registry.npmjs.org/serialize-javascript/-/serialize-javascript-6.0.0.tgz", + "integrity": "sha512-Qr3TosvguFt8ePWqsvRfrKyQXIiW+nGbYpy8XK24NQHE83caxWt+mIymTT19DGFbNWNLfEwsrkSmN64lVWB9ag==", + "dev": true, + "dependencies": { + "randombytes": "^2.1.0" + } + }, + "node_modules/serve-static": { + "version": "1.15.0", + "resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz", + "integrity": "sha512-XGuRDNjXUijsUL0vl6nSD7cwURuzEgglbOaFuZM9g3kwDXOWVTck0jLzjPzGD+TazWbboZYu52/9/XPdUgne9g==", + "dependencies": { + "encodeurl": "~1.0.2", + "escape-html": "~1.0.3", + "parseurl": "~1.3.3", + "send": "0.18.0" + }, + "engines": { + "node": ">= 0.8.0" + } + }, + "node_modules/set-function-length": { + "version": "1.1.1", + "resolved": "https://registry.npmjs.org/set-function-length/-/set-function-length-1.1.1.tgz", + "integrity": "sha512-VoaqjbBJKiWtg4yRcKBQ7g7wnGnLV3M8oLvVWwOk2PdYY6PEFegR1vezXR0tw6fZGF9csVakIRjrJiy2veSBFQ==", + "dependencies": { + "define-data-property": "^1.1.1", + "get-intrinsic": "^1.2.1", + "gopd": "^1.0.1", + "has-property-descriptors": "^1.0.0" + }, + "engines": { + "node": ">= 0.4" + } + }, + "node_modules/setprototypeof": { + "version": "1.2.0", + "resolved": "https://registry.npmjs.org/setprototypeof/-/setprototypeof-1.2.0.tgz", + "integrity": "sha512-E5LDX7Wrp85Kil5bhZv46j8jOeboKq5JMmYM3gVGdGH8xFpPWXUMsNrlODCrkoxMEeNi/XZIwuRvY4XNwYMJpw==" + }, + "node_modules/side-channel": { + "version": "1.0.4", + "resolved": "https://registry.npmjs.org/side-channel/-/side-channel-1.0.4.tgz", + "integrity": "sha512-q5XPytqFEIKHkGdiMIrY10mvLRvnQh42/+GoBlFW3b2LXLE2xxJpZFdm94we0BaoV3RwJyGqg5wS7epxTv0Zvw==", + "dependencies": { + "call-bind": "^1.0.0", + "get-intrinsic": "^1.0.2", + "object-inspect": "^1.9.0" + }, + "funding": { + "url": "https://github.com/sponsors/ljharb" + } + }, + "node_modules/socket.io": { + "version": "4.7.2", + "resolved": "https://registry.npmjs.org/socket.io/-/socket.io-4.7.2.tgz", + "integrity": "sha512-bvKVS29/I5fl2FGLNHuXlQaUH/BlzX1IN6S+NKLNZpBsPZIDH+90eQmCs2Railn4YUiww4SzUedJ6+uzwFnKLw==", + "dependencies": { + "accepts": "~1.3.4", + "base64id": "~2.0.0", + "cors": "~2.8.5", + "debug": "~4.3.2", + "engine.io": "~6.5.2", + "socket.io-adapter": "~2.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.2.0" + } + }, + "node_modules/socket.io-adapter": { + "version": "2.5.2", + "resolved": "https://registry.npmjs.org/socket.io-adapter/-/socket.io-adapter-2.5.2.tgz", + "integrity": "sha512-87C3LO/NOMc+eMcpcxUBebGjkpMDkNBS9tf7KJqcDsmL936EChtVva71Dw2q4tQcuVC+hAUy4an2NO/sYXmwRA==", + "dependencies": { + "ws": "~8.11.0" + } + }, + "node_modules/socket.io-client": { + "version": "4.7.5", + "resolved": "https://registry.npmjs.org/socket.io-client/-/socket.io-client-4.7.5.tgz", + "integrity": "sha512-sJ/tqHOCe7Z50JCBCXrsY3I2k03iOiUe+tj1OmKeD2lXPiGH/RUCdTZFoqVyN7l1MnpIzPrGtLcijffmeouNlQ==", + "dev": true, + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.2", + "engine.io-client": "~6.5.2", + "socket.io-parser": "~4.2.4" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-client/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dev": true, + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-client/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", + "dev": true + }, + "node_modules/socket.io-parser": { + "version": "4.2.4", + "resolved": "https://registry.npmjs.org/socket.io-parser/-/socket.io-parser-4.2.4.tgz", + "integrity": "sha512-/GbIKmo8ioc+NIWIhwdecY0ge+qVBSMdgxGygevmdHj24bsfgtCmcUUcQ5ZzcylGFHsN3k4HB4Cgkl96KVnuew==", + "dependencies": { + "@socket.io/component-emitter": "~3.1.0", + "debug": "~4.3.1" + }, + "engines": { + "node": ">=10.0.0" + } + }, + "node_modules/socket.io-parser/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io-parser/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/socket.io/node_modules/debug": { + "version": "4.3.4", + "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", + "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", + "dependencies": { + "ms": "2.1.2" + }, + "engines": { + "node": ">=6.0" + }, + "peerDependenciesMeta": { + "supports-color": { + "optional": true + } + } + }, + "node_modules/socket.io/node_modules/ms": { + "version": "2.1.2", + "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" + }, + "node_modules/sparse-bitfield": { + "version": "3.0.3", + "resolved": "https://registry.npmjs.org/sparse-bitfield/-/sparse-bitfield-3.0.3.tgz", + "integrity": "sha512-kvzhi7vqKTfkh0PZU+2D2PIllw2ymqJKujUcyPMd9Y75Nv4nPbGJZXNhxsgdQab2BmlDct1YnfQCguEvHr7VsQ==", + "dependencies": { + "memory-pager": "^1.0.2" + } + }, + "node_modules/statuses": { + "version": "2.0.1", + "resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz", + "integrity": "sha512-RwNA9Z/7PrK06rYLIzFMlaF+l73iwpzsqRIFgbMLbTcLD6cOao82TaWefPXQvB2fOC4AjuYSEndS7N/mTCbkdQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/string-width": { + "version": "4.2.3", + "resolved": "https://registry.npmjs.org/string-width/-/string-width-4.2.3.tgz", + "integrity": "sha512-wKyQRQpjJ0sIp62ErSZdGsjMJWsap5oRNihHhu6G7JVO/9jIB6UyevL+tXuOqrng8j/cxKTWyWUwvSTriiZz/g==", + "dev": true, + "dependencies": { + "emoji-regex": "^8.0.0", + "is-fullwidth-code-point": "^3.0.0", + "strip-ansi": "^6.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-ansi": { + "version": "6.0.1", + "resolved": "https://registry.npmjs.org/strip-ansi/-/strip-ansi-6.0.1.tgz", + "integrity": "sha512-Y38VPSHcqkFrCpFnQ9vuSXmquuv5oXOKpGeT6aGrr3o3Gc9AlVa6JBfUSOCnbxGGZF+/0ooI7KrPuUSztUdU5A==", + "dev": true, + "dependencies": { + "ansi-regex": "^5.0.1" + }, + "engines": { + "node": ">=8" + } + }, + "node_modules/strip-json-comments": { + "version": "3.1.1", + "resolved": "https://registry.npmjs.org/strip-json-comments/-/strip-json-comments-3.1.1.tgz", + "integrity": "sha512-6fPc+R4ihwqP6N/aIv2f1gMH8lOVtWQHoqC4yK6oSDVVocumAsfCqjkXnqiYMhmMwS/mEHLp7Vehlt3ql6lEig==", + "dev": true, + "engines": { + "node": ">=8" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + }, + "node_modules/supports-color": { + "version": "8.1.1", + "resolved": "https://registry.npmjs.org/supports-color/-/supports-color-8.1.1.tgz", + "integrity": "sha512-MpUEN2OodtUzxvKQl72cUF7RQ5EiHsGvSsVG0ia9c5RbWGL2CI4C7EpPS8UTBIplnlzZiNuV56w+FuNxy3ty2Q==", + "dev": true, + "dependencies": { + "has-flag": "^4.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/supports-color?sponsor=1" + } + }, + "node_modules/to-regex-range": { + "version": "5.0.1", + "resolved": "https://registry.npmjs.org/to-regex-range/-/to-regex-range-5.0.1.tgz", + "integrity": "sha512-65P7iz6X5yEr1cwcgvQxbbIw7Uk3gOy5dIdtZ4rDveLqhrdJP+Li/Hx6tyK0NEb+2GCyneCMJiGqrADCSNk8sQ==", + "dev": true, + "dependencies": { + "is-number": "^7.0.0" + }, + "engines": { + "node": ">=8.0" + } + }, + "node_modules/toidentifier": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/toidentifier/-/toidentifier-1.0.1.tgz", + "integrity": "sha512-o5sSPKEkg/DIQNmH43V0/uerLrpzVedkUh8tGNvaeXpfpuwjKenlSox/2O/BTlZUtEe+JG7s5YhEz608PlAHRA==", + "engines": { + "node": ">=0.6" + } + }, + "node_modules/tr46": { + "version": "4.1.1", + "resolved": "https://registry.npmjs.org/tr46/-/tr46-4.1.1.tgz", + "integrity": "sha512-2lv/66T7e5yNyhAAC4NaKe5nVavzuGJQVVtRYLyQ2OI8tsJ61PMLlelehb0wi2Hx6+hT/OJUWZcw8MjlSRnxvw==", + "dependencies": { + "punycode": "^2.3.0" + }, + "engines": { + "node": ">=14" + } + }, + "node_modules/ts-mixer": { + "version": "6.0.3", + "resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.3.tgz", + "integrity": "sha512-k43M7uCG1AkTyxgnmI5MPwKoUvS/bRvLvUb7+Pgpdlmok8AoqmUaZxUUw8zKM5B1lqZrt41GjYgnvAi0fppqgQ==" + }, + "node_modules/tslib": { + "version": "2.6.2", + "resolved": "https://registry.npmjs.org/tslib/-/tslib-2.6.2.tgz", + "integrity": "sha512-AEYxH93jGFPn/a2iVAwW87VuUIkR1FVUKB77NwMF7nBTDkDrrT/Hpt/IrCJ0QXhW27jTBDcf5ZY7w6RiqTMw2Q==" + }, + "node_modules/type-is": { + "version": "1.6.18", + "resolved": "https://registry.npmjs.org/type-is/-/type-is-1.6.18.tgz", + "integrity": "sha512-TkRKr9sUTxEH8MdfuCSP7VizJyzRNMjj2J2do2Jr3Kym598JVdEksuzPQCnlFPW4ky9Q+iA+ma9BGm06XQBy8g==", + "dependencies": { + "media-typer": "0.3.0", + "mime-types": "~2.1.24" + }, + "engines": { + "node": ">= 0.6" + } + }, + "node_modules/undici": { + "version": "5.27.2", + "resolved": "https://registry.npmjs.org/undici/-/undici-5.27.2.tgz", + "integrity": "sha512-iS857PdOEy/y3wlM3yRp+6SNQQ6xU0mmZcwRSriqk+et/cwWAtwmIGf6WkoDN2EK/AMdCO/dfXzIwi+rFMrjjQ==", + "dependencies": { + "@fastify/busboy": "^2.0.0" + }, + "engines": { + "node": ">=14.0" + } + }, + "node_modules/undici-types": { + "version": "5.26.5", + "resolved": "https://registry.npmjs.org/undici-types/-/undici-types-5.26.5.tgz", + "integrity": "sha512-JlCMO+ehdEIKqlFxk6IfVoAUVmgz7cU7zD/h9XZ0qzeosSHmUJVOzSQvvYSYWXkFXC+IfLKSIffhv0sVZup6pA==" + }, + "node_modules/unpipe": { + "version": "1.0.0", + "resolved": "https://registry.npmjs.org/unpipe/-/unpipe-1.0.0.tgz", + "integrity": "sha512-pjy2bYhSsufwWlKwPc+l3cN7+wuJlK6uz0YdJEOlQDbl6jo/YlPi4mb8agUkVC8BF7V8NuzeyPNqRksA3hztKQ==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/utils-merge": { + "version": "1.0.1", + "resolved": "https://registry.npmjs.org/utils-merge/-/utils-merge-1.0.1.tgz", + "integrity": "sha512-pMZTvIkT1d+TFGvDOqodOclx0QWkkgi6Tdoa8gC8ffGAAqz9pzPTZWAybbsHHoED/ztMtkv/VoYTYyShUn81hA==", + "engines": { + "node": ">= 0.4.0" + } + }, + "node_modules/vary": { + "version": "1.1.2", + "resolved": "https://registry.npmjs.org/vary/-/vary-1.1.2.tgz", + "integrity": "sha512-BNGbWLfd0eUPabhkXUVm0j8uuvREyTh5ovRa/dyow/BqAbZJyC+5fU+IzQOzmAKzYqYRAISoRhdQr3eIZ/PXqg==", + "engines": { + "node": ">= 0.8" + } + }, + "node_modules/webidl-conversions": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/webidl-conversions/-/webidl-conversions-7.0.0.tgz", + "integrity": "sha512-VwddBukDzu71offAQR975unBIGqfKZpM+8ZX6ySk8nYhVoo5CYaZyzt3YBvYtRtO+aoGlqxPg/B87NGVZ/fu6g==", + "engines": { + "node": ">=12" + } + }, + "node_modules/whatwg-url": { + "version": "13.0.0", + "resolved": "https://registry.npmjs.org/whatwg-url/-/whatwg-url-13.0.0.tgz", + "integrity": "sha512-9WWbymnqj57+XEuqADHrCJ2eSXzn8WXIW/YSGaZtb2WKAInQ6CHfaUUcTyyver0p8BDg5StLQq8h1vtZuwmOig==", + "dependencies": { + "tr46": "^4.1.1", + "webidl-conversions": "^7.0.0" + }, + "engines": { + "node": ">=16" + } + }, + "node_modules/workerpool": { + "version": "6.2.1", + "resolved": "https://registry.npmjs.org/workerpool/-/workerpool-6.2.1.tgz", + "integrity": "sha512-ILEIE97kDZvF9Wb9f6h5aXK4swSlKGUcOEGiIYb2OOu/IrDU9iwj0fD//SsA6E5ibwJxpEvhullJY4Sl4GcpAw==", + "dev": true + }, + "node_modules/wrap-ansi": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/wrap-ansi/-/wrap-ansi-7.0.0.tgz", + "integrity": "sha512-YVGIj2kamLSTxw6NsZjoBxfSwsn0ycdesmc4p+Q21c5zPuZ1pl+NfxVdxPtdHvmNVOQ6XSYG4AUtyt/Fi7D16Q==", + "dev": true, + "dependencies": { + "ansi-styles": "^4.0.0", + "string-width": "^4.1.0", + "strip-ansi": "^6.0.0" + }, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/chalk/wrap-ansi?sponsor=1" + } + }, + "node_modules/wrappy": { + "version": "1.0.2", + "resolved": "https://registry.npmjs.org/wrappy/-/wrappy-1.0.2.tgz", + "integrity": "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ==", + "dev": true + }, + "node_modules/ws": { + "version": "8.11.0", + "resolved": "https://registry.npmjs.org/ws/-/ws-8.11.0.tgz", + "integrity": "sha512-HPG3wQd9sNQoT9xHyNCXoDUa+Xw/VevmY9FoHyQ+g+rrMn4j6FB4np7Z0OhdTgjx6MgQLK7jwSy1YecU1+4Asg==", + "engines": { + "node": ">=10.0.0" + }, + "peerDependencies": { + "bufferutil": "^4.0.1", + "utf-8-validate": "^5.0.2" + }, + "peerDependenciesMeta": { + "bufferutil": { + "optional": true + }, + "utf-8-validate": { + "optional": true + } + } + }, + "node_modules/xmlhttprequest-ssl": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/xmlhttprequest-ssl/-/xmlhttprequest-ssl-2.0.0.tgz", + "integrity": "sha512-QKxVRxiRACQcVuQEYFsI1hhkrMlrXHPegbbd1yn9UHOmRxY+si12nQYzri3vbzt8VdTTRviqcKxcyllFas5z2A==", + "dev": true, + "engines": { + "node": ">=0.4.0" + } + }, + "node_modules/y18n": { + "version": "5.0.8", + "resolved": "https://registry.npmjs.org/y18n/-/y18n-5.0.8.tgz", + "integrity": "sha512-0pfFzegeDWJHJIAmTLRP2DwHjdF5s7jo9tuztdQxAhINCdvS+3nGINqPd00AphqJR/0LhANUS6/+7SCb98YOfA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs": { + "version": "16.2.0", + "resolved": "https://registry.npmjs.org/yargs/-/yargs-16.2.0.tgz", + "integrity": "sha512-D1mvvtDG0L5ft/jGWkLpG1+m0eQxOfaBvTNELraWj22wSVUMWxZUvYgJYcKh6jGGIkJFhH4IZPQhR4TKpc8mBw==", + "dev": true, + "dependencies": { + "cliui": "^7.0.2", + "escalade": "^3.1.1", + "get-caller-file": "^2.0.5", + "require-directory": "^2.1.1", + "string-width": "^4.2.0", + "y18n": "^5.0.5", + "yargs-parser": "^20.2.2" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-parser": { + "version": "20.2.4", + "resolved": "https://registry.npmjs.org/yargs-parser/-/yargs-parser-20.2.4.tgz", + "integrity": "sha512-WOkpgNhPTlE73h4VFAFsOnomJVaovO8VqLDzy5saChRBFQFBoMYirowyW+Q9HB4HFF4Z7VZTiG3iSzJJA29yRA==", + "dev": true, + "engines": { + "node": ">=10" + } + }, + "node_modules/yargs-unparser": { + "version": "2.0.0", + "resolved": "https://registry.npmjs.org/yargs-unparser/-/yargs-unparser-2.0.0.tgz", + "integrity": "sha512-7pRTIA9Qc1caZ0bZ6RYRGbHJthJWuakf+WmHK0rVeLkNrrGhfoabBNdue6kdINI6r4if7ocq9aD/n7xwKOdzOA==", + "dev": true, + "dependencies": { + "camelcase": "^6.0.0", + "decamelize": "^4.0.0", + "flat": "^5.0.2", + "is-plain-obj": "^2.1.0" + }, + "engines": { + "node": ">=10" + } + }, + "node_modules/yocto-queue": { + "version": "0.1.0", + "resolved": "https://registry.npmjs.org/yocto-queue/-/yocto-queue-0.1.0.tgz", + "integrity": "sha512-rVksvsnNCdJ/ohGc6xgPwyN8eheCxsiLM8mxuE/t/mOVqJewPuO1miLpTHQiRgTKCLexL4MeAFVagts7HmNZ2Q==", + "dev": true, + "engines": { + "node": ">=10" + }, + "funding": { + "url": "https://github.com/sponsors/sindresorhus" + } + } + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..bb85c79 --- /dev/null +++ b/package.json @@ -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" + } +} diff --git a/server.js b/server.js new file mode 100644 index 0000000..04769c3 --- /dev/null +++ b/server.js @@ -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); \ No newline at end of file diff --git a/test/socketServerWrappers.test.js b/test/socketServerWrappers.test.js new file mode 100644 index 0000000..04caf89 --- /dev/null +++ b/test/socketServerWrappers.test.js @@ -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 + }); + }) +}); \ No newline at end of file