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