Major updates to core

This commit is contained in:
Logan Cusano
2024-02-10 15:10:35 -05:00
parent d563021866
commit 9c46792959
22 changed files with 977 additions and 156 deletions

View File

@@ -1,7 +1,7 @@
import { generateUniqueID } from './modules/baseUtils.mjs';
import { updateId } from './modules/updateConfig.mjs';
import { ClientNodeConfig } from './modules/clientObjectDefinitions.mjs';
import { initSocketConnection, initSocketListeners, sendNodeUpdate } from './modules/socketClient.mjs';
import { initSocketConnection } from './modules/socketClient.mjs';
import dotenv from 'dotenv';
dotenv.config()
@@ -9,7 +9,7 @@ dotenv.config()
var localNodeConfig = new ClientNodeConfig({})
async function boot() {
if (localNodeConfig.node.id === undefined || localNodeConfig.node.id === '' || localNodeConfig.node.id === 0) {
if (localNodeConfig.node.nuid === undefined || localNodeConfig.node.nuid === '' || localNodeConfig.node.nuid === 0) {
// Run the first time boot sequence
await firstTimeBoot();
}
@@ -25,10 +25,10 @@ async function boot() {
async function firstTimeBoot() {
// Generate a new ID for the node
localNodeConfig.node.id = generateUniqueID();
console.log(`Generated a new unique ID for this node: '${localNodeConfig.node.id}'`);
console.log(`Generated a new unique ID for this node: '${localNodeConfig.node.nuid}'`);
// Update the config with the new ID
updateId(localNodeConfig.node.id);
updateId(localNodeConfig.node.nuid);
console.log("Updated the config with the new node ID");
// TODO - Create the config file with the ID given and replace the update above
// TODO - Check if the system is linux or windows and set the 'type' param in DAB
@@ -41,7 +41,5 @@ async function firstTimeBoot() {
// Boot the client application
boot().then((openSocket) => {
initSocketListeners(openSocket, localNodeConfig);
//console.log(openSocket, "Open socket");
setTimeout(() => {sendNodeUpdate(openSocket);}, 2500);
console.log(openSocket, "Booted Sucessfully");
})

View File

@@ -78,6 +78,7 @@ export async function connectToChannel(channel) {
});
try {
await entersState(connection, VoiceConnectionStatus.Ready, 30_000);
await connection.subscribe(player);
return connection;
} catch (error) {
connection.destroy();
@@ -100,6 +101,8 @@ export async function initDiscordBotClient(token, readyCallback){
readyCallback(client);
});
/* on event create
// TODO - Implement methods for discord users to interact directly with the bots for realtime info (last talked ID/TG, etc.)
client.on(Events.MessageCreate, async (message) => {
if (!message.guild) return;
console.log(`New Message:`, message.content);
@@ -118,6 +121,7 @@ export async function initDiscordBotClient(token, readyCallback){
}
}
});
*/
client.login(token);
}

View File

@@ -1,5 +1,7 @@
import { networkInterfaces } from 'os';
import { createHash, randomBytes } from 'crypto';
import { promises, constants } from 'fs';
import { dirname } from "path";
/**
* Check to see if the input is a valid JSON string
@@ -7,7 +9,7 @@ import { createHash, randomBytes } from 'crypto';
* @param {*} str The string to check for valud JSON
* @returns {true|false}
*/
export function isJsonString (str) {
export const isJsonString = (str) => {
try {
JSON.parse(str);
} catch (e) {
@@ -16,12 +18,25 @@ export function isJsonString (str) {
return true;
}
/**
* Check to see if a path exists, creating it if it does not
* @returns {undefined}
*/
export const ensureDirectoryExists = async (filePath) => {
const directory = dirname(filePath);
try {
await promises.access(directory, constants.F_OK);
} catch (error) {
await promises.mkdir(directory, { recursive: true });
}
};
/**
* Generate a unique ID for a given device, this will also be unique on the same device at a different time.
* @returns {string} A generated unique ID
*/
export function generateUniqueID () {
export const generateUniqueID = () => {
const netInterfaces = networkInterfaces();
let macAddress = '';

View File

@@ -1,3 +1,5 @@
import { getAllPresets } from "./radioPresetHandler.mjs";
import dotenv from 'dotenv';
dotenv.config()
@@ -7,33 +9,43 @@ dotenv.config()
export class ClientNodeObject {
/**
*
* @param {string} param0._id The ID of the node (Assigned by the client on first boot)
* @param {string} param0._nuid The ID of the node (Assigned by the client on first boot)
* @param {string} param0._name The name of the node (Assigned by the user)
* @param {string} param0._location The physical location of the node (Assigned by the user)
* @param {object} param0._nearbySystems An object array of nearby systems (Assigned by the user)
* @param {string} param0._location The physical location of the node (Assigned by the user)
* @param {object} param0._capabilities The capabilities of this node (Assigned by the user, and determined by the hardware)
*/
constructor({ _id = undefined, _name = undefined, _location = undefined, _nearbySystems = undefined}) {
this.id = _id;
constructor({ _nuid = undefined, _name = undefined, _location = undefined, _capabilities = undefined }) {
this.nuid = _nuid;
this.name = _name;
this.location = _location;
this.nearbySystems = _nearbySystems;
this.location = _location;
this.capabilities = _capabilities
}
}
/** The configuration object for the node */
export class ClientNodeConfig {
/**
*
* @param {string} param0._nuid The ID of the node (Assigned by the client on first boot)
* @param {string} param0._name The name of the node (Assigned by the user)
* @param {string} param0._location The physical location of the node (Assigned by the user)
* @param {object} param0._nearbySystems An object array of nearby systems (Assigned by the user)
* @param {object} param0._capabilities The capabilities of this node (Assigned by the user, and determined by the hardware)
*/
constructor({
_id = process.env.CLIENT_ID,
_name = process.env.CLIENT_NAME,
_location = process.env.CLIENT_LOCATION,
_nearbySystems = undefined, // TODO - Get the presets when loading the config instead of having to do it after the fact
_serverIp = process.env.SERVER_IP,
_serverPort = process.env.SERVER_PORT,
_nuid = process.env.CLIENT_NUID,
_name = process.env.CLIENT_NAME,
_location = process.env.CLIENT_LOCATION,
_nearbySystems = getAllPresets(),
_capabilities = process.env.CLIENT_CAPABILITIES.split(", "),
_serverIp = process.env.SERVER_IP,
_serverPort = process.env.SERVER_PORT,
}) {
this.node = new ClientNodeObject({
_id:_id, _name:_name, _location:_location, _nearbySystems:_nearbySystems
_nuid: _nuid, _name: _name, _location: _location, _capabilities: _capabilities
});
this.nearbySystems = _nearbySystems;
this.serverIp = _serverIp;
this.serverPort = _serverPort;
}

View File

@@ -1,22 +1,27 @@
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client", "updatePresets");
// Modules
const fs = require('fs');
const path = require("path");
const converter = require("convert-units");
import { writeFile, existsSync, readFileSync } from 'fs';
import { resolve } from "path";
import { ensureDirectoryExists } from "./baseUtils.mjs";
import convert_units from "convert-units";
const { converter } = convert_units;
import dotenv from 'dotenv';
dotenv.config()
const configFilePath = process.env.CONFIG_PATH;
/**
* Write the given presets to the JSON file
* @param presets The preset or presets to be written
* @param {function} callback The function to be called when this wrapper completes
*/
function writePresets(presets, callback = undefined) {
log.DEBUG(`${__dirname}`);
fs.writeFile("./config/radioPresets.json", JSON.stringify(presets), (err) => {
const writePresets = async (presets, callback = undefined) => {
console.log(`${__dirname}`);
await ensureDirectoryExists(configFilePath);
writeFile(configFilePath, JSON.stringify(presets), (err) => {
// Error checking
if (err) throw err;
log.DEBUG("Write Complete");
console.log("Write Complete");
if (callback) callback(); else return
});
}
@@ -26,14 +31,14 @@ function writePresets(presets, callback = undefined) {
* @param frequenciesArray
* @returns {*[]}
*/
function sanitizeFrequencies(frequenciesArray) {
const sanitizeFrequencies = async (frequenciesArray) => {
let sanitizedFrequencyArray = [];
for (const freq of frequenciesArray) {
sanitizedFrequencyArray.push(convertFrequencyToHertz(freq));
}
log.DEBUG("Sanitized Frequency Array", sanitizedFrequencyArray);
console.log("Sanitized Frequency Array", sanitizedFrequencyArray);
return sanitizedFrequencyArray;
}
@@ -42,23 +47,23 @@ function sanitizeFrequencies(frequenciesArray) {
* @param frequency Could be a string, number or float,
* @returns {number|number|*} Return the value to be saved in Hz format ("154.875"MHz format = "154875000")
*/
function convertFrequencyToHertz(frequency){
const convertFrequencyToHertz = async (frequency) => {
// check if the passed value is a number
if(typeof frequency == 'number' && !isNaN(frequency)){
if (typeof frequency == 'number' && !isNaN(frequency)) {
if (Number.isInteger(frequency)) {
log.DEBUG(`${frequency} is an integer.`);
console.log(`${frequency} is an integer.`);
// Check to see if the frequency has the correct length
if (frequency >= 1000000) return frequency
if (frequency >= 100 && frequency <= 999) return frequency * 1000000
log.WARN("Frequency hasn't matched filters: ", frequency);
console.log("Frequency hasn't matched filters: ", frequency);
}
else {
log.DEBUG(`${frequency} is a float value.`);
console.log(`${frequency} is a float value.`);
// Convert to a string to remove the decimal in place and then correct the length
return parseInt(converter(frequency).from("MHz").to("Hz"));
}
} else {
log.DEBUG(`${frequency} is not a number`);
console.log(`${frequency} is not a number`);
frequency = convertFrequencyToHertz(parseFloat(frequency));
return parseInt(frequency)
@@ -69,10 +74,10 @@ function convertFrequencyToHertz(frequency){
* Gets the saved presets and returns a preset object
* @returns {any} The object containing the different systems the bot is near
*/
function getPresets() {
const presetDir = path.resolve("./config/radioPresets.json");
log.DEBUG(`Getting presets from directory: '${presetDir}'`);
if (fs.existsSync(presetDir)) return JSON.parse(fs.readFileSync(presetDir));
export const getAllPresets = () => {
const presetDir = resolve(configFilePath);
console.log(`Getting presets from directory: '${presetDir}'`);
if (existsSync(presetDir)) return JSON.parse(readFileSync(presetDir));
else return {};
}
@@ -86,15 +91,15 @@ function getPresets() {
* @param {string} trunkFile The file that contains all trunking information (if applicable to the selected listening mode)
* @param {string} whitelistFile The file that contains the whitelisted talkgroups [optional]
*/
function addNewPreset(systemName, frequencies, mode, callback, trunkFile = undefined, whitelistFile = undefined) {
export const addNewPreset = (systemName, frequencies, mode, callback, trunkFile = undefined, whitelistFile = undefined) => {
const presets = this.getPresets();
// Create the preset for the new system
presets[systemName] = {
"frequencies": sanitizeFrequencies(frequencies),
"mode": mode,
"trunkFile": trunkFile ?? "none",
"whitelistFile": whitelistFile ?? "none"
}
"frequencies": sanitizeFrequencies(frequencies),
"mode": mode,
"trunkFile": trunkFile ?? "none",
"whitelistFile": whitelistFile ?? "none"
}
// Write the changes to the preset config file
writePresets(presets, callback);
}
@@ -109,15 +114,15 @@ function addNewPreset(systemName, frequencies, mode, callback, trunkFile = undef
* @param {string} trunkFile The file that contains all trunking information (if applicable to the selected listening mode)
* @param {string} whitelistFile The file that contains the whitelisted talkgroups [optional]
*/
function updatePreset(systemName, callback, { frequencies = undefined, mode = undefined, trunkFile = undefined, whitelistFile = undefined }) {
export const updatePreset = (systemName, callback, { frequencies = undefined, mode = undefined, trunkFile = undefined, whitelistFile = undefined }) => {
const presets = this.getPresets();
// Check if a system name was passed
if (systemName in presets) {
// System name exists, checking to see if the keys are different
if(frequencies && sanitizeFrequencies(frequencies) !== presets[systemName].frequencies) presets[systemName].frequencies = sanitizeFrequencies(frequencies);
if(mode && mode !== presets[systemName].mode) presets[systemName].mode = mode;
if(trunkFile && trunkFile !== presets[systemName].trunkFile || trunkFile === "") presets[systemName].trunkFile = trunkFile ?? "none";
if(whitelistFile && whitelistFile !== presets[systemName].whitelistFile || whitelistFile === "") presets[systemName].whitelistFile = whitelistFile ?? "none";
if (frequencies && sanitizeFrequencies(frequencies) !== presets[systemName].frequencies) presets[systemName].frequencies = sanitizeFrequencies(frequencies);
if (mode && mode !== presets[systemName].mode) presets[systemName].mode = mode;
if (trunkFile && trunkFile !== presets[systemName].trunkFile || trunkFile === "") presets[systemName].trunkFile = trunkFile ?? "none";
if (whitelistFile && whitelistFile !== presets[systemName].whitelistFile || whitelistFile === "") presets[systemName].whitelistFile = whitelistFile ?? "none";
// Write the changes
writePresets(presets, callback);
}
@@ -129,14 +134,11 @@ function updatePreset(systemName, callback, { frequencies = undefined, mode = un
* @param {string} systemName The name of the system being modified
* @param {function} callback The callback function to be called when the function completes
*/
function removePreset(systemName, callback) {
export const removePreset = (systemName, callback) => {
const presets = this.getPresets();
// Check if a system name was passed
if (systemName in presets) {
delete presets[systemName];
writePresets(presets, callback);
}
}
}

View File

@@ -1,34 +1,28 @@
import { io } from "socket.io-client";
import { connectToChannel, initDiscordBotClient, getVoiceChannelFromID } from '../discordAudioBot/dab.mjs';
import { logIntoServerWrapper, sendNodeUpdateWrapper } from "./socketClientWrappers.mjs";
export function initSocketConnection(localNodeConfig) {
export const initSocketConnection = async (localNodeConfig) => {
const serverEndpoint = `http://${localNodeConfig.serverIp}:${localNodeConfig.serverPort}` || 'http://localhost:3000'; // Adjust the server endpoint
const socket = io.connect(serverEndpoint);
return socket;
}
export function initSocketListeners(socket, localNodeConfig) {
socket.on('connect', () => {
socket.on('connect', async () => {
console.log('Connected to the server');
logIntoServer(socket, localNodeConfig.node);
await logIntoServerWrapper(socket, localNodeConfig);
});
socket.on('node-join', async (joinData) => {
console.log("Join requested: ", joinData)
// TODO - Implement logic to control OP25 for the requested channel
// TODO - Implement logic to control OP25 for the requested channel/system
// Join the requested channel with the requested ID
initDiscordBotClient(joinData.clientID, client => {
console.log("Client:", client);
initDiscordBotClient(joinData.clientID, client => {
getVoiceChannelFromID(client, joinData.channelID).then(vc => {
console.log("Voice Channel:", vc);
console.log("Voice Channel Guild:", vc.Guild);
const connection = connectToChannel(vc);
console.log("Bot Connected to VC");
})
});
console.log("All done?");
});
});
socket.on('node-leave', () => {
@@ -39,12 +33,5 @@ export function initSocketListeners(socket, localNodeConfig) {
console.log('Disconnected from the server');
});
}
export function logIntoServer(socket, nodeData) {
socket.emit("node-login", nodeData);
}
export function sendNodeUpdate(socket, nodeData) {
socket.emit('node-update', nodeData);
return socket;
}

View File

@@ -0,0 +1,11 @@
export const logIntoServerWrapper = async (socket, localNodeConfig) => {
socket.emit("node-login", localNodeConfig.node);
sendNodeUpdateWrapper(socket, localNodeConfig);
}
export const sendNodeUpdateWrapper = async (socket, localNodeConfig) => {
socket.emit('node-update', {
'node': localNodeConfig.node,
'nearbySystems': localNodeConfig.nearbySystems
});
}

View File

@@ -17,8 +17,8 @@ class Options {
* @param updatedId The updated ID assigned to the node
*/
export function updateId (updatedId) {
updateConfig('CLIENT_ID', updatedId);
process.env.CLIENT_ID = updatedId;
updateConfig('CLIENT_NUID', updatedId);
process.env.CLIENT_NUID = updatedId;
console.log("Updated ID to: ", updatedId);
}
@@ -41,7 +41,7 @@ export function updateClientConfig (runningConfig, newConfigObject) {
if (configKeys.includes("id")) {
if (runningConfig.id != newConfigObject.id) {
this.updateId(newConfigObject.id);
updatedKeys.push({ 'CLIENT_ID': newConfigObject.id });
updatedKeys.push({ 'CLIENT_NUID': newConfigObject.id });
}
}
if (configKeys.includes("name")) {

164
client/package-lock.json generated
View File

@@ -10,6 +10,7 @@
"license": "ISC",
"dependencies": {
"@discordjs/voice": "^0.16.1",
"convert-units": "^2.3.4",
"discord.js": "^14.14.1",
"dotenv": "^16.3.1",
"libsodium-wrappers": "^0.7.13",
@@ -321,6 +322,15 @@
"resolved": "https://registry.npmjs.org/color-name/-/color-name-1.1.4.tgz",
"integrity": "sha512-dOy+3AuW3a2wNbZHIuMZpTcgjGuLU/uBL/ubcZF9OXbDo8ff4O8yVp5Bf0efS8uEoYo5q4Fx7dY9OgQGXgAsQA=="
},
"node_modules/convert-units": {
"version": "2.3.4",
"resolved": "https://registry.npmjs.org/convert-units/-/convert-units-2.3.4.tgz",
"integrity": "sha512-ERHfdA0UhHJp1IpwE6PnFJx8LqG7B1ZjJ20UvVCmopEnVCfER68Tbe3kvN63dLbYXDA2xFWRE6zd4Wsf0w7POg==",
"dependencies": {
"lodash.foreach": "2.3.x",
"lodash.keys": "2.3.x"
}
},
"node_modules/debug": {
"version": "4.3.4",
"resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz",
@@ -522,11 +532,165 @@
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.17.21.tgz",
"integrity": "sha512-v2kDEe57lecTulaDIuNTPy3Ry4gLGJ6Z1O3vE1krgXZNrsQ+LFTGHVxVjcXPs17LhbZVGedAJv8XZ1tvj5FvSg=="
},
"node_modules/lodash._basebind": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lodash._basebind/-/lodash._basebind-2.3.0.tgz",
"integrity": "sha512-SHqM7YCuJ+BeGTs7lqpWnmdHEeF4MWxS3dksJctHFNxR81FXPOzA4bS5Vs5CpcGTkBpM8FCl+YEbQEblRw8ABg==",
"dependencies": {
"lodash._basecreate": "~2.3.0",
"lodash._setbinddata": "~2.3.0",
"lodash.isobject": "~2.3.0"
}
},
"node_modules/lodash._basecreate": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lodash._basecreate/-/lodash._basecreate-2.3.0.tgz",
"integrity": "sha512-vwZaWldZwS2y9b99D8i9+WtgiZXbHKsBsMrpxJEqTsNW20NhJo5W8PBQkeQO9CmxuqEYn8UkMnfEM2MMT4cVrw==",
"dependencies": {
"lodash._renative": "~2.3.0",
"lodash.isobject": "~2.3.0",
"lodash.noop": "~2.3.0"
}
},
"node_modules/lodash._basecreatecallback": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lodash._basecreatecallback/-/lodash._basecreatecallback-2.3.0.tgz",
"integrity": "sha512-Ev+pDzzfVfgbiucpXijconLGRBar7/+KNCf05kSnk4CmdDVhAy1RdbU9efCJ/o9GXI08JdUGwZ+5QJ3QX3kj0g==",
"dependencies": {
"lodash._setbinddata": "~2.3.0",
"lodash.bind": "~2.3.0",
"lodash.identity": "~2.3.0",
"lodash.support": "~2.3.0"
}
},
"node_modules/lodash._basecreatewrapper": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lodash._basecreatewrapper/-/lodash._basecreatewrapper-2.3.0.tgz",
"integrity": "sha512-YLycQ7k8AB9Wc1EOvLNxuRWcqipDkMXq2GCgnLWQR6qtgTb3gY3LELzEpnFshrEO4LOLs+R2EpcY+uCOZaLQ8Q==",
"dependencies": {
"lodash._basecreate": "~2.3.0",
"lodash._setbinddata": "~2.3.0",
"lodash._slice": "~2.3.0",
"lodash.isobject": "~2.3.0"
}
},
"node_modules/lodash._createwrapper": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lodash._createwrapper/-/lodash._createwrapper-2.3.0.tgz",
"integrity": "sha512-XjaI/rzg9W+WO4WJDQ+PRlHD5sAMJ1RhJLuT65cBxLCb1kIYs4U20jqvTDGAWyVT3c34GYiLd9AreHYuB/8yJA==",
"dependencies": {
"lodash._basebind": "~2.3.0",
"lodash._basecreatewrapper": "~2.3.0",
"lodash.isfunction": "~2.3.0"
}
},
"node_modules/lodash._objecttypes": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lodash._objecttypes/-/lodash._objecttypes-2.3.0.tgz",
"integrity": "sha512-jbA6QyHt9cw3BzvbWzIcnU3Z12jSneT6xBgz3Y782CJsN1tV5aTBKrFo2B4AkeHBNaxSrbPYZZpi1Lwj3xjdtg=="
},
"node_modules/lodash._renative": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lodash._renative/-/lodash._renative-2.3.0.tgz",
"integrity": "sha512-v44MRirqYqZGK/h5UKoVqXWF2L+LUiLTU+Ogu5rHRVWJUA1uWIlHaMpG8f/OA8j++BzPMQij9+erXHtgFcbuwg=="
},
"node_modules/lodash._setbinddata": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lodash._setbinddata/-/lodash._setbinddata-2.3.0.tgz",
"integrity": "sha512-xMFfbF7dL+sFtrdE49uHFmfpBAEwlFtfgMp86nQRlAF6aizYL+3MTbnYMKJSkP1W501PhsgiBED5kBbZd8kR2g==",
"dependencies": {
"lodash._renative": "~2.3.0",
"lodash.noop": "~2.3.0"
}
},
"node_modules/lodash._shimkeys": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lodash._shimkeys/-/lodash._shimkeys-2.3.0.tgz",
"integrity": "sha512-9Iuyi7TiWMGa/9+2rqEE+Zwye4b/U2w7Saw6UX1h6Xs88mEER+uz9FZcEBPKMVKsad9Pw5GNAcIBRnW2jNpneQ==",
"dependencies": {
"lodash._objecttypes": "~2.3.0"
}
},
"node_modules/lodash._slice": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lodash._slice/-/lodash._slice-2.3.0.tgz",
"integrity": "sha512-7C61GhzRUv36gTafr+RIb+AomCAYsSATEoK4OP0VkNBcwvsM022Z22AVgqjjzikeNO1U29LzsJZDvLbiNPUYvA=="
},
"node_modules/lodash.bind": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lodash.bind/-/lodash.bind-2.3.0.tgz",
"integrity": "sha512-goakyOo+FMN8lttMPnZ0UNlr5RlzX4IrUXyTJPT2A0tGCMXySupond9wzvDqTvVmYTcQjIKGrj8naJDS2xWAlQ==",
"dependencies": {
"lodash._createwrapper": "~2.3.0",
"lodash._renative": "~2.3.0",
"lodash._slice": "~2.3.0"
}
},
"node_modules/lodash.foreach": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lodash.foreach/-/lodash.foreach-2.3.0.tgz",
"integrity": "sha512-yLnyptVRJd0//AbGp480grgQG9iaDIV5uOgSbpurRy1dYybPbjNTLQ3FyLEQ84buVLPG7jyaiyvpzgfOutRB3Q==",
"dependencies": {
"lodash._basecreatecallback": "~2.3.0",
"lodash.forown": "~2.3.0"
}
},
"node_modules/lodash.forown": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lodash.forown/-/lodash.forown-2.3.0.tgz",
"integrity": "sha512-dUnCsuQTtq3Y7bxPNoEEqjJjPL2ftLtcz2PTeRKvhbpdM514AvnqCjewHGsm/W+dwspIwa14KoWEZeizJ7smxA==",
"dependencies": {
"lodash._basecreatecallback": "~2.3.0",
"lodash._objecttypes": "~2.3.0",
"lodash.keys": "~2.3.0"
}
},
"node_modules/lodash.identity": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lodash.identity/-/lodash.identity-2.3.0.tgz",
"integrity": "sha512-NYJ2r2cwy3tkx/saqbIZEX6oQUzjWTnGRu7d/zmBjMCZos3eHBxCpbvWFWSetv8jFVrptsp6EbWjzNgBKhUoOA=="
},
"node_modules/lodash.isfunction": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lodash.isfunction/-/lodash.isfunction-2.3.0.tgz",
"integrity": "sha512-X5lteBYlCrVO7Qc00fxP8W90fzRp6Ax9XcHANmU3OsZHdSyIVZ9ZlX5QTTpRq8aGY+9I5Rmd0UTzTIIyWPugEQ=="
},
"node_modules/lodash.isobject": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lodash.isobject/-/lodash.isobject-2.3.0.tgz",
"integrity": "sha512-jo1pfV61C4TE8BfEzqaHj6EIKiSkFANJrB6yscwuCJMSRw5tbqjk4Gv7nJzk4Z6nFKobZjGZ8Qd41vmnwgeQqQ==",
"dependencies": {
"lodash._objecttypes": "~2.3.0"
}
},
"node_modules/lodash.keys": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lodash.keys/-/lodash.keys-2.3.0.tgz",
"integrity": "sha512-c0UW0ffqMxSCtoVbmVt2lERJLkEqgoOn2ejPsWXzr0ZrqRbl3uruGgwHzhtqXxi6K/ei3Ey7zimOqSwXgzazPg==",
"dependencies": {
"lodash._renative": "~2.3.0",
"lodash._shimkeys": "~2.3.0",
"lodash.isobject": "~2.3.0"
}
},
"node_modules/lodash.noop": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lodash.noop/-/lodash.noop-2.3.0.tgz",
"integrity": "sha512-NpSm8HRm1WkBBWHUveDukLF4Kfb5P5E3fjHc9Qre9A11nNubozLWD2wH3UBTZbu+KSuX8aSUvy9b+PUyEceJ8g=="
},
"node_modules/lodash.snakecase": {
"version": "4.1.1",
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="
},
"node_modules/lodash.support": {
"version": "2.3.0",
"resolved": "https://registry.npmjs.org/lodash.support/-/lodash.support-2.3.0.tgz",
"integrity": "sha512-etc7VWbB0U3Iya8ixj2xy4sDBN3jvPX7ODi8iXtn4KkkjNpdngrdc7Vlt5jub/Vgqx6/dWtp7Ml9awhCQPYKGQ==",
"dependencies": {
"lodash._renative": "~2.3.0"
}
},
"node_modules/magic-bytes.js": {
"version": "1.7.0",
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.7.0.tgz",

View File

@@ -12,6 +12,7 @@
"type": "module",
"dependencies": {
"@discordjs/voice": "^0.16.1",
"convert-units": "^2.3.4",
"discord.js": "^14.14.1",
"dotenv": "^16.3.1",
"libsodium-wrappers": "^0.7.13",