48 Commits

Author SHA1 Message Date
Logan Cusano
96b0bf6adb Initial push for #17
- Partial functionality
- No pagination
- Only 9 results
2023-06-16 21:53:54 -04:00
Logan Cusano
f5e119d845 Bugfixes and functional #7 & #9
- #7 needs to error check more
- both need to be cleaned up
2023-06-11 04:40:40 -04:00
Logan Cusano
e8d68b2da7 Initial #7 & #9
- Working commands
- Keeps track of open connections
2023-06-10 22:16:39 -04:00
Logan Cusano
041e0d485d Fix error status in client join 2023-06-10 20:46:43 -04:00
Logan Cusano
fc11324714 Add function to get all client IDs from JSON file #7 2023-06-04 00:24:50 -04:00
Logan Cusano
c6c048c919 Update default command with autocomplete 2023-06-03 23:35:07 -04:00
Logan Cusano
8ab611836b Allow commands to use autocomplete 2023-06-03 23:31:27 -04:00
7d8ad68e27 Merge pull request 'Add join command to server #7' (#15) from Add-join-command-to-server-#7 into master
Reviewed-on: #15
2023-06-03 23:05:39 -04:00
200ca9c926 Merge branch 'master' into Add-join-command-to-server-#7 2023-06-03 23:05:12 -04:00
Logan Cusano
ff8e86cc3a Updated client setup script
- Create a copy of the .env example
- Updated the installed packages to allow for installation
2023-06-03 23:02:41 -04:00
Logan Cusano
6b12c3e3df Remove unused keys from example .env file 2023-06-03 23:01:47 -04:00
Logan Cusano
fa2f28207e wrapping up join command
- API untested
2023-06-03 23:00:50 -04:00
Logan Cusano
5c8414b4d8 Moved to issues in git 2023-06-03 22:58:31 -04:00
Logan Cusano
edaeb261f7 Add additional info on connection status to nodeObject 2023-06-03 19:00:16 -04:00
Logan Cusano
c31ccff5ca Added JSDoc to Join wrapper and updated wrapper to also take just a client ID string 2023-06-03 16:03:07 -04:00
Logan Cusano
d2186e9471 Added a join command #7
- Added a JSON example for Known Client IDs
- Implemented a custom slash command builder to add the available presets as options in the discord command
2023-06-03 15:47:07 -04:00
Logan Cusano
07743cf8a3 Updated requirements and versions 2023-06-03 15:43:15 -04:00
Logan Cusano
18afa7c058 Added extra logging when deploying commands 2023-06-03 15:42:40 -04:00
Logan Cusano
a5cff9ec7e Check if the returned data from HTTP is valid JSON and parse if so, return the string if not 2023-06-03 15:42:23 -04:00
Logan Cusano
9450b78bd4 Updated all functions to return nodeObjects instead of SQL response 2023-06-03 15:41:29 -04:00
Logan Cusano
5757c51fa3 Added new utils
- isJsonString
    - This can be used to check if a string is valid json before parsing it
- getMembersInRole
    - This can be used to check online/offline/all members in a role
- (unused) SanitizePresetName
2023-06-03 15:40:48 -04:00
Logan Cusano
fa91cbc887 Used env var for the listening port 2023-06-03 15:39:16 -04:00
Logan Cusano
7fbaf31335 Updated server intents 2023-06-03 15:38:40 -04:00
Logan Cusano
0280cb5384 Update gitignore 2023-06-03 15:38:24 -04:00
Logan Cusano
a298be40d5 Accidentally set the wrong variable for the device ID 2023-06-03 15:32:08 -04:00
Logan Cusano
43d60a748b Remove device ID requirement for API
- The device ID is handled by the .env file
2023-06-03 15:28:46 -04:00
Logan Cusano
51f517cae5 Fixed node '^=' to python '>=' 2023-06-03 15:03:18 -04:00
Logan Cusano
06cb2cc352 Fix logging namespace and windows launch 2023-06-03 15:00:42 -04:00
Logan Cusano
5ce525f2b5 Updating install script #6 2023-06-03 02:51:19 -04:00
Logan Cusano
69fdc63983 Fixing linux bug, added noisegate switch to the call 2023-05-27 21:34:19 -04:00
Logan Cusano
a9d3c33af2 Improved client bot logging 2023-05-27 21:17:31 -04:00
Logan Cusano
3719fce86a Update client package.json 2023-05-27 18:25:12 -04:00
Logan Cusano
ba927bae8c Implement install and update system for the bot
- LINUX OR WINDOW WSL ONLY
2023-05-27 17:00:57 -04:00
79fe542143 Merge pull request 'Implement Python Discord Bot to Handle Voice Connection' (#5) from feature/implement-discordpy into master
Reviewed-on: #5
2023-05-27 16:19:38 -04:00
Logan Cusano
7512c8e1df Removing the static ENV var for nearby systems 2023-05-21 15:22:43 -04:00
Logan Cusano
c882fb63d3 Moving PDAB to pdab 2023-05-20 15:24:28 -04:00
Logan Cusano
7fc61bbf2e renaming PDAB to pdab to try to fix git issues 2023-05-20 15:20:53 -04:00
Logan Cusano
e1c2ce6484 Updating and streamlining radio controller side 2023-05-20 15:18:50 -04:00
Logan Cusano
c4070cc420 Initial working radio controller for OP25 2023-05-20 14:31:43 -04:00
Logan Cusano
0f003f907e Discord voice bot handler working 2023-05-20 00:01:12 -04:00
Logan Cusano
e7b802839e Initial removal of internal discord bot 2023-05-18 22:53:25 -04:00
Logan Cusano
48999e0d63 Resolved bug in Client with .env config migration 2023-05-07 19:31:53 -04:00
Logan Cusano
2c25be1de7 Removed Embedded discord 2023-05-07 19:31:33 -04:00
Logan Cusano
cf04e37f89 Implement example .env files 2023-05-07 04:48:19 -04:00
Logan Cusano
d04cc8d5b1 Update gitignore for .env files 2023-05-07 04:43:46 -04:00
Logan Cusano
4662f37a72 Removing real .env files 2023-05-07 04:43:00 -04:00
Logan Cusano
be34c5381b Removing real .envs 2023-05-07 04:42:05 -04:00
Logan Cusano
ed79403a9b Remove config file for client, moved to .env 2023-05-07 04:40:46 -04:00
71 changed files with 2440 additions and 5468 deletions

4
.gitignore vendored
View File

@@ -5,4 +5,6 @@ node_modules/
# Development files # Development files
*.log *.log
*.txt *.txt
*.env
!requirements.txt
*testOP25Dir/

View File

@@ -1,8 +0,0 @@
DEBUG="client:*"
TOKEN=""
# Bot Config
APPLICATION_ID=""
GUILD_ID=""
# Audio Config
AUDIO_DEVICE_ID="1"
AUDIO_DEVICE_NAME="VoiceMeeter VAIO3 Output (VB-Au"

20
Client/.env.example Normal file
View File

@@ -0,0 +1,20 @@
DEBUG="client:*"
# Audio Config
AUDIO_DEVICE_ID=""
AUDIO_DEVICE_NAME=""
# Client Config
CLIENT_ID=0
CLIENT_NAME=""
CLIENT_IP=""
CLIENT_PORT=3010
CLIENT_LOCATION=""
CLIENT_ONLINE=true
# Configuration for the connection to the server
SERVER_IP=""
SERVER_HOSTNAME=""
SERVER_PORT=3000
# Configuration of the local OP25 application
OP25_BIN_PATH=""

View File

@@ -5,39 +5,20 @@ var cookieParser = require('cookie-parser');
var logger = require('morgan'); var logger = require('morgan');
var http = require('http'); var http = require('http');
require('dotenv').config(); require('dotenv').config();
const fs = require('fs');
const { DebugBuilder } = require("./utilities/debugBuilder"); const { DebugBuilder } = require("./utilities/debugBuilder");
const deployCommands = require('./utilities/deployCommands');
const { checkIn } = require("./controllers/clientController"); const { checkIn } = require("./controllers/clientController");
var indexRouter = require('./routes/index'); var indexRouter = require('./routes/index');
var botRouter = require('./routes/bot'); var botRouter = require('./routes/bot');
var clientRouter = require('./routes/client'); var clientRouter = require('./routes/client');
var radioRouter = require('./routes/radio'); var radioRouter = require('./routes/radio');
var { attachRadioSessionToRequest } = require('./controllers/radioController');
const log = new DebugBuilder("client", "app"); const log = new DebugBuilder("client", "app");
const {
Client,
Events,
Collection,
GatewayIntentBits,
MessageActionRow,
MessageButton
} = require('discord.js');
var app = express(); var app = express();
var discordToken = process.env.TOKEN;
var port = process.env.HTTP_PORT || '3010'; var port = process.env.HTTP_PORT || '3010';
const discordClient = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildVoiceStates
]
});
// view engine setup // view engine setup
app.set('views', path.join(__dirname, 'views')); app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs'); app.set('view engine', 'ejs');
@@ -53,16 +34,13 @@ app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter); app.use('/', indexRouter);
// Discord bot control route // Discord bot control route
app.use('/bot', (req, res, next) => { app.use('/bot', attachRadioSessionToRequest, botRouter);
req.discordClient = discordClient; // Add the discord client to bot requests to be used downstream
next();
}, botRouter);
// Local client control route // Local client control route
app.use("/client", clientRouter); app.use("/client", clientRouter);
// Local radio controller route // Local radio controller route
app.use("/radio", radioRouter); app.use("/radio", attachRadioSessionToRequest, radioRouter);
// catch 404 and forward to error handler // catch 404 and forward to error handler
app.use((req, res, next) => { app.use((req, res, next) => {
@@ -116,54 +94,8 @@ async function runHTTPServer() {
}) })
} }
log.DEBUG(`Starting HTTP Server`);
// Discord bot config
// Setup commands for the Discord bot
discordClient.commands = new Collection();
const commandsPath = path.join(__dirname, 'commands');
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
//const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));
for (const file of commandFiles) {
const filePath = path.join(commandsPath, file);
const command = require(filePath);
log.DEBUG("Importing command: ", command.data.name);
// Set a new item in the Collection
// With the key as the command name and the value as the exported module
discordClient.commands.set(command.data.name, command);
}
// Run when the bot is ready
discordClient.on('ready', () => {
log.DEBUG(`Discord server up and running with client: ${discordClient.user.tag}`);
log.INFO(`Logged in as ${discordClient.user.tag}!`);
// Deploy slash commands
log.DEBUG("Deploying slash commands");
deployCommands.deploy(discordClient.user.id, discordClient.guilds.cache.map(guild => guild.id));
log.DEBUG(`Starting HTTP Server`);
runHTTPServer(); runHTTPServer();
log.DEBUG("Checking in with the master server") log.DEBUG("Checking in with the master server")
checkIn(); checkIn();
});
// Setup any additional event handlers
const eventsPath = path.join(__dirname, 'events');
if (fs.existsSync(eventsPath)) {
const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js'));
if (eventFiles.length > 0) {
for (const file of eventFiles) {
const filePath = path.join(eventsPath, file);
const event = require(filePath);
if (event.once) {
discordClient.once(event.name, (...args) => event.execute(...args));
} else {
discordClient.on(event.name, (...args) => event.execute(...args));
}
}
}
}
discordClient.login(discordToken); //Load Client Discord Token

View File

@@ -1,76 +0,0 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
const app = require('../app');
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client", "www");
const http = require('http');
const config = require('../config/clientConfig');
const clientController = require('../controllers/clientController');
/**
* Get port from environment and store in Express.
*/
app.set('port', config.clientConfig.port);
/**
* Create HTTP server.
*/
const server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(config.clientConfig.port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
const bind = typeof port === 'string'
? 'Pipe ' + port
: 'Port ' + port;
// handle specific listen errors with friendly messages
switch (error.code) {
case 'EACCES':
console.error(bind + ' requires elevated privileges');
process.exit(1);
break;
case 'EADDRINUSE':
console.error(bind + ' is already in use');
process.exit(1);
break;
default:
throw error;
}
}
/**
* Event listener for HTTP server "listening" event.
*/
function onListening() {
const addr = server.address();
const bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
log.DEBUG('Listening on ' + bind);
// check in with the server to add this node or come back online
clientController.checkIn();
}

View File

@@ -1,17 +0,0 @@
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("client", "ping");
// Modules
const { SlashCommandBuilder } = require('discord.js');
const { join } = require("../controllers/commandController")
module.exports = {
data: new SlashCommandBuilder()
.setName('join')
.setDescription('Join a voice channel'),
example: "join",
isPrivileged: false,
async execute(interaction) {
await join({ interaction: interaction });
}
}

View File

@@ -1,17 +0,0 @@
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client", "leave");
// Modules
const { SlashCommandBuilder } = require('discord.js');
const { leave } = require("../controllers/commandController")
module.exports = {
data: new SlashCommandBuilder()
.setName('leave')
.setDescription('Leave a voice channel'),
example: "leave",
isPrivileged: false,
async execute(interaction) {
await leave({ interaction: interaction })
}
}

View File

@@ -1,28 +0,0 @@
// Utilities
const { replyToInteraction } = require('../utilities/messageHandler.js');
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("client", "ping");
module.exports = {
data: new SlashCommandBuilder()
.setName('ping')
.setDescription('Replies with your input!'),
/*
.addStringOption(option =>
option.setName('input')
.setDescription('The input to echo back')
.setRequired(false)
.addChoices()),
*/
example: "ping",
isPrivileged: false,
async execute(interaction) {
try{
await replyToInteraction(interaction, "Pong! I have Aids and now you do too!"); // TODO - Add insults as the response to this command
}catch(err){
log.ERROR(err)
//await interaction.reply(err.toString());
}
}
};

View File

@@ -1,19 +0,0 @@
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client", "status");
// Modules
const { status } = require('../controllers/commandController');
// Utilities
const { SlashCommandBuilder } = require('discord.js');
module.exports = {
data: new SlashCommandBuilder()
.setName('status')
.setDescription('Check the status of the bot'),
example: "status",
isPrivileged: false,
async execute(interaction) {
await status({ interaction: interaction });
}
}

View File

@@ -1,23 +0,0 @@
// Core config settings for the node, these are the settings that are checked with the server
const path = require("path");
exports.clientConfig = {
"id": 13,
"name": "boilin balls in the hall",
"ip": "172.16.100.150",
"port": 3010,
"location": "the house",
"nearbySystems": ["Westchester Cty. Simulcast"],
"online": true
}
// Configuration for the connection to the server
exports.serverConfig = {
"ip": "172.16.100.108",
"hostname": "localhost",
"port": 3000
}
// Configuration of the local OP25 application
exports.radioAppConfig = {
"bin": "H:/Logan/Projects/Discord-Radio-Bot-CnC/Client/.idea/testOP25Dir/multi_rx.py"
}

View File

@@ -1,16 +0,0 @@
// Core config settings for the node, these are the settings that are checked with the server
exports.nodeConfig = {
"id": 0,
"name": "",
"ip": "",
"port": 0,
"location": "",
"nearbySystems": {
"System Name": {
"frequencies": [],
"mode": "",
"trunkFile": ""
}
},
"online": false
}

View File

@@ -1 +1 @@
{"Westchester Cty. Simulcast":{"frequencies":[470575000,470375000,470525000,470575000,470550000],"mode":"p25","trunkFile":"trunk.tsv"},"coppies":{"frequencies":[154875000],"mode":"nbfm","trunkFile":"none"}} {"Westchester Cty. Simulcast":{"frequencies":[470575000,470375000,470525000,470575000,470550000],"mode":"p25","trunkFile":"trunk.tsv"},"coppies":{"frequencies":[154690000],"mode":"nbfm","trunkFile":"none"},"poopoo":{"frequencies":[479135500],"mode":"nbfm","trunkFile":"none"},"ppeeeeeeeeee":{"frequencies":[479135500,133990000,133000000,555999000],"mode":"p25","trunkFile":"none"}}

View File

@@ -1,88 +0,0 @@
// Config
const { getDeviceID } = require('../utilities/configHandler.js');
// Modules
const portAudio = require('naudiodon');
const { returnAlsaDeviceObject } = require("../utilities/executeConsoleCommands.js");
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder.js");
// Global Vars
const log = new DebugBuilder("client", "audioController");
/**
* Checks to make sure the selected audio device is available and returns the device object (PortAudio Device Info)
* At least one option must be supplied, it will prefer ID to device name
*
* @param deviceName The name of the device being queried
* @param deviceId The ID of the device being queried
* @returns {unknown}
*/
async function confirmAudioDevice({deviceName = undefined, deviceId = undefined}){
const deviceList = await getAudioDevices();
if (!deviceName && !deviceId) throw new Error("No device given");
let confirmedDevice;
if (deviceId) confirmedDevice = deviceList.find(device => device.id === deviceId);
if (deviceName) confirmedDevice = deviceList.find(device => device.name === deviceName);
log.DEBUG("Confirmed Audio Device: ", confirmedDevice);
return confirmedDevice;
}
exports.confirmAudioDevice = confirmAudioDevice;
/**
* Return a list of the audio devices connected with input channels
*
* @returns {unknown[]}
*/
async function getAudioDevices(){
// Exec output contains both stderr and stdout outputs
//const deviceList = await returnAlsaDeviceObject();
const deviceList = portAudio.getDevices().map((device) => {
if (device.maxInputChannels > 2) {
return device;
}
else {
return null;
}
}).filter(Boolean);
log.VERBOSE("Device List: ", deviceList);
return deviceList;
}
exports.getAudioDevices = getAudioDevices;
/**
* Create and return the audio instance from the saved settings
* TODO Allow the client to save and load these settings dynamically
*
* @returns new portAudio.AudioIO
*/
async function createAudioInstance() {
const selectedDevice = await confirmAudioDevice({deviceId: getDeviceID()});//{deviceName: "VoiceMeeter VAIO3 Output (VB-Au"});
log.DEBUG("Device selected from config: ", selectedDevice);
// Create an instance of AudioIO with outOptions (defaults are as below), which will return a WritableStream
return new portAudio.AudioIO({
inOptions: {
channelCount: 2,
sampleFormat: portAudio.SampleFormat16Bit,
sampleRate: 48000,
deviceId: selectedDevice.id, // Use -1 or omit the deviceId to select the default device
closeOnError: false, // Close the stream if an audio error is detected, if set false then just log the error
framesPerBuffer: 20, //(48000 / 1000) * 20, //(48000 * 16 * 2) / 1000 * 20 // (48000 * (16 / 8) * 2) / 60 / 1000 * 20 //0.025 * 48000 / 2
highwaterMark: 3840,
},
});
/*
return new alsaInstance({
channels: 2,
format: "U8",
rate: 48000,
device: selectedDevice.name ?? "default", // Omit the deviceId to select the default device
periodSize: 100, //(48000 / 1000) * 20, //(48000 * 16 * 2) / 1000 * 20 // (48000 * (16 / 8) * 2) / 60 / 1000 * 20 //0.025 * 48000 / 2
periodTime: undefined,
// highwaterMark: 3840
});
*/
}
exports.createAudioInstance = createAudioInstance;

View File

@@ -1,98 +1,84 @@
// Debug // Debug
const { DebugBuilder } = require("../utilities/debugBuilder.js"); const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client", "clientController"); const log = new DebugBuilder("client", "botController");
const botLog = new DebugBuilder("client", "botController:bot");
// Modules // Modules
const { status, join, leave } = require("./commandController") const spawn = require('child_process').spawn;
const { resolve } = require("path");
require('dotenv').config();
const { closeProcessWrapper } = require("../utilities/utilities");
/** // Global vars
* Get an object of client guilds let pythonProcess;
* @param req The express request which includes the discord client
* @returns
*/
function getGuilds(req) {
return req.discordClient.guilds.cache.map(guild => guild.id)
}
/**
* Get an object of the channels in a guild
* @param {*} guildId The Guild ID to check the channels of
* @param {*} req The request object to use to check the discord client
*/
function getChannels(guildId, req) {
const guild = req.discordClient.guilds.find(guildId);
log.DEBUG("Found Guild channels with guild", guild.channels, guild);
return guild.channels;
}
/**
* Check to see if a given guild has a given channel
* @param {*} guildId The guild ID to check if the channel exists
* @param {*} channelId The channel ID to check if exists in the guild
* @param {*} req The express request param to use the discord client
* @returns {true|false}
*/
function checkIfGuildHasChannel(guildId, channelId, req){
const guildChannels = getChannels(guildId, req)
const checkedChannel = guildChannels.find(c => c.id === channelId);
if (!checkedChannel) return false;
return true;
}
function getGuildFromChannel(channelId, req){
const channel = req.discordClient.channels.cache.get(channelId);
if (!channel) return new Error("Error getting channel from client");
if (channel.guild) return channel.guild;
return new Error("No Guild found with the given ID");
}
/** /**
* Get Status of the discord process * Get Status of the discord process
*/ */
exports.getStatus = (req, res) => { exports.getStatus = (req, res) => {
log.INFO("Getting the status of the bot"); log.INFO("Getting the status of the bot");
guildIds = getGuilds(req); if (pythonProcess) return res.sendStatus(200);
log.DEBUG("Guild IDs: ", guildIds); return res.sendStatus(201);
var guildStatuses = []
for (const guildId of guildIds){
status({guildID: guildId, callback: (statusObj) => {
log.DEBUG("Status Object string: ", statusObj);
guildStatuses.push(statusObj);
}});
}
return res.status(200).json(guildStatuses);
} }
/** /**
* Start the bot and join the server and preset specified * Start the bot and join the server and preset specified
*/ */
exports.joinServer = (req, res) => { exports.joinServer = async (req, res) => {
const channelId = req.body.channelID; if (!req.body.clientId || !req.body.channelId) return res.status(500).json({"message": "You must include the client ID (discord token), channel ID (The discord ID of the channel to connect to)"});
const deviceId = process.env.AUDIO_DEVICE_ID;
const channelId = req.body.channelId;
const clientId = req.body.clientId;
const presetName = req.body.presetName; const presetName = req.body.presetName;
const guildObj = getGuildFromChannel(channelId, req); const NGThreshold = req.body.NGThreshold ?? 50
if (!channelId || !presetName || !guildObj) return res.status(400).json({'message': "Request does not have all components to proceed"}); // Joining the discord server
log.INFO("Join requested to: ", deviceId, channelId, clientId, presetName, NGThreshold);
// join the sever if (process.platform === "win32") {
join({guildID: guildObj.id, guildObj: guildObj, channelID: channelId, callback: () => { log.DEBUG("Starting Windows Python");
return res.sendStatus(202); pythonProcess = await spawn('python.exe', [resolve(__dirname, "../pdab/main.py"), deviceId, channelId, clientId, '-n', NGThreshold], { cwd: resolve(__dirname, "../pdab/").toString() });
}}); //pythonProcess = await spawn('C:\\Python310\\python.exe', [resolve(__dirname, "../PDAB/main.py"), deviceId, channelId, clientId, NGThreshold ]);
}
else {
log.DEBUG("Starting Linux Python");
pythonProcess = await spawn('python3', [resolve(__dirname, "../pdab/main.py"), deviceId, channelId, clientId,'-n', NGThreshold ], { cwd: resolve(__dirname, "../pdab/") });
}
log.VERBOSE("Python Process: ", pythonProcess);
let fullOutput;
pythonProcess.stdout.setEncoding('utf8');
pythonProcess.stdout.on("data", (data) => {
botLog.VERBOSE("From Process: ", data);
fullOutput += data.toString();
});
pythonProcess.stderr.on('data', (data) => {
botLog.VERBOSE(`stderr: ${data}`);
fullOutput += data.toString();
});
pythonProcess.on('close', (code) => {
log.DEBUG(`child process exited with code ${code}`);
log.VERBOSE("Full output from bot: ", fullOutput);
});
pythonProcess.on("error", (code, signal) => {
log.ERROR("Error from the discord bot process: ", code, signal);
});
// Starting the radio application
return res.sendStatus(200);
} }
/** /**
* Leaves the server if it's in one * Leaves the server if it's in one
*/ */
exports.leaveServer = (req, res) => { exports.leaveServer = async (req, res) => {
log.INFO("Leaving the server"); log.INFO("Leaving the server");
const guildIds = getGuilds(req); if (!pythonProcess) return res.sendStatus(200)
log.DEBUG("Guild IDs: ", guildIds);
for (const guildId of guildIds){ pythonProcess = await closeProcessWrapper(pythonProcess);
leave({guildID: guildId, callback: (response) => {
log.DEBUG("Response from leaving server on guild ID", guildId, response);
}});
}
return res.sendStatus(202); return res.sendStatus(202);
} }

View File

@@ -2,14 +2,16 @@
const { DebugBuilder } = require("../utilities/debugBuilder.js"); const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client", "clientController"); const log = new DebugBuilder("client", "clientController");
// Configs // Configs
const config = require("../config/clientConfig"); require('dotenv').config();
const modes = require("../config/modes"); const modes = require("../config/modes");
// Modules // Modules
const { executeAsyncConsoleCommand } = require("../utilities/executeConsoleCommands.js"); const { executeAsyncConsoleCommand, nodeObject } = require("../utilities/utilities");
// Utilities // Utilities
const { updateId, updateConfig } = require("../utilities/updateConfig"); const { updateId, updateConfig } = require("../utilities/updateConfig");
const updatePreset = require("../utilities/updatePresets"); const { updatePreset, addNewPreset, getPresets, removePreset } = require("../utilities/updatePresets");
const requests = require("../utilities/httpRequests"); const { onHttpError, requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
var runningClientConfig = new nodeObject({_id: process.env.CLIENT_ID, _ip: process.env.CLIENT_IP, _name: process.env.CLIENT_NAME, _port: process.env.CLIENT_PORT, _location: process.env.CLIENT_LOCATION, _nearbySystems: getPresets(), _online: process.env.CLIENT_ONLINE});
/** /**
* Check the body for the required fields to update or add a preset * Check the body for the required fields to update or add a preset
@@ -31,8 +33,7 @@ function checkBodyForPresetFields(req, res, callback) {
return callback(); return callback();
} }
async function checkLocalIP() { async function checkLocalIP() {
let ipAddr;
if (process.platform === "win32") { if (process.platform === "win32") {
// Windows // Windows
var networkConfig = await executeAsyncConsoleCommand("ipconfig"); var networkConfig = await executeAsyncConsoleCommand("ipconfig");
@@ -66,23 +67,23 @@ async function checkLocalIP() {
* Checks the config file for all required fields or gets and updates the required fields * Checks the config file for all required fields or gets and updates the required fields
*/ */
exports.checkConfig = async function checkConfig() { exports.checkConfig = async function checkConfig() {
if (!config.clientConfig.ip) { if (!runningClientConfig.ip) {
const ipAddr = await checkLocalIP(); const ipAddr = await checkLocalIP();
updateConfig('ip', ipAddr); updateConfig('ip', ipAddr);
config.clientConfig.ip = ipAddr; runningClientConfig.ip = ipAddr;
} }
if(!config.clientConfig.name) { if(!runningClientConfig.name) {
const lastOctet = await String(checkLocalIP()).spit('.')[-1]; const lastOctet = await String(checkLocalIP()).spit('.')[-1];
const clientName = `Radio-Node-${lastOctet}`; const name = `Radio-Node-${lastOctet}`;
updateConfig('name', clientName); updateConfig('name', name);
config.clientConfig.name = clientName; runningClientConfig.name = name;
} }
if(!config.clientConfig.port) { if(!runningClientConfig.port) {
const port = 3010; const port = 3010;
updateConfig('port', port); updateConfig('port', port);
config.clientConfig.port = port; runningClientConfig.port = port;
} }
} }
@@ -95,28 +96,43 @@ exports.checkIn = async () => {
let reqOptions; let reqOptions;
await this.checkConfig(); await this.checkConfig();
// Check if there is an ID found, if not add the node to the server. If there was an ID, check in with the server to make sure it has the correct information // Check if there is an ID found, if not add the node to the server. If there was an ID, check in with the server to make sure it has the correct information
if (config.clientConfig.id === 0) { try {
// ID was not found in the config, creating a new node if (runningClientConfig.id === 0) {
reqOptions = new requests.requestOptions("/nodes/newNode", "POST"); // ID was not found in the config, creating a new node
requests.sendHttpRequest(reqOptions, JSON.stringify(config.clientConfig), (responseObject) => { reqOptions = new requestOptions("/nodes/newNode", "POST");
// Update the client's ID if the server accepted it sendHttpRequest(reqOptions, JSON.stringify(), (responseObject) => {
if (responseObject.statusCode === 202) { // Update the client's ID if the server accepted it
config.clientConfig.id = responseObject.body.nodeId; if (responseObject.statusCode === 202) {
updateId(responseObject.body.nodeId); runningClientConfig.id = responseObject.body.nodeId;
} updateId(responseObject.body.nodeId);
}); }
if (responseObject.statusCode >= 300) {
// Server threw an error
onHttpError(responseObject.statusCode);
}
});
}
else {
// ID is in the config, checking in with the server
reqOptions = new requestOptions("/nodes/nodeCheckIn", "POST");
sendHttpRequest(reqOptions, JSON.stringify(runningClientConfig), (responseObject) => {
if (responseObject.statusCode === 202) {
// Server accepted an update
}
if (responseObject.statusCode === 200) {
// Server accepted the response but there were no keys to be updated
}
if (responseObject.statusCode >= 300) {
// Server threw an error
onHttpError(responseObject.statusCode);
}
});
}
} }
else { catch (err) {
// ID is in the config, checking in with the server log.ERROR("Error checking in: ", err);
reqOptions = new requests.requestOptions("/nodes/nodeCheckIn", "POST");
requests.sendHttpRequest(reqOptions, JSON.stringify(config.clientConfig), (responseObject) => {
if (responseObject.statusCode === 202) {
// Server accepted an update
}
if (responseObject.statusCode === 200) {
// Server accepted the response but there were no keys to be updated
}
});
} }
} }
@@ -132,7 +148,7 @@ exports.requestCheckIn = async (req, res) => {
* This is the endpoint wrapper to get the presets object * This is the endpoint wrapper to get the presets object
*/ */
exports.getPresets = async (req, res) => { exports.getPresets = async (req, res) => {
return res.status(200).json(updatePreset.getPresets()); return res.status(200).json(getPresets());
} }
/** Controller for the /client/updatePreset endpoint /** Controller for the /client/updatePreset endpoint
@@ -140,7 +156,7 @@ exports.getPresets = async (req, res) => {
*/ */
exports.updatePreset = async (req, res) => { exports.updatePreset = async (req, res) => {
checkBodyForPresetFields(req, res, () => { checkBodyForPresetFields(req, res, () => {
updatePreset.updatePreset(req.body.systemName, () => { updatePreset(req.body.systemName, () => {
return res.sendStatus(200); return res.sendStatus(200);
}, {frequencies: req.body.frequencies, mode: req.body.mode, trunkFile: req.body.trunkFile}); }, {frequencies: req.body.frequencies, mode: req.body.mode, trunkFile: req.body.trunkFile});
}) })
@@ -151,10 +167,28 @@ exports.updatePreset = async (req, res) => {
*/ */
exports.addNewPreset = async (req, res) => { exports.addNewPreset = async (req, res) => {
checkBodyForPresetFields(req, res, () => { checkBodyForPresetFields(req, res, () => {
updatePreset.addNewPreset(req.body.systemName, req.body.frequencies, req.body.mode, () => { addNewPreset(req.body.systemName, req.body.frequencies, req.body.mode, () => {
return res.sendStatus(200); return res.sendStatus(200);
}, req.body.trunkFile); }, req.body.trunkFile);
}); });
} }
/**
* Removes a preset from the client
*/
exports.removePreset = async (req, res) => {
checkBodyForPresetFields(req, res, () => {
if (!req.body.systemName) return res.status("500").json({"message": "You must specify a system name to delete, this must match exactly to how the system name is saved."})
removePreset(req.body.systemName, () => {
return res.sendStatus(200);
}, req.body.trunkFile);
});
}
/**
* Runs the updater service
*/
exports.updateClient = async (req, res) => {
await executeAsyncConsoleCommand("systemctl start RadioNodeUpdater.service");
return res.sendStatus(200);
}

View File

@@ -1,163 +0,0 @@
require('dotenv').config();
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client", "commandController");
// Modules
const { joinVoiceChannel, VoiceConnectionStatus, getVoiceConnection, createAudioPlayer, createAudioResource, AudioPlayerStatus } = require("@discordjs/voice");
const { OpusEncoder } = require("@discordjs/opus");
const { calcRmsSync } = require("../utilities/rmsCalculator.js");
// Utilities
const {replyToInteraction} = require("../utilities/messageHandler.js");
const {createAudioInstance} = require("../controllers/audioController.js");
const { all } = require('../routes/index.js');
// Declare the encoder
const encoder = new OpusEncoder(48000, 2);
const noiseGateOpen = process.env.AUDIO_NOISE_GATE_OPEN ?? 100;
/**
* Join the specified voice channel
*
* @param interaction Message interaction from discord
* @param {string||any} guildID The specified Guild ID if this function is run from the client instead of from an interaction in Discord
* @param {string||any} channelID The channel ID to join
* @param guild The guild object to be used to create a voice adapter
* @param {function} callback The callback that will be needed if this function is run with a Guild ID instead of an interaction
*/
exports.join = async function join({interaction= undefined, guildID= undefined, channelID = undefined, guildObj = undefined, callback = undefined}){
if (interaction){
const voiceChannel = interaction.options.getChannel('voicechannel');
channelID = voiceChannel.id;
guildID = interaction.guildId;
guildObj = interaction.guild;
if (interaction) replyToInteraction(interaction, `Ok, Joining ${voiceChannel.name}`);
}
log.DEBUG("Channel ID: ", channelID)
log.DEBUG("Guild ID: ", guildID)
const vcConnectionObj = {
channelId: channelID,
guildId: guildID,
adapterCreator: guildObj.voiceAdapterCreator,
selfMute: false,
selfDeaf: false,
};
// Join the voice channel
const voiceConnection = await joinVoiceChannel(vcConnectionObj);
// Create the audio stream instance
const audioInstance = await createAudioInstance();
log.VERBOSE("Audio Instance: ", audioInstance);
// Play audio data when it's received from the stream
audioInstance.on('data', buffer => {
buffer = Buffer.from(buffer);
//log.VERBOSE("Audio buffer: ", buffer);
// Check intensity of audio and only play when audio is present (no white noise/static)
volume = Math.trunc(calcRmsSync(buffer, buffer.length));
if (parseInt(volume) > parseInt(noiseGateOpen)) {
//log.VERBOSE("Noisegate and buffer volume: ", (parseInt(volume) > parseInt(noiseGateOpen)), noiseGateOpen, volume);
const encodedBuffer = encoder.encode(buffer);
voiceConnection.playOpusPacket(encodedBuffer);
}
});
audioInstance.start();
// Handle state changes in the voice connection
voiceConnection.on('stateChange', async (oldState, newState) => {
//log.VERBOSE("VoiceConnection state Changed from state: ", oldState, "\n\nto state: ", newState);
log.DEBUG("VoiceConnection state Changed from: ", oldState.status, "to: ", newState.status);
// Ready -> Connecting
if (oldState.status === VoiceConnectionStatus.Ready && newState.status === VoiceConnectionStatus.Connecting) {
log.DEBUG("Configuring Network");
voiceConnection.configureNetworking();
return;
}
// Ready -> Disconnected
if (oldState.status === VoiceConnectionStatus.Ready && newState.status === VoiceConnectionStatus.Disconnected) {
log.DEBUG("Attempting to reconnect the voice connection");
voiceConnection = joinVoiceChannel(vcConnectionObj);
return;
}
// Ready
if (newState.status === VoiceConnectionStatus.Ready){
log.DEBUG("Bot connected to voice channel");
return;
}
// Destroyed
if (newState.status === VoiceConnectionStatus.Destroyed){
log.DEBUG("Quitting audio instance");
audioInstance.quit();
return;
}
});
voiceConnection.on('error', async (error) => {
log.ERROR("Voice Connection Error: ", error);
})
if (guildID && callback) return callback();
else return;
}
/**
* If in a voice channel for the specified guild, leave
*
* @param interaction Message interaction from discord
* @param guildID
* @param callback
*/
exports.leave = async function leave({interaction = undefined, guildID= undefined, callback = undefined}) {
if(interaction) {
guildID = interaction.guild.id;
}
const voiceConnection = getVoiceConnection(guildID);
let response;
if (!voiceConnection){
response = "Not in a voice channel."
if (interaction) return replyToInteraction(interaction, response);
else callback(response);
}
voiceConnection.destroy();
response = "Goodbye"
if (interaction) return replyToInteraction(interaction, response);
else callback(response);
}
/**
* Get the voice status of the bots
* @param {*} param0
* @returns
*/
exports.status = async function status({interaction= undefined, guildID= undefined, callback = undefined}) {
//if (!interaction && !guildID) // Need error of sorts
if (interaction){
guildID = interaction.guild.id;
}
const voiceConnection = getVoiceConnection(guildID);
const statusObj = {
"guildID": guildID,
"voiceConnection": typeof g !== 'undefined' ? true : false // True if there is a voice connection, false if undefined
}
log.DEBUG('Status Object: ', statusObj);
// get the status and return it accordingly (message reply / module)
if (interaction) {
return replyToInteraction(interaction, "Pong! I have Aids and now you do too!");
}
else {
callback(statusObj);
}
}

View File

@@ -2,146 +2,72 @@
const { DebugBuilder } = require("../utilities/debugBuilder.js"); const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client", "radioController"); const log = new DebugBuilder("client", "radioController");
// Modules // Modules
const { resolve, dirname } = require('path'); require('dotenv').config();
const fs = require('fs'); const { closeProcessWrapper, changeCurrentConfigWrapper, openRadioSessionWrapper } = require("../utilities/utilities");
const radioConfig = require('../config/clientConfig').radioAppConfig;
const radioConfigHelper = require("../utilities/radioConfigHelper");
const presetWrappers = require("../utilities/updatePresets");
const spawn = require('child_process').spawn;
const converter = require("convert-units");
let radioChildProcess, tempRes, radioConfigPath; let radioChildProcess;
/** /**
* Closes the radio executable if it's in one * Closes the radio executable if it's in one
*/ */
exports.closeRadioSession = (req, res) => { exports.closeRadioSession = async (req, res) => {
if (!radioChildProcess) return res.sendStatus(200) if (!radioChildProcess || !req.body.radioSession) return res.sendStatus(204);
tempRes = res; if (radioChildProcess) radioChildProcess = await closeProcessWrapper(radioChildProcess);
radioChildProcess.kill(); if (req.body.radioSession) req.body.radioSession = await closeProcessWrapper(req.body.radioSession);
radioChildProcess = undefined; if (!radioChildProcess) return res.sendStatus(200);
} }
/** /**
* Change the current 'cfg.json' file to the preset specified * Change the current 'cfg.json' file to the preset specified
* @param {string} presetName * @param {string} presetName
*/ */
exports.changeCurrentConfig = (req, res) => { exports.changeCurrentConfig = async (req, res) => {
// Check if the given config is saved const presetName = req.body.presetName;
log.DEBUG("[/radio/changeCurrentConfig] - Checking if provided preset is in the config"); if (!presetName) return res.status(500).json("You must include the preset name")
if (!checkIfPresetExists(req.body.presetName)) return res.status(500).JSON("No preset with given name found in config"); // No preset with the given name is in the config
// Check if the current config is the same as the preset given const updatedConfigObject = await changeCurrentConfigWrapper(presetName);
const currentConfig = readOP25Config();
if (currentConfig.channels && currentConfig.channels.name === req.body.presetName) { // No change was made to the config
log.DEBUG("[/radio/changeCurrentConfig] - Current config is the same as the preset given"); if (!updatedConfigObject) return res.sendStatus(200);
return res.sendStatus(202);
// Error was encountered
if (typeof updatedConfigObject === "string") return res.status(500).json(updatedConfigObject);
// There was a change made to the config, reopening the radio session if it was open
if (radioChildProcess) {
log.DEBUG("Radio session open, restarting to accept the new config");
const radioSessionResult = await openRadioSessionWrapper(radioChildProcess, presetName);
// throw an error to the client if the wrapper ran into an error
if (typeof radioSessionResult === "string") return res.status(500).json(updatedConfigObject);
} }
// Convert radioPreset to OP25 'cfg.json. file return res.sendStatus(202);
log.DEBUG("[/radio/changeCurrentConfig] - Converting radioPreset to OP25 config");
const updatedConfigObject = convertRadioPresetsToOP25Config(req.body.presetName);
// Replace current JSON file with the updated file
writeOP25Config(updatedConfigObject, () => {
res.sendStatus(200);
})
} }
/** /**
* Open a new OP25 process tuned to the specified system * Open a new OP25 process tuned to the specified system
*/ */
exports.openRadioSession = () => { exports.openRadioSession = async (req, res) => {
if (radioChildProcess) closeRadioSession(); const presetName = req.body.presetName;
radioChildProcess = spawn(getRadioBinPath()); if(!presetName) return res.status(500).json({"message": "You must include the preset name to start the radio session with"})
radioChildProcess = await openRadioSessionWrapper(radioChildProcess, presetName);
// throw an error to the client if the wrapper ran into an error
if (typeof radioSessionResult === "string") return res.status(500).json(updatedConfigObject);
return res.sendStatus(200);
} }
/** /**
* Get the location of the 'multi_rx.py' binary from the config * Attach the radio session to the request to be used elsewhere
*
* @param {*} req
* @param {*} res
*/ */
function getRadioBinPath(){ exports.attachRadioSessionToRequest = async (req, res, next) => {
return resolve(radioConfig.bin); req.body.radioSession = radioChildProcess;
} next();
/**
* Write the given config to the JSON file in OP25 the bin dir
* @param config The full config to be written to the file
* @param {function} callback The function to be called when this wrapper completes
*/
function writeOP25Config(config, callback = undefined) {
log.DEBUG("Updating OP25 config with: ", config);
fs.writeFile(getRadioConfigPath(), JSON.stringify(config), (err) => {
// Error checking
if (err) {
log.ERROR(err);
throw err;
}
log.DEBUG("Write Complete");
if (callback) callback()
});
}
/**
* Get the current config file in use by OP25
* @returns {object|*} The parsed config object currently set in OP25
*/
function readOP25Config() {
const configPath = getRadioConfigPath();
log.DEBUG(`Reading from config path: '${configPath}'`);
return JSON.parse(fs.readFileSync(configPath));
}
/**
* Get the path of the config for the radio app (OP25) and set the global variable
*/
function getRadioConfigPath(){
let radioConfigDirPath = dirname(getRadioBinPath());
return resolve(`${radioConfigDirPath}/cfg.json`);
}
/**
* Check to see if the preset name exists in the config
* @param {string} presetName The system name as saved in the preset
* @returns {true||false}
*/
function checkIfPresetExists(presetName) {
const savedPresets = presetWrappers.getPresets();
if (!Object.keys(savedPresets).includes(presetName)) return false;
else return true;
}
/**
* Convert a radioPreset to OP25's cfg.json file
*/
function convertRadioPresetsToOP25Config(presetName){
const savedPresets = presetWrappers.getPresets();
let frequencyString = "";
for (const frequency of savedPresets[presetName].frequencies){
frequencyString += `${converter(frequency).from("Hz").to("MHz")},`
}
frequencyString = frequencyString.slice(0, -1);
let updatedOP25Config;
switch (savedPresets[presetName].mode){
case "p25":
updatedOP25Config = new radioConfigHelper.P25({
"systemName": presetName,
"controlChannelsString": frequencyString,
"tagsFile": savedPresets[presetName].trunkFile
});
break;
case "nbfm":
//code for nbfm here
updatedOP25Config = new radioConfigHelper.NBFM({
"frequency": frequencyString,
"systemName": presetName
});
break;
default:
throw new Error("Radio mode of selected preset not recognized");
}
log.DEBUG(updatedOP25Config);
return updatedOP25Config;
} }

View File

@@ -1,150 +0,0 @@
//Config
import { getTOKEN, getGuildID, getApplicationID } from './utilities/configHandler.js';
// Commands
import ping from './commands/ping.js';
import join from './commands/join.js';
import leave from './commands/leave.js';
import status from './commands/status.js';
// Debug
import ModuleDebugBuilder from "./utilities/moduleDebugBuilder.js";
const log = new ModuleDebugBuilder("bot", "app");
// Modules
import { Client, GatewayIntentBits } from 'discord.js';
// Utilities
import registerCommands from './utilities/registerCommands.js';
/**
* Host Process Object Builder
*
* This constructor is used to easily construct responses to the host process
*/
class HPOB {
/**
* Build an object to be passed to the host process
* @param command The command to that was run ("Status", "Join", "Leave", "ChgPreSet")
* @param response The response from the command that was run
*/
constructor(command = "Status"||"Join"||"Leave"||"ChgPreSet", response) {
this.cmd = command;
this.msg = response;
}
}
// Create the Discord client
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildVoiceStates
]
});
/**
* When the parent process sends a message, this will interpret the message and act accordingly
*
* DRB IPC Message Structure:
* msg.cmd = The command keyword; Commands covered on the server side
* msg.params = An array containing the parameters for the command
*
*/
process.on('message', (msg) => {
log.DEBUG('IPC Message: ', msg);
const guildID = getGuilds()[0];
log.DEBUG("Guild Name: ", getGuildNameFromID(guildID));
switch (msg.cmd) {
// Check the status of the bot
case "Status":
log.INFO("Status command run from IPC");
status({guildID: guildID, callback: (statusObj) => {
log.DEBUG("Status Object string: ", statusObj);
if (!statusObj.voiceConnection) return process.send(new HPOB("Status", "VDISCONN"));
}});
break;
// Check the params for a server ID and if so join the server
case "Join":
log.INFO("Join command run from IPC");
join({guildID: guildID, guildObj: client.guilds.cache.get(guildID), channelID: msg.params.channelID, callback: () => {
process.send(new HPOB("Join", "AIDS"));
}})
break;
// Check to see if the bot is in a server and if so leave
case "Leave":
log.INFO("Leave command run from IPC");
leave({guildID: guildID, callback: (response) => {
process.send(new HPOB("Leave", response));
}});
break;
default:
// Command doesn't exist
log.INFO("Unknown command run from IPC");
break;
}
})
// When the client is connected and ready
client.on('ready', () =>{
log.INFO(`${client.user.tag} is ready`)
process.send({'msg': "INIT READY"});
});
/*
* Saved For later
client.on('messageCreate', (message) => {
log.DEBUG(`Message Sent by: ${message.author.tag}\n\t'${message.content}'`);
});
*/
// When a command is sent
client.on('interactionCreate', (interaction) => {
if (interaction.isChatInputCommand()){
switch (interaction.commandName) {
case "ping":
ping(interaction);
break;
case "join":
join({ interaction: interaction });
break;
case "leave":
leave({ interaction: interaction });
break;
case "status":
status({ interaction: interaction });
break;
default:
interaction.reply({ content: 'Command not found, try one that exists', fetchReply: true })
.then((message) => log.DEBUG(`Reply sent with content ${message.content}`))
.catch((err) => log.ERROR(err));
}
}
})
function loginBot(){
client.login(getTOKEN());
}
function getGuilds() {
return client.guilds.cache.map(guild => guild.id)
}
function getGuildNameFromID(guildID) {
return client.guilds.cache.map((guild) => {
if (guild.id === guildID) return guild.name;
})[0]
}
function main(){
registerCommands(() => {
loginBot();
});
}
main();
//module.exports = client;

File diff suppressed because it is too large Load Diff

View File

@@ -1,25 +0,0 @@
{
"name": "discord-bot",
"version": "1.0.0",
"description": "",
"main": "app.js",
"scripts": {
"test": "echo \"Error: no test specified\" && exit 1"
},
"keywords": [],
"author": "",
"license": "ISC",
"dependencies": {
"@discordjs/builders": "^1.4.0",
"@discordjs/opus": "^0.9.0",
"@discordjs/rest": "^1.4.0",
"@discordjs/voice": "^0.14.0",
"@mapbox/node-pre-gyp": "^1.0.10",
"debug": "^4.3.4",
"discord.js": "^14.7.1",
"node-gyp": "^9.3.0",
"libsodium-wrappers": "^0.7.10",
"alsa-capture": "0.3.0"
},
"type": "module"
}

View File

@@ -1,46 +0,0 @@
# Discord Radio Bot: Command & Control - Client: Discord Bot (Client)
---
Explanation here
## Requirements
---
Requirements here (not modules, that will be installed with npm)
## Installation
---
Notes here
### Installation here
```shell
```
## Configuration
---
Notes here
### Configuration here
```shell
```
## Usage
---
### Usage here
```javascript
```

View File

@@ -1,17 +0,0 @@
// Debug
import Debug from 'debug';
/**
* Create the different logging methods for a function
* Namespace template = ("[app]:[fileName]:['INFO', 'WARNING', 'DEBUG', 'ERROR']")
* @param {string} appName The name of the app to be used in the 'app' portion of the namespace
* @param {string} fileName The name of the file calling the builder to be used in the 'fileName' portion of the namespace
*/
export default class ModuleDebugBuilder {
constructor(appName, fileName) {
this.INFO = Debug(`${appName}:${fileName}:INFO`);
this.DEBUG = Debug(`${appName}:${fileName}:DEBUG`);
this.WARN = Debug(`${appName}:${fileName}:WARNING`);
this.ERROR = Debug(`${appName}:${fileName}:ERROR`);
}
}

View File

@@ -1,55 +0,0 @@
import {SlashCommandBuilder} from "@discordjs/builders";
import {REST} from "@discordjs/rest";
import {getApplicationID, getGuildID, getTOKEN} from "./configHandler.js";
import { Routes, ChannelType } from "discord.js";
// Debug
import ModuleDebugBuilder from "./moduleDebugBuilder.js";
const log = new ModuleDebugBuilder("bot", "registerCommands");
const pingCommand = new SlashCommandBuilder()
.setName("ping")
.setDescription("Confirm the bot is online")
.toJSON();
const joinCommand = new SlashCommandBuilder()
.setName('join')
.setDescription('Joins a voice channel')
.addChannelOption((option) => option
.setName('voicechannel')
.setDescription('The Channel to voiceController')
.setRequired(false)
.addChannelTypes(ChannelType.GuildVoice))
.toJSON();
const leaveCommand = new SlashCommandBuilder()
.setName("leave")
.setDescription("Leave current voice channel")
.toJSON();
const statusCommand = new SlashCommandBuilder()
.setName("status")
.setDescription("Returns if the bot is connected to a channel or not")
.toJSON();
export default async function registerCommands(callback){
const commands = [
pingCommand,
joinCommand,
leaveCommand,
statusCommand
];
try {
const rest = new REST({ version: '10' }).setToken(getTOKEN());
const clientID = getApplicationID();
const guildID = getGuildID();
await rest.put(Routes.applicationGuildCommands(clientID, guildID), {
body: commands,
});
log.DEBUG("Successfully registered the following commands: ", commands)
callback();
} catch (err) {
log.ERROR(err);
}
}

View File

@@ -19,13 +19,7 @@
"morgan": "~1.9.1", "morgan": "~1.9.1",
"replace-in-file": "~6.3.5", "replace-in-file": "~6.3.5",
"@discordjs/builders": "^1.4.0", "@discordjs/builders": "^1.4.0",
"@discordjs/opus": "^0.9.0",
"@discordjs/rest": "^1.4.0", "@discordjs/rest": "^1.4.0",
"@discordjs/voice": "^0.14.0", "discord.js": "^14.7.1"
"@mapbox/node-pre-gyp": "^1.0.10",
"discord.js": "^14.7.1",
"node-gyp": "^9.3.0",
"libsodium-wrappers": "^0.7.10",
"naudiodon": "^2.3.6"
} }
} }

5
Client/pdab/.gitignore vendored Normal file
View File

@@ -0,0 +1,5 @@
*venv/
*__pycache__/
*.html
*.exe
LICENSE

215
Client/pdab/NoiseGatev2.py Normal file
View File

@@ -0,0 +1,215 @@
import audioop
import logging
import math
import time
import pyaudio
import discord
import numpy
voice_connection = None
LOGGER = logging.getLogger("Discord_Radio_Bot.NoiseGateV2")
# noinspection PyUnresolvedReferences
class AudioStream:
def __init__(self, _channels: int = 2, _sample_rate: int = 48000, _frames_per_buffer: int = 1024,
_input_device_index: int = None, _output_device_index: int = None, _input: bool = True,
_output: bool = True, _init_on_startup: bool = True):
self.paInstance_kwargs = {
'format': pyaudio.paInt16,
'channels': _channels,
'rate': _sample_rate,
'input': _input,
'output': _output,
'frames_per_buffer': _frames_per_buffer
}
if _input_device_index:
if _input:
self.paInstance_kwargs['input_device_index'] = _input_device_index
else:
LOGGER.warning(f"[AudioStream.__init__]:\tInput was not enabled."
f" Reinitialize with '_input=True'")
if _output_device_index:
if _output:
self.paInstance_kwargs['output_device_index'] = _output_device_index
else:
LOGGER.warning(f"[AudioStream.__init__]:\tOutput was not enabled."
f" Reinitialize with '_output=True'")
if _init_on_startup:
# Init PyAudio instance
LOGGER.info("Creating PyAudio instance")
self.paInstance = pyaudio.PyAudio()
# Define and initialize stream object if we have been passed a device ID (pyaudio.open)
self.stream = None
if _output_device_index or _input_device_index:
if _init_on_startup:
LOGGER.info("Init stream")
self.init_stream()
def init_stream(self, _new_output_device_index: int = None, _new_input_device_index: int = None):
# Check what device was asked to be changed (or set)
if _new_input_device_index:
if self.paInstance_kwargs['input']:
self.paInstance_kwargs['input_device_index'] = _new_input_device_index
else:
LOGGER.warning(f"[AudioStream.init_stream]:\tInput was not enabled when initialized."
f" Reinitialize with '_input=True'")
if _new_output_device_index:
if self.paInstance_kwargs['output']:
self.paInstance_kwargs['output_device_index'] = _new_output_device_index
else:
LOGGER.warning(f"[AudioStream.init_stream]:\tOutput was not enabled when initialized."
f" Reinitialize with '_output=True'")
self.close_if_open()
# Open the stream
self.stream = self.paInstance.open(**self.paInstance_kwargs)
def close_if_open(self):
# Stop the stream if it is started
if self.stream:
if self.stream.is_active():
self.stream.stop_stream()
self.stream.close()
LOGGER.debug(f"[ReopenStream.close_if_open]:\t Stream was open; It was closed.")
def list_devices(self, _display_input_devices: bool = True, _display_output_devices: bool = True):
LOGGER.info('Getting a list of the devices connected')
info = self.paInstance.get_host_api_info_by_index(0)
numdevices = info.get('deviceCount')
devices = {
'Input': {},
'Output': {}
}
for i in range(0, numdevices):
if (self.paInstance.get_device_info_by_host_api_device_index(0, i).get('maxInputChannels')) > 0:
input_device = self.paInstance.get_device_info_by_host_api_device_index(0, i).get('name')
devices['Input'][i] = input_device
if _display_input_devices:
LOGGER.debug(f"Input Device id {i} - {input_device}")
if (self.paInstance.get_device_info_by_host_api_device_index(0, i).get('maxOutputChannels')) > 0:
output_device = self.paInstance.get_device_info_by_host_api_device_index(0, i).get('name')
devices['Output'][i] = output_device
if _display_output_devices:
LOGGER.debug(f"Output Device id {i} - {output_device}")
return devices
async def stop(self):
await voice_connection.disconnect()
self.close_if_open()
self.stream.close()
self.paInstance.terminate()
# noinspection PyUnresolvedReferences
class NoiseGate(AudioStream):
def __init__(self, _voice_connection, _noise_gate_threshold: int, **kwargs):
super(NoiseGate, self).__init__(_init_on_startup=True, **kwargs)
global voice_connection
voice_connection = _voice_connection
self.THRESHOLD = _noise_gate_threshold
self.NGStream = NoiseGateStream(self)
self.Voice_Connection_Thread = None
def run(self) -> None:
global voice_connection
# Start the audio stream
LOGGER.debug(f"Starting stream")
self.stream.start_stream()
# Start the stream to discord
self.core()
def core(self, error=None):
if error:
LOGGER.warning(error)
while not voice_connection.is_connected():
time.sleep(.2)
if not voice_connection.is_playing():
LOGGER.debug(f"Playing stream to discord")
voice_connection.play(self.NGStream, after=self.core)
async def close(self):
LOGGER.debug(f"Closing")
await voice_connection.disconnect()
if self.stream.is_active:
self.stream.stop_stream()
LOGGER.debug(f"Stopping stream")
# noinspection PyUnresolvedReferences
class NoiseGateStream(discord.AudioSource):
def __init__(self, _stream):
super(NoiseGateStream, self).__init__()
self.stream = _stream # The actual audio stream object
self.NG_fadeout = 240/20 # Fadeout value used to hold the noisegate after de-triggering
self.NG_fadeout_count = 0 # A count set when the noisegate is triggered and was de-triggered
self.process_set_count = 0 # Counts how many processes have been made
def read(self):
try:
while voice_connection.is_connected():
curr_buffer = bytearray(self.stream.stream.read(960))
buffer_rms = audioop.rms(curr_buffer, 2)
if buffer_rms > 0:
buffer_decibel = 20 * math.log10(buffer_rms)
if self.process_set_count % 10 == 0:
if buffer_decibel >= self.stream.THRESHOLD:
LOGGER.debug(f"[Noisegate Open] {buffer_decibel} db")
else:
LOGGER.debug(f"[Noisegate Closed] {buffer_decibel} db")
if buffer_decibel >= self.stream.THRESHOLD:
self.NG_fadeout_count = self.NG_fadeout
self.process_set_count += 1
if curr_buffer:
return bytes(curr_buffer)
else:
if self.NG_fadeout_count > 0:
self.NG_fadeout_count -= 1
LOGGER.debug(f"Frames in fadeout remaining: {self.NG_fadeout_count}")
self.process_set_count += 1
if curr_buffer:
return bytes(curr_buffer)
except OSError as e:
LOGGER.warning(e)
pass
def audio_datalist_set_volume(self, datalist, volume):
""" Change value of list of audio chunks """
sound_level = (volume / 100.)
for i in range(len(datalist)):
chunk = numpy.fromstring(datalist[i], numpy.int16)
chunk = chunk * sound_level
datalist[i] = chunk.astype(numpy.int16)
if __name__ == '__main__':
input_index = int(input("Input:\t"))
output_index = int(input("Output:\t"))
ng = NoiseGate(_input_device_index=input_index, _output_device_index=output_index)
ng.list_devices()
ng.start()

11
Client/pdab/getDevices.py Normal file
View File

@@ -0,0 +1,11 @@
from NoiseGatev2 import AudioStream
print('Getting a list of devices')
list_of_devices = AudioStream().list_devices()
print("----- INPUT DEVICES -----")
for inputDevice in list_of_devices['Input']:
print(f"{inputDevice}\t-\t{list_of_devices['Input'][inputDevice]}")
print("----- OUTPUT DEVICES -----")
for outputDevice in list_of_devices['Output']:
print(f"{outputDevice}\t-\t{list_of_devices['Output'][outputDevice]}")

75
Client/pdab/main.py Normal file
View File

@@ -0,0 +1,75 @@
import argparse, platform, os
from discord import Intents, Client, Member, opus
from discord.ext import commands
from NoiseGatev2 import NoiseGate
# Load the proper OPUS library for the device being used
async def load_opus():
# Check the system type and load the correct library
# Linux ARM AARCH64 running 32bit OS
processor = platform.machine()
print("Processor: ", processor)
if os.name == 'nt':
if processor == "AMD64":
print(f"Loaded OPUS library for AMD64")
opus.load_opus('./opus/libopus_amd64.dll')
return "AMD64"
else:
if processor == "aarch64":
print(f"Loaded OPUS library for aarch64")
opus.load_opus('./opus/libopus_aarcch64.so')
return "aarch64"
elif processor == "armv7l":
print(f"Loaded OPUS library for armv7l")
opus.load_opus('./opus/libopus_armv7l.so')
return "armv7l"
def main(clientId='OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY', channelId=367396189529833476, NGThreshold=50, deviceId=1):
intents = Intents.default()
client = commands.Bot(command_prefix='!', intents=intents)
@client.event
async def on_ready():
print(f'We have logged in as {client.user}')
channelIdToJoin = client.get_channel(channelId)
print("Channel", channelIdToJoin)
print("Loading opus")
await load_opus()
if opus.is_loaded():
print("Joining voice")
channelConnection = await channelIdToJoin.connect(timeout=60.0, reconnect=True)
print("Voice Connected")
streamHandler = NoiseGate(
_input_device_index=deviceId,
_voice_connection=channelConnection,
_noise_gate_threshold=NGThreshold)
# Start the audio stream
streamHandler.run()
print("stream running")
client.run(clientId)
parser = argparse.ArgumentParser()
parser.add_argument("deviceId", type=int, help="The ID of the audio device to use")
parser.add_argument("channelId", type=int, help="The ID of the voice channel to use")
parser.add_argument("clientId", type=str, help="The discord client ID")
parser.add_argument("-n", "--NGThreshold", type=int, help="Change the noisegate threshold. This defaults to 50")
args = parser.parse_args()
if (not args.NGThreshold):
args.NGThreshold = 50
print("Arguments:", args)
main(
clientId=args.clientId,
channelId=args.channelId,
NGThreshold=args.NGThreshold,
deviceId=args.deviceId
)

Binary file not shown.

Binary file not shown.

Binary file not shown.

View File

@@ -0,0 +1,5 @@
discord>=2.2.3
PyNaCl>=1.5.0
pyaudio>=0.2.13
numpy>=1.24.3
argparse

View File

@@ -0,0 +1,7 @@
#!/bin/bash
# This script should be another service on the machine to watch the main script for failures and restart it if there are any
( tail -f -n0 /opt/sdr-scanner/scanner_log & ) | grep -q ": cb transfer status: 1, canceling..."
systemctl restart radioNode.service
echo "Restarted SDR Scanner service"

View File

@@ -5,7 +5,7 @@ const botController = require("../controllers/botController");
/** GET bot status /** GET bot status
* Check to see if the bot is online and if so, if it is currently connected to anything * Check to see if the bot is online and if so, if it is currently connected to anything
* *
* The status of the bot: 200 = client is online but not connected to discord, 201 = online on discord, 202 = connected to a channel, 500 + JSON = encountered error * The status of the bot: 200 = connected to discord, 201 = not connected to discord, 500 + JSON = encountered error
* @returns status * @returns status
*/ */
router.get('/status', botController.getStatus); router.get('/status', botController.getStatus);

View File

@@ -2,22 +2,21 @@
const express = require('express'); const express = require('express');
const router = express.Router(); const router = express.Router();
// Controllers // Controllers
const clientController = require("../controllers/clientController"); const { requestCheckIn, getPresets, updatePreset, addNewPreset, removePreset, updateClient } = require("../controllers/clientController");
/** GET Request a check in from the client /** GET Request a check in from the client
* Queue the client to check in with the server * Queue the client to check in with the server
* *
* The status of the checkin request: 200 = Queued * The status of the checkin request: 200 = Queued
*/ */
router.get('/requestCheckIn', clientController.requestCheckIn); router.get('/requestCheckIn', requestCheckIn);
/** GET Object of all known presets /** GET Object of all known presets
* Query the client to get all the known presets * Query the client to get all the known presets
*/ */
router.get('/presets', clientController.getPresets); router.get('/presets', getPresets);
/** POST Update to preset /** POST Update to preset
* Join the channel specified listening to the specified freq/mode
* *
* @param req The request sent from the master * @param req The request sent from the master
* @param {string} req.body.systemName The name of the system to be updated * @param {string} req.body.systemName The name of the system to be updated
@@ -25,7 +24,7 @@ router.get('/presets', clientController.getPresets);
* @param {string} req.body.mode The listening mode for the SDR * @param {string} req.body.mode The listening mode for the SDR
* @param {string} req.body.trunkFile If the listening mode is digital this can be set to identify the communications * @param {string} req.body.trunkFile If the listening mode is digital this can be set to identify the communications
*/ */
router.post('/updatePreset', clientController.updatePreset); router.post('/updatePreset', updatePreset);
/** POST Add new preset /** POST Add new preset
* Join the channel specified listening to the specified freq/mode * Join the channel specified listening to the specified freq/mode
@@ -36,6 +35,20 @@ router.post('/updatePreset', clientController.updatePreset);
* @param {string} req.body.mode The listening mode for the SDR * @param {string} req.body.mode The listening mode for the SDR
* @param {string} req.body.trunkFile If the listening mode is digital this can be set to identify the communications * @param {string} req.body.trunkFile If the listening mode is digital this can be set to identify the communications
*/ */
router.post('/addPreset', clientController.addNewPreset); router.post('/addPreset', addNewPreset);
/** POST Remove a preset
*
* @param req The request sent from the master
* @param {string} req.body.systemName The name of the system to be updated
*/
router.post('/removePreset', removePreset);
/** POST Update the bot
*
* @param req The request sent from the master
*/
router.post('/updateClient', updateClient);
module.exports = router; module.exports = router;

View File

@@ -15,6 +15,7 @@ router.post('/start', radioController.openRadioSession);
/** /**
* POST Close the current radio session * POST Close the current radio session
* Response from the radio: 200: closed; 204: not connected
*/ */
router.post('/stop', radioController.closeRadioSession); router.post('/stop', radioController.closeRadioSession);

View File

@@ -5,6 +5,19 @@ if [ "$EUID" -ne 0 ]
then echo "Please run as root" then echo "Please run as root"
exit exit
fi fi
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
ls -ld $SCRIPT_DIR | awk '{print $3}' >> ./config/installerName
# Setup user for service
useradd -M RadioNode
usermod -s -L RadioNode
# Create the .env file from the example
cp $SCRIPT_DIR/.env.example $SCRIPT_DIR/.env
# Change the ownership of the directory to the service user
chown RadioNode -R $SCRIPT_DIR
# Check for updates # Check for updates
apt-get update apt-get update
@@ -17,10 +30,49 @@ apt-get update
apt-get upgrade -y apt-get upgrade -y
# Install the necessary packages # Install the necessary packages
apt-get install -y nodejs npm libopus-dev gcc make alsa-utils libasound2 libasound2-dev libpulse-dev pulseaudio apulse apt-get install -y nodejs portaudio19-dev libportaudio2 libpulse-dev pulseaudio apulse python3 python3-pip
# Ensure pulse audio is running # Ensure pulse audio is running
pulseaudio pulseaudio
# Install the node packages from the project # Install the node packages from the project
npm i npm i
# Install the python packages needed for the bot
pip install -r
# Setup bot service
echo "[Unit]
Description=Radio Node Service
After=network.target
[Service]
WorkingDirectory="$SCRIPT_DIR"
ExecStart=/usr/bin/node .
Restart=always
RestartDelay=10
User=RadioNode
Environment=DEBUG='client:*'
[Install]
WantedBy=multi-user.target" >> /etc/systemd/system/RadioNode.service
# Setup bot update service
echo "[Unit]
Description=Radio Node Updater Service
After=network.target
[Service]
WorkingDirectory="$SCRIPT_DIR"
ExecStart=/usr/bin/bash update.sh
Restart=on-failure
User=RadioNode
[Install]
WantedBy=multi-user.target" >> /etc/systemd/system/RadioNodeUpdater.service
# Enable the service
systemctl enable RadioNode.service
# Start the service
systemctl start RadioNode.service

View File

@@ -1,15 +1,32 @@
#!/bin/bash #!/bin/bash
# Check if the user is root
if [ "$EUID" -ne 0 ]
then echo "Please run as root"
exit
fi
# Sleeping to give the client time to respond to the requester
sleep 5
# Stating Message # Stating Message
echo "<!-- UPDATING ---!>" echo "<!-- UPDATING ---!>"
# TODO - Add an updater for Stable Diffusion API # TODO - Add an updater for Stable Diffusion API
# Stop any running service
systemctl stop RadioNode
# Update the git Repo # Update the git Repo
git fetch -a -p installUser=$(cat ./config/installerName)
git pull sudo su -l $installUser -c 'git fetch -a -p'
sudo su -l $installUser -c 'git pull'
# Install any new libraries # Install any new libraries
npm i npm i
# Start the service
systemctl start RadioNode
# Update complete message # Update complete message
echo "<!--- UPDATE COMPLETE! ---!>" echo "<!--- UPDATE COMPLETE! ---!>"

View File

@@ -1,49 +0,0 @@
const { REST, Routes } = require('discord.js');
require('dotenv').config();
const token = process.env.TOKEN;
//const clientId = process.env.clientId;
//const guildId = process.env.guildId;
const fs = require('node:fs');
const path = require('node:path');
const { DebugBuilder } = require("./debugBuilder");
const log = new DebugBuilder("client", "deployCommands");
const commands = [];
// Grab all the command files from the commands directory you created earlier
const commandsPath = path.resolve(__dirname, '../commands');
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
exports.deploy = (clientId, guildIDs) => {
log.DEBUG("Deploying commands for: ", guildIDs);
if (Array.isArray(guildIDs)) guildIDs = [guildIDs];
// Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment
for (const file of commandFiles) {
const command = require(`${path.resolve(commandsPath, file)}`);
commands.push(command.data.toJSON());
}
// Construct and prepare an instance of the REST module
const rest = new REST({ version: '10' }).setToken(token);
// and deploy your commands!
for (const guildId of guildIDs){
(async () => {
try {
log.DEBUG(`Started refreshing ${commands.length} application (/) commands for guild ID: ${guildId}.`);
// 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, guildId),
{ body: commands },
);
log.DEBUG(`Successfully reloaded ${data.length} application (/) commands for guild ID: ${guildId}.`);
} catch (error) {
// And of course, make sure you catch and log any errors!
log.ERROR("ERROR Deploying commands: ", error, "Body from error: ", commands);
}
})()
}
};

View File

@@ -1,52 +0,0 @@
// Modules
const { promisify } = require('util');
const { exec } = require("child_process");
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder.js");
// Global Vars
const log = new DebugBuilder("client", "executeConsoleCommands");
const execCommand = promisify(exec);
async function executeAsyncConsoleCommand(consoleCommand) {
// Check to see if the command is a real command
// TODO needs to be improved
const acceptableCommands = [ "arecord -L", 'ipconfig', 'ip addr' ];
if (!acceptableCommands.includes(consoleCommand)) {
log.WARN("Console command is not acceptable: ", consoleCommand);
return undefined;
}
log.DEBUG("Running console command: ", consoleCommand);
const tempOutput = await execCommand(consoleCommand);
const output = tempOutput.stdout.trim();
log.DEBUG("Executed Console Command Response: ", output)
// TODO add some error checking
return output;
}
exports.executeAsyncConsoleCommand = executeAsyncConsoleCommand;
async function returnAlsaDeviceObject() {
const listAlsaDevicesCommand = "arecord -L";
const commandResponse = await executeAsyncConsoleCommand(listAlsaDevicesCommand);
const brokenCommand = String(commandResponse).split('\n');
var devices = [];
var i = 0;
for (const responseLine of brokenCommand) {
if (String(responseLine) && !String(responseLine).match(/^\s/g)) {
const tempDevice = {
id: i,
name: responseLine
}
devices.push(tempDevice);
i += 1;
}
}
return devices;
}
exports.returnAlsaDeviceObject = returnAlsaDeviceObject;

View File

@@ -2,20 +2,20 @@
const { DebugBuilder } = require("../utilities/debugBuilder.js"); const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client", "httpRequests"); const log = new DebugBuilder("client", "httpRequests");
// Config // Config
const config = require("../config/clientConfig"); require('dotenv').config();
// Modules // Modules
const http = require("http"); const http = require("http");
exports.requestOptions = class requestOptions { exports.requestOptions = class requestOptions {
constructor(path, method, hostname = undefined, headers = undefined, port = undefined) { constructor(path, method, hostname = undefined, headers = undefined, port = undefined) {
if (method === "POST"){ if (method === "POST"){
this.hostname = hostname ?? config.serverConfig.hostname this.hostname = hostname ?? process.env.SERVER_HOSTNAME ?? process.env.SERVER_IP;
this.path = path this.path = path;
this.port = port ?? config.serverConfig.port this.port = port ?? process.env.SERVER_PORT;
this.method = method this.method = method;
this.headers = headers ?? { this.headers = headers ?? {
'Content-Type': 'application/json', 'Content-Type': 'application/json',
} };
} }
} }
} }
@@ -31,12 +31,22 @@ exports.sendHttpRequest = function sendHttpRequest(requestOptions, data, callbac
// Create the request // Create the request
const req = http.request(requestOptions, res => { const req = http.request(requestOptions, res => {
res.on('data', (data) => { res.on('data', (data) => {
const responseObject = { if (res.statusCode >= 200 && res.statusCode <= 299) {
"statusCode": res.statusCode, const responseObject = {
"body": JSON.parse(data) "statusCode": res.statusCode,
}; "body": JSON.parse(data)
log.DEBUG("Response Object: ", responseObject); };
callback(responseObject); log.DEBUG("Response Object: ", responseObject);
callback(responseObject);
}
if (res.statusCode >= 300) {
const responseObject = {
"statusCode": res.statusCode,
"body": data
};
log.DEBUG("Response Object: ", responseObject);
callback(responseObject);
}
}) })
}).on('error', err => { }).on('error', err => {
log.ERROR('Error: ', err.message) log.ERROR('Error: ', err.message)
@@ -46,4 +56,18 @@ exports.sendHttpRequest = function sendHttpRequest(requestOptions, data, callbac
// Write the data to the request and send it // Write the data to the request and send it
req.write(data) req.write(data)
req.end() req.end()
}
exports.onHttpError = function onHttpError(httpStatusCode) {
switch(httpStatusCode){
case 404:
// Endpoint not found
log.WARN("404 received");
break;
default:
// Unhandled HTTP error code
log.ERROR("HTTP request returned with status: ", httpStatusCode)
break;
}
} }

View File

@@ -1,9 +0,0 @@
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client", "messageHandler");
exports.replyToInteraction = async function replyToInteraction(interaction, message){
interaction.reply({ content: message, fetchReply: true })
.then((message) => log.DEBUG(`Reply sent with content ${message.content}`))
.catch((err) => log.ERROR(err));
}

View File

@@ -134,7 +134,7 @@ class audioConfig {
"instance_name": "audio0", "instance_name": "audio0",
"device_name": deviceName, "device_name": deviceName,
"udp_port": port, "udp_port": port,
"audio_gain": 1.0, "audio_gain": 2.0,
"number_channels": 1 "number_channels": 1
}]; }];
} }

View File

@@ -1,21 +0,0 @@
exports.calcRmsSync = (arr , n) => {
var square = 0;
var mean = 0;
var root = 0;
// Calculate square.
for (i = 0; i < n; i++) {
square += Math.pow(arr[i], 2);
}
// Calculate Mean.
mean = (square / (n));
// Calculate Root.
root = Math.sqrt(mean);
// Normalize the output
root = root / 10
return root;
}

View File

@@ -6,12 +6,12 @@ const replace = require('replace-in-file');
class Options { class Options {
constructor(key, updatedValue) { constructor(key, updatedValue) {
this.files = "./config/clientConfig.js"; this.files = "./.env";
// A regex of the line containing the key in the config file // A regex of the line containing the key in the config file
this.from = new RegExp(`"${key}": (.+),`, "g"); this.from = new RegExp(`${key}="(.+)",`, "g");
// Check to see if the value is a string and needs to be wrapped in double quotes // Check to see if the value is a string and needs to be wrapped in double quotes
if (typeof updatedValue === "string") this.to = `"${key}": "${updatedValue}",`; if (Array(["string", "number"]).includes(typeof updatedValue)) this.to = `${key}="${updatedValue}",`;
else this.to = `"${key}": ${updatedValue},`; else this.to = `${key}=${updatedValue},`;
} }
} }

View File

@@ -48,12 +48,14 @@ function convertFrequencyToHertz(frequency){
if (Number.isInteger(frequency)) { if (Number.isInteger(frequency)) {
log.DEBUG(`${frequency} is an integer.`); log.DEBUG(`${frequency} is an integer.`);
// Check to see if the frequency has the correct length // Check to see if the frequency has the correct length
if (frequency.toString().length >= 7 && frequency.toString().length <= 9) return frequency if (frequency >= 1000000) return frequency
if (frequency >= 100 && frequency <= 999) return frequency * 1000000
log.WARN("Frequency hasn't matched filters: ", frequency);
} }
else { else {
log.DEBUG(`${frequency} is a float value.`); log.DEBUG(`${frequency} is a float value.`);
// Convert to a string to remove the decimal in place and then correct the length // Convert to a string to remove the decimal in place and then correct the length
return converter(frequency).from("MHz").to("Hz"); return parseInt(converter(frequency).from("MHz").to("Hz"));
} }
} else { } else {
log.DEBUG(`${frequency} is not a number`); log.DEBUG(`${frequency} is not a number`);
@@ -116,5 +118,20 @@ exports.updatePreset = (systemName, callback, { frequencies = undefined, mode =
} }
} }
/**
* Deletes the specified system
*
* @param {string} systemName The name of the system being modified
* @param {function} callback The callback function to be called when the function completes
*/
exports.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

@@ -0,0 +1,264 @@
// Modules
const { promisify } = require('util');
const { exec, spawn } = require("child_process");
const { resolve, dirname } = require('path');
const radioConfigHelper = require("../utilities/radioConfigHelper");
const presetWrappers = require("../utilities/updatePresets");
const converter = require("convert-units");
const fs = require('fs');
require('dotenv').config();
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder.js");
// Global Vars
const log = new DebugBuilder("client", "executeConsoleCommands");
const execCommand = promisify(exec);
const radioBinPath = process.env.OP25_BIN_PATH;
/**
* An object containing the variables needed to run the local node
*/
exports.nodeObject = class nodeObject {
/**
*
* @param {*} param0._id The ID of the node
* @param {*} param0._name The name of the node
* @param {*} param0._ip The IP that the master can contact the node at
* @param {*} param0._port The port that the client is listening on
* @param {*} param0._location The physical location of the node
* @param {*} param0._online An integer representation of the online status of the bot, ie 0=off, 1=on
* @param {*} param0._nearbySystems An object array of nearby systems
*/
constructor({ _id = null, _name = null, _ip = null, _port = null, _location = null, _nearbySystems = null, _online = null }) {
this.id = _id;
this.name = _name;
this.ip = _ip;
this.port = _port;
this.location = _location;
this.nearbySystems = _nearbySystems;
this.online = _online;
}
}
/**
*
* @param {*} consoleCommand
* @returns
*/
exports.executeAsyncConsoleCommand = async function executeAsyncConsoleCommand(consoleCommand) {
// Check to see if the command is a real command
// TODO needs to be improved
const acceptableCommands = [ "arecord -L", 'ipconfig', 'ip addr' ];
if (!acceptableCommands.includes(consoleCommand)) {
log.WARN("Console command is not acceptable: ", consoleCommand);
return undefined;
}
log.DEBUG("Running console command: ", consoleCommand);
const tempOutput = await execCommand(consoleCommand);
const output = tempOutput.stdout.trim();
log.DEBUG("Executed Console Command Response: ", output)
// TODO add some error checking
return output;
}
/**
*
* @param {*} process The process to close
* @returns {undefined} Undefined to replace the existing process in the parent
*/
exports.closeProcessWrapper = async (process) => {
log.INFO("Leaving the server");
if (!process) return undefined;
// Try to close the process gracefully
await process.kill(2);
// Wait 25 seconds and see if the process is still open, if it is force it close
await setTimeout(async () => {
if (process) await process.kill(9);
}, 25000)
return undefined;
}
/**
* This wrapper closes any open radio sessions and the opens a new one
*
* @returns {radioChildProcess} The process of the radio session for use
*/
exports.openRadioSessionWrapper = async (radioChildProcess, presetName) => {
if (radioChildProcess) radioChildProcess = await this.closeProcessWrapper(radioChildProcess);
const configChangeResult = await this.changeCurrentConfigWrapper(presetName);
// Throw an error to the client if the config change ran into an error
if (typeof configChangeResult === "string") return configChangeResult;
if (process.platform === "win32") {
log.DEBUG("Starting Windows OP25");
radioChildProcess = await spawn("C:\\Python310\\python.exe", [getRadioBinPath(), "-c", getRadioConfigPath()], { cwd: dirname(getRadioBinPath()) });
}
else {
log.DEBUG("Starting Linux OP25");
radioChildProcess = await spawn(getRadioBinPath(), ["-c", getRadioConfigPath()], { cwd: dirname(getRadioBinPath()) });
}
log.VERBOSE("Radio Process: ", radioChildProcess);
let fullOutput;
radioChildProcess.stdout.setEncoding('utf8');
radioChildProcess.stdout.on("data", (data) => {
log.VERBOSE("From Process: ", data);
fullOutput += data.toString();
});
radioChildProcess.stderr.on('data', (data) => {
log.VERBOSE(`stderr: ${data}`);
fullOutput += data.toString();
});
radioChildProcess.on('close', (code) => {
log.DEBUG(`child process exited with code ${code}`);
log.VERBOSE("Full output from radio: ", fullOutput);
});
radioChildProcess.on("error", (code, signal) => {
log.ERROR("Error from the radio process: ", code, signal);
});
// Starting the radio application
return radioChildProcess
}
/**
* Update the OP25 config with a preset
*
* @param {*} presetName The preset name to update the OP25 config file with
* @returns
*/
exports.changeCurrentConfigWrapper = async (presetName) => {
// Check if the given config is saved
log.DEBUG("Checking if provided preset is in the config");
const presetIsPresent = await checkIfPresetExists(presetName);
if (!presetIsPresent) return "No preset with given name found in config"; // No preset with the given name is in the config
// Check if the current config is the same as the preset given
try {
const currentConfig = readOP25Config();
if (currentConfig.channels && currentConfig.channels.name === presetName) {
log.DEBUG("Current config is the same as the preset given");
return undefined;
}
}
catch (err) {
log.WARN("Problem reading the config file, overwriting with the new config", err);
}
// Convert radioPreset to OP25 'cfg.json. file
log.DEBUG("Converting radioPreset to OP25 config");
const updatedConfigObject = convertRadioPresetsToOP25Config(presetName);
// Replace current JSON file with the updated file
writeOP25Config(updatedConfigObject, () => {
return updatedConfigObject;
})
}
/**
* Get the location of the 'multi_rx.py' binary from the config
*/
function getRadioBinPath(){
return resolve(radioBinPath);
}
/**
* Get the path of the config for the radio app (OP25) and set the global variable
*/
function getRadioConfigPath(){
let radioConfigDirPath = dirname(getRadioBinPath());
return resolve(`${radioConfigDirPath}/cfg.json`);
}
/**
* Write the given config to the JSON file in OP25 the bin dir
* @param config The full config to be written to the file
* @param {function} callback The function to be called when this wrapper completes
*/
function writeOP25Config(config, callback = undefined) {
log.DEBUG("Updating OP25 config with: ", config);
fs.writeFile(getRadioConfigPath(), JSON.stringify(config), (err) => {
// Error checking
if (err) {
log.ERROR(err);
throw err;
}
log.DEBUG("Write Complete");
if (callback) callback()
});
}
/**
* Get the current config file in use by OP25
* @returns {object|*} The parsed config object currently set in OP25
*/
function readOP25Config() {
const configPath = getRadioConfigPath();
log.DEBUG(`Reading from config path: '${configPath}'`);
const readFile = fs.readFileSync(configPath);
log.VERBOSE("File Contents: ", readFile.toString());
return JSON.parse(readFile);
}
/**
* Check to see if the preset name exists in the config
* @param {string} presetName The system name as saved in the preset
* @returns {true||false}
*/
function checkIfPresetExists(presetName) {
const savedPresets = presetWrappers.getPresets();
log.DEBUG("Found presets: ", savedPresets, presetName, Object.keys(savedPresets).includes(presetName));
if (!Object.keys(savedPresets).includes(presetName)) return false;
else return true;
}
/**
* Convert a radioPreset to OP25's cfg.json file
*/
function convertRadioPresetsToOP25Config(presetName){
const savedPresets = presetWrappers.getPresets();
let frequencyString = "";
for (const frequency of savedPresets[presetName].frequencies){
frequencyString += `${converter(frequency).from("Hz").to("MHz")},`
}
frequencyString = frequencyString.slice(0, -1);
let updatedOP25Config;
switch (savedPresets[presetName].mode){
case "p25":
updatedOP25Config = new radioConfigHelper.P25({
"systemName": presetName,
"controlChannelsString": frequencyString,
"tagsFile": savedPresets[presetName].trunkFile
});
break;
case "nbfm":
//code for nbfm here
updatedOP25Config = new radioConfigHelper.NBFM({
"frequency": frequencyString,
"systemName": presetName
});
break;
default:
throw new Error("Radio mode of selected preset not recognized");
}
log.DEBUG(updatedOP25Config);
return updatedOP25Config;
}

View File

@@ -1 +0,0 @@
DEBUG="server:*";

58
Server/.env.example Normal file
View File

@@ -0,0 +1,58 @@
# Discord Bot Configs
# Bot Token
TOKEN=""
# Client ID
clientId=""
# Prefix (deprecated)
PREFIX="^"
# ID of the Group than Can Admin The Bot
BOT_ADMINS=""
# Default Voice Channel to Join if None are Specified
DEFAULT_VOICE_CHANNEL_ID=""
# HTTP Server Config (DRB_CNC)
# HTTP Port to listen on
HTTP_PORT=3000
# MySQL Config for Emmelia
# Core DB Info and Login
EM_DB_HOST=""
EM_DB_USER=""
EM_DB_PASS=""
EM_DB_NAME=""
# Names of DB Tables
DB_RSS_FEEDS_TABLE="RSSFeeds"
DB_RSS_POSTS_TABLE="RSSPosts"
DB_ACCOUNTS_TABLE="accounts"
DB_TRANSACTIONS_TABLE="transactions"
DB_PRICING_TABLE="pricing"
# MySQL Config for Node Control
NODE_DB_HOST=''
NODE_DB_USER=''
NODE_DB_PASS=''
NODE_DB_NAME=''
# Node Config
# Time betwen check ins with the nodes
NODE_MONITOR_REFRESH_INTERVAL=100000
# RSS Config
# Interval between refreshes
RSS_REFRESH_INTERVAL=3000000
# OpenAI Config
# OpenAI Organization ID
OPENAI_ORG=""
# OpenAI API Key
OPENAI_KEY=""
# Stable Diffusion (Stability AI) Config
# API KEY
STABILITY_API_KEY=""
# General Config
# Exit when the program encounters and error (this may be ignored in some instances, and the error will exit the program either way)
EXIT_ON_ERROR=false
# Delay the exit of the program for X miliseconds, this can be used if you want to see what happens just after the error occurs or see if something else errors
EXIT_ON_ERROR_DELAY=0

3
Server/.gitignore vendored
View File

@@ -3,4 +3,5 @@ node_modules/
package-lock.json package-lock.json
*.bak *.bak
*.log *.log
*._.* *._.*
clientIds.json

View File

@@ -1,10 +0,0 @@
## TODOs
- ~~Create balance command for user to view their balance~~
- ~~Create a command that explains the pricing~~
- ~~Update welcome and insufficient replies ~~
- ~~add a section for the help menu to show items that need tokens~~
- add a limiter to the rss feeds to slow down the sends when multiple updates are occurring
- add a blank .env file to the git
- clean up logging
- ensure documentation for functions
- merge with Discord CnC

View File

@@ -0,0 +1,6 @@
{
"[ID from Discord]": {
"name": "[Nickname of the Bot]",
"id": "[Client ID from Discord Dev Portal]"
}
}

120
Server/commands/join.js Normal file
View File

@@ -0,0 +1,120 @@
// Modules
const { customSlashCommandBuilder } = require('../utilities/customSlashCommandBuilder');
const { DebugBuilder } = require("../utilities/debugBuilder");
const { getMembersInRole, getAllClientIds } = require("../utilities/utils");
const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
const { getOnlineNodes, updateNodeInfo, addNodeConnection, getConnectionByNodeId } = require("../utilities/mysqlHandler");
// Global Vars
const log = new DebugBuilder("server", "join");
/**
* * This wrapper will check if there is an available node with the requested preset and if so checks for an available client ID to join with
*
* @param {*} presetName The preset name to listen to on the client
* @param {*} channelId The channel ID to join the bot to
* @param {*} clientIdsUsed EITHER A collection of clients that are currently connected OR a single discord client ID (NOT dev portal ID) that should be used to join the server with
* @returns
*/
async function joinServerWrapper(presetName, channelId, clientIdsUsed) {
// Get nodes online
var onlineNodes = await new Promise((recordResolve, recordReject) => {
getOnlineNodes((nodeRows) => {
recordResolve(nodeRows);
});
});
// Check which nodes have the selected preset
onlineNodes = onlineNodes.filter(node => node.nearbySystems.includes(presetName));
log.DEBUG("Filtered Online Nodes: ", onlineNodes);
// Check if any nodes with this preset are available
var nodesCurrentlyAvailable = [];
for (const node of onlineNodes) {
const currentConnection = await getConnectionByNodeId(node.id);
log.DEBUG("Checking to see if there is a connection for Node: ", node, currentConnection);
if(!currentConnection) nodesCurrentlyAvailable.push(node);
}
log.DEBUG("Nodes Currently Available: ", nodesCurrentlyAvailable);
// If not, let the user know
if (!nodesCurrentlyAvailable.length > 0) return Error("All nodes with this channel are unavailable, consider swapping one of the currently joined bots.");
// If so, join with the first node
var availableClientIds = await getAllClientIds();
log.DEBUG("All clients: ", Object.keys(availableClientIds));
var selectedClientId;
if (typeof clientIdsUsed === 'string') {
for (const availableClientId of availableClientIds) {
if (availableClientId.discordId != clientIdsUsed ) selectedClientId = availableClientId;
}
}
else {
log.DEBUG("Client IDs Used: ", clientIdsUsed.keys());
for (const usedClientId of clientIdsUsed.keys()) {
log.DEBUG("Used Client ID: ", usedClientId);
availableClientIds = availableClientIds.filter(cid => cid.discordId != usedClientId);
}
log.DEBUG("Available Client IDs: ", availableClientIds);
if (!Object.keys(availableClientIds).length > 0) return log.ERROR("All client ID have been used, consider swapping one of the curretly joined bots or adding more Client IDs to the pool.")
selectedClientId = availableClientIds[0];
}
const selectedNode = nodesCurrentlyAvailable[0];
const reqOptions = new requestOptions("/bot/join", "POST", selectedNode.ip, selectedNode.port);
const postObject = {
"channelId": channelId,
"clientId": selectedClientId.clientId,
"presetName": presetName
};
log.INFO("Post Object: ", postObject);
sendHttpRequest(reqOptions, JSON.stringify(postObject), async (responseObj) => {
log.VERBOSE("Response Object from node ", selectedNode, responseObj);
if (!responseObj || !responseObj.statusCode == 200) return false;
// Node has connected to discord
// Updating node Object in DB
const updatedNode = await updateNodeInfo(selectedNode);
log.DEBUG("Updated Node: ", updatedNode);
// Adding a new node connection
const nodeConnection = await addNodeConnection(selectedNode, selectedClientId);
log.DEBUG("Node Connection: ", nodeConnection);
});
}
exports.joinServerWrapper = joinServerWrapper;
module.exports = {
data: new customSlashCommandBuilder()
.setName('join')
.setDescription('Join the channel you are in with the preset you choose')
.addAllSystemPresetOptions(),
example: "join",
isPrivileged: false,
requiresTokens: false,
defaultTokenUsage: 0,
deferInitialReply: true,
async execute(interaction) {
try{
const guildId = interaction.guild.id;
const presetName = interaction.options.getString('preset');
if (!interaction.member.voice.channel.id) return interaction.editReply(`You need to be in a voice channel, ${interaction.user}`)
const channelId = interaction.member.voice.channel.id;
log.DEBUG(`Join requested by: ${interaction.user.username}, to: '${presetName}', in channel: ${channelId} / ${guildId}`);
const onlineBots = await getMembersInRole(interaction);
log.DEBUG("Online Bots: ", onlineBots);
await joinServerWrapper(presetName, channelId, onlineBots.online);
await interaction.editReply('**Pong.**');
//await interaction.channel.send('**Pong.**'); // This will send a message to the channel of the interaction outside of the initial reply
}catch(err){
log.ERROR(err)
//await interaction.reply(err.toString());
}
}
}

77
Server/commands/leave.js Normal file
View File

@@ -0,0 +1,77 @@
// Modules
const { customSlashCommandBuilder } = require('../utilities/customSlashCommandBuilder');
const { DebugBuilder } = require("../utilities/debugBuilder");
const { getAllClientIds, getKeyByArrayValue } = require("../utilities/utils");
const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
const { checkNodeConnectionByClientId, removeNodeConnectionByNodeId, updateNodeInfo, getConnectedNodes, getAllConnections } = require('../utilities/mysqlHandler');
// Global Vars
const log = new DebugBuilder("server", "leave");
const logAC = new DebugBuilder("server", "leave_autocorrect");
async function leaveServerWrapper(clientIdObject) {
if (!clientIdObject.clientId || !clientIdObject.name) return log.ERROR("Tried to leave server without client ID and/or Name");
const node = await checkNodeConnectionByClientId(clientIdObject);
reqOptions = new requestOptions("/bot/leave", "POST", node.ip, node.port);
const responseObj = await new Promise((recordResolve, recordReject) => {
sendHttpRequest(reqOptions, JSON.stringify({}), async (responseObj) => {
recordResolve(responseObj);
});
});
log.VERBOSE("Response Object from node ", node, responseObj);
if (!responseObj || !responseObj.statusCode == 202) return false;
// Node has disconnected from discor
// Removing the node connection from the DB
const removedConnection = removeNodeConnectionByNodeId(node.id);
log.DEBUG("Removed Node Connection: ", removedConnection);
return;
}
exports.leaveServerWrapper = leaveServerWrapper;
module.exports = {
data: new customSlashCommandBuilder()
.setName('leave')
.setDescription('Disconnect a bot from the server')
.addStringOption(option =>
option.setName("bot")
.setDescription("The bot to disconnect from the server")
.setAutocomplete(true)),
example: "leave",
isPrivileged: false,
requiresTokens: false,
defaultTokenUsage: 0,
deferInitialReply: true,
async autocomplete(interaction) {
const focusedValue = interaction.options.getFocused();
const connections = await getAllConnections();
const filtered = connections.filter(conn => String(conn.clientObject.name).startsWith(focusedValue)).map(conn => conn.clientObject.name);
logAC.DEBUG("Focused Value: ", focusedValue, connections, filtered);
await interaction.respond(
filtered.map(option => ({ name: option, value: option })),
);
},
async execute(interaction) {
try{
const guildId = interaction.guild.id;
const botName = interaction.options.getString('bot');
log.DEBUG("Bot Name: ", botName)
const clinetIds = await getAllClientIds();
log.DEBUG("Client names: ", clinetIds);
const clientDiscordId = getKeyByArrayValue(clinetIds, {'name': botName});
log.DEBUG("Selected bot: ", clinetIds[clientDiscordId]);
// Need to create a table in DB to keep track of what bots have what IDs or an endpoint on the clients to return what ID they are running with
await leaveServerWrapper(clinetIds[clientDiscordId]);
await interaction.editReply(`**${clinetIds[clientDiscordId].name}** has been disconnected`); // This will reply to the initial interaction
//await interaction.channel.send('**word.**'); // This will send a message to the channel of the interaction outside of the initial reply
}catch(err){
log.ERROR(err)
//await interaction.reply(err.toString());
}
}
}

View File

@@ -18,6 +18,9 @@ module.exports = {
requiresTokens: false, requiresTokens: false,
defaultTokenUsage: 0, defaultTokenUsage: 0,
deferInitialReply: false, deferInitialReply: false,
/*async autocomplete(interaction) {
const focusedValue = interaction.options.getFocused();
},*/
async execute(interaction) { async execute(interaction) {
try{ try{
await interaction.channel.send('**Pong.**'); // TODO - Add insults as the response to this command await interaction.channel.send('**Pong.**'); // TODO - Add insults as the response to this command

View File

@@ -6,6 +6,7 @@ const {getAllNodes, addNewNode, updateNodeInfo, getNodeInfoFromId, getOnlineNode
const utils = require("../utilities/utils"); const utils = require("../utilities/utils");
const { sendHttpRequest, requestOptions } = require("../utilities/httpRequests.js"); const { sendHttpRequest, requestOptions } = require("../utilities/httpRequests.js");
const { nodeObject } = require("../utilities/recordHelper.js"); const { nodeObject } = require("../utilities/recordHelper.js");
const { joinServerWrapper } = require("../commands/join");
const refreshInterval = process.env.NODE_MONITOR_REFRESH_INTERVAL ?? 1200000; const refreshInterval = process.env.NODE_MONITOR_REFRESH_INTERVAL ?? 1200000;
@@ -90,6 +91,21 @@ exports.nodeCheckIn = async (req, res) => {
} }
/**
* Request the node to join the specified server/channel and listen to the specified resource
*
* @param req.body.clientId The client ID to join discord with (NOT dev portal ID, right click user -> Copy ID)
* @param req.body.channelId The Channel ID to join in Discord
* @param req.body.presetName The preset name to listen to in Discord
*/
exports.requestNodeJoinServer = async (req, res) => {
if (!req.body.clientId || !req.body.channelId || !req.body.presetName) return res.status(400).json("Missing information in request, requires clientId, channelId, presetName");
await joinServerWrapper(req.body.presetName, req.body.channelId, req.body.clientId)
}
/**
* The node monitor service, this will periodically check in on the online nodes to make sure they are still online
*/
exports.nodeMonitorService = class nodeMonitorService { exports.nodeMonitorService = class nodeMonitorService {
constructor() { constructor() {
} }

View File

@@ -8,45 +8,53 @@ const log = new DebugBuilder("server", "interactionCreate");
module.exports = { module.exports = {
name: Events.InteractionCreate, name: Events.InteractionCreate,
async execute(interaction) { async execute(interaction) {
const command = interaction.client.commands.get(interaction.commandName);
log.VERBOSE("Interaction for command: ", command);
// Execute autocomplete if the user is checking autocomplete
if (interaction.isAutocomplete()) {
log.DEBUG("Running autocomplete for command: ", command.data.name);
return await command.autocomplete(interaction);
}
// Check if the interaction is a command
if (!interaction.isChatInputCommand()) return; if (!interaction.isChatInputCommand()) return;
const command = interaction.client.commands.get(interaction.commandName); if (!command) {
log.ERROR(`No command matching ${interaction.commandName} was found.`);
return;
}
if (!command) { log.DEBUG(`${interaction.member.user} is running '${interaction.commandName}'`);
log.ERROR(`No command matching ${interaction.commandName} was found.`);
return;
}
log.DEBUG(`${interaction.member.user} is running '${interaction.commandName}'`); await authorizeCommand(interaction, command, async () => {
await authorizeTokenUsage(interaction, command, undefined, async () => {
await authorizeCommand(interaction, command, async () => { try {
await authorizeTokenUsage(interaction, command, undefined, async () => { if (command.deferInitialReply) {
try { try {
if (command.deferInitialReply) { if (interaction.options.getBool('public') && interaction.options.getBool('public') == false) await interaction.deferReply({ ephemeral: true });
try { else await interaction.deferReply({ ephemeral: false });
if (interaction.options.getBool('public') && interaction.options.getBool('public') == false) await interaction.deferReply({ ephemeral: true });
else await interaction.deferReply({ ephemeral: false });
}
catch (err) {
if (err instanceof TypeError) {
// The public option doesn't exist in this command
await interaction.deferReply({ ephemeral: false });
} else {
throw err;
} }
catch (err) {
if (err instanceof TypeError) {
// The public option doesn't exist in this command
await interaction.deferReply({ ephemeral: false });
} else {
throw err;
}
}
}
command.execute(interaction);
} catch (error) {
log.ERROR(error);
if (interaction.replied || interaction.deferred) {
interaction.followUp({ content: 'There was an error while executing this command!', ephemeral: true });
} else {
interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
} }
}
command.execute(interaction);
} catch (error) {
log.ERROR(error);
if (interaction.replied || interaction.deferred) {
interaction.followUp({ content: 'There was an error while executing this command!', ephemeral: true });
} else {
interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
} }
} })
}) });
});
}, },
}; };

View File

@@ -9,6 +9,6 @@ const log = new DebugBuilder("server", "messageCreate");
module.exports = { module.exports = {
name: Events.MessageCreate, name: Events.MessageCreate,
async execute(interaction) { async execute(interaction) {
await linkCop(interaction); //await linkCop(interaction);
}, },
}; };

View File

@@ -26,7 +26,7 @@ const {
} = require('discord.js'); } = require('discord.js');
const client = new Client({ const client = new Client({
intents: [GatewayIntentBits.GuildMessages, GatewayIntentBits.Guilds] intents: [GatewayIntentBits.GuildMessages, GatewayIntentBits.Guilds, GatewayIntentBits.GuildPresences, GatewayIntentBits.GuildMembers]
}); });
prefix = process.env.PREFIX prefix = process.env.PREFIX
@@ -117,11 +117,22 @@ const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('
//const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js')); //const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));
for (const file of commandFiles) { for (const file of commandFiles) {
const filePath = path.join(commandsPath, file); const filePath = path.join(commandsPath, file);
const command = require(filePath); const command = require(filePath);
log.DEBUG("Importing command: ", command.data.name); if (command.data instanceof Promise) {
// Set a new item in the Collection command.data.then(async (builder) => {
// With the key as the command name and the value as the exported module command.data = builder;
client.commands.set(command.data.name, command); log.DEBUG("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
client.commands.set(command.data.name, command);
});
}
else {
log.DEBUG("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
client.commands.set(command.data.name, command);
}
} }
// Run when the bot is ready // Run when the bot is ready
@@ -137,7 +148,7 @@ client.on('ready', () => {
runHTTPServer(); runHTTPServer();
log.DEBUG("Starting Node Monitoring Service"); log.DEBUG("Starting Node Monitoring Service");
runNodeMonitorService(); //runNodeMonitorService();
log.DEBUG("Starting RSS watcher"); log.DEBUG("Starting RSS watcher");
runRssService(); runRssService();

View File

@@ -4,6 +4,7 @@ const log = new DebugBuilder("server", "libUtils");
const { NodeHtmlMarkdown } = require('node-html-markdown'); const { NodeHtmlMarkdown } = require('node-html-markdown');
const { parse } = require("node-html-parser"); const { parse } = require("node-html-parser");
const crypto = require("crypto"); const crypto = require("crypto");
require('dotenv').config();
const imageRegex = /(http(s?):)([/|.|\w|\s|-])*((\.(?:jpg|gif|png|webm))|(\/gallery\/(?:[/|.|\w|\s|-])*))/g; const imageRegex = /(http(s?):)([/|.|\w|\s|-])*((\.(?:jpg|gif|png|webm))|(\/gallery\/(?:[/|.|\w|\s|-])*))/g;
const youtubeVideoRegex = /((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube(-nocookie)?\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)/g const youtubeVideoRegex = /((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube(-nocookie)?\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)/g
@@ -55,6 +56,7 @@ exports.onError = (error) => {
throw error; throw error;
} }
var port = process.env.HTTP_PORT;
var bind = typeof port === 'string' var bind = typeof port === 'string'
? 'Pipe ' + port ? 'Pipe ' + port
: 'Port ' + port; : 'Port ' + port;

View File

@@ -0,0 +1,189 @@
import re
import json
import pandas as pd
import requests
import os
import random
from fake_useragent import UserAgent
from bs4 import BeautifulSoup
from urllib.parse import urlparse, unquote, parse_qs
from time import sleep
ua = UserAgent()
#simply scrape
def scrape(url,**kwargs):
session=requests.Session()
session.headers.update({
'User-Agent': ua.random,
"authority": "www.zillow.com",
"accept": "*/*",
"accept-language": "en-US,en;q=0.9",
"cache-control": "no-cache",
"sec-ch-ua-mobile": "?0",
"sec-ch-ua-platform": '^\^"Windows^\^"',
"sec-fetch-dest": "empty",
"sec-fetch-mode": "cors",
"sec-fetch-site": "same-origin",
})
response=session.get(url,**kwargs)
return response
# Return all sections with key and attributes
def slurp(html, tag, attributes):
return BeautifulSoup(html, features="html.parser").findAll(tag, attributes)
# Returns the first number group from a given string
def return_numbers(string):
return int(re.findall(r'\d+', string)[0])
class Listing:
def __init__(self, address, bedrooms, bathrooms, sqft, price, link):
self.address = address
self.bedrooms = bedrooms
self.bathrooms = bathrooms
self.sqft = sqft
self.price = price
self.link = link
class ScrapeZillowListings:
def __init__(self, url):
self.parsed_original_url = self.init_check_url(urlparse(url))
self.html = scrape(url).text
self.listings = []
def init_check_url(self, parsed_url):
# Check to see if we are requesting listResults
print(parsed_url)
print(unquote(parsed_url.query))
print(parse_qs(parsed_url.query)['wants'])
for want in parse_qs(parsed_url.query)['wants']:
print(unquote(unquote(want)))
return parsed_url
def run(self):
self.listings.extend(self.scrape_listings(self.html))
pages = []
for page_nav in slurp(self.html, "nav", {"role":"navigation", "aria-label":"Pagination"}):
page_nav = f"<html><head><head/><body>{page_nav}<body/><html/>"
pages_list = slurp(page_nav, "li", {})
for page in pages_list:
if re.match("\d{1,2}", page.text) and not page.text == "1":
parsed_url = self.setup_url(page.find('a').get('href'))
sleep(random.randint(0,15))
temp_html = scrape(parsed_url.geturl()).text
self.listings.extend(self.scrape_listings(temp_html))
return self.listings
def print_listings(self):
index = 0
for temp_listing in self.listings:
print("--------")
print(f"Listing #{index}")
print(temp_listing.address)
print(temp_listing.price)
print(temp_listing.bedrooms)
print(temp_listing.bathrooms)
print(temp_listing.sqft)
print(temp_listing.link)
print("--------")
index += 1
def scrape_listings(self, html):
temp_listings = []
for listing in slurp(html, "article", {"data-test":"property-card"}):
listing = f"<html><head><head/><body>{listing}<body/><html/>"
uls = slurp(listing, "li", {})
beds = 0
baths = 0
sqft = 0
for ul in uls:
ul = ul.get_text()
if ("bds" in str(ul)):
beds = return_numbers(ul)
if ("ba" in str(ul)):
baths = return_numbers(ul)
if ("sqft" in str(ul)):
sqft = return_numbers(ul)
temp_listings.append(Listing(
address=slurp(listing, "address", {"data-test":"property-card-addr"})[0].get_text(),
bedrooms=beds,
bathrooms=baths,
sqft=sqft,
price=slurp(listing, "span", {"data-test":"property-card-price"})[0].get_text(),
link=slurp(listing, "a", {"data-test":"property-card-link"})[0].get('href'),
))
return temp_listings
def setup_url(self, url):
parsed_url = urlparse(url)
print(parsed_url)
if not parsed_url.netloc:
return urlparse(f"{self.parsed_original_url.scheme}://{self.parsed_original_url.netloc}{parsed_url.path}{self.parsed_original_url.query}{self.parsed_original_url.params}")
#create dataframe
def etl(response):
#regex to find the data
for listing in listings:
print("--------")
print(listing)
print("--------")
print("FORCE STOP")
exit()
#convert text to dict via json
dicts=[json.loads('{'+i+'}') for i in num]
#create dataframe
df=pd.DataFrame()
for ind,val in enumerate(text):
df[val]=dicts[ind].values()
df.index=dicts[ind].keys()
return df
def main():
#scrapper = ScrapeZillowListings('https://www.zillow.com/westchester-county-ny/?searchQueryState=%7B%22usersSearchTerm%22%3A%22Yorktown%20Heights%2C%20NY%22%2C%22mapBounds%22%3A%7B%22north%22%3A41.69948153143324%2C%22east%22%3A-72.68804025585938%2C%22south%22%3A40.83865274682678%2C%22west%22%3A-74.29479074414063%7D%2C%22isMapVisible%22%3Atrue%2C%22filterState%22%3A%7B%22price%22%3A%7B%22max%22%3A250000%7D%2C%22ah%22%3A%7B%22value%22%3Atrue%7D%2C%22sort%22%3A%7B%22value%22%3A%22days%22%7D%2C%22land%22%3A%7B%22value%22%3Afalse%7D%2C%22cmsn%22%3A%7B%22value%22%3Afalse%7D%2C%22sche%22%3A%7B%22value%22%3Afalse%7D%2C%22schm%22%3A%7B%22value%22%3Afalse%7D%2C%22schh%22%3A%7B%22value%22%3Afalse%7D%2C%22schp%22%3A%7B%22value%22%3Afalse%7D%2C%22schr%22%3A%7B%22value%22%3Afalse%7D%2C%22schc%22%3A%7B%22value%22%3Afalse%7D%2C%22schu%22%3A%7B%22value%22%3Afalse%7D%7D%2C%22isListVisible%22%3Atrue%2C%22regionSelection%22%3A%5B%7B%22regionId%22%3A3148%2C%22regionType%22%3A4%7D%2C%7B%22regionId%22%3A2694%2C%22regionType%22%3A4%7D%5D%2C%22pagination%22%3A%7B%7D%7D')
scrapper = ScrapeZillowListings("https://www.zillow.com/search/GetSearchPageState.htm?searchQueryState=^%^7B^%^22pagination^%^22^%^3A^%^7B^%^7D^%^2C^%^22usersSearchTerm^%^22^%^3A^%^22Yorktown^%^20Heights^%^2C^%^20NY^%^22^%^2C^%^22mapBounds^%^22^%^3A^%^7B^%^22north^%^22^%^3A42.99146217894271^%^2C^%^22east^%^22^%^3A-70.80209903627659^%^2C^%^22south^%^22^%^3A39.549453943310084^%^2C^%^22west^%^22^%^3A-77.00937442690159^%^7D^%^2C^%^22mapZoom^%^22^%^3A8^%^2C^%^22regionSelection^%^22^%^3A^%^5B^%^7B^%^22regionId^%^22^%^3A3148^%^2C^%^22regionType^%^22^%^3A4^%^7D^%^2C^%^7B^%^22regionId^%^22^%^3A2694^%^2C^%^22regionType^%^22^%^3A4^%^7D^%^5D^%^2C^%^22isMapVisible^%^22^%^3Atrue^%^2C^%^22filterState^%^22^%^3A^%^7B^%^22price^%^22^%^3A^%^7B^%^22max^%^22^%^3A250000^%^7D^%^2C^%^22isAllHomes^%^22^%^3A^%^7B^%^22value^%^22^%^3Atrue^%^7D^%^2C^%^22sortSelection^%^22^%^3A^%^7B^%^22value^%^22^%^3A^%^22days^%^22^%^7D^%^2C^%^22isLotLand^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isMiddleSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isHighSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22includeUnratedSchools^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isComingSoon^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isPublicSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isPrivateSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isElementarySchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isCharterSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^7D^%^2C^%^22isListVisible^%^22^%^3Atrue^%^7D&wants=^\{^%^22cat1^%^22:^\[^%^22mapResults^%^22^\]^\}&requestId=3")
listings = scrapper.run()
scrapper.print_listings()
#df=etl(response)
return
if __name__ == "__main__":
main()
#curl "https://www.zillow.com/search/GetSearchPageState.htm?searchQueryState=^%^7B^%^22pagination^%^22^%^3A^%^7B^%^7D^%^2C^%^22usersSearchTerm^%^22^%^3A^%^22Yorktown^%^20Heights^%^2C^%^20NY^%^22^%^2C^%^22mapBounds^%^22^%^3A^%^7B^%^22north^%^22^%^3A42.99146217894271^%^2C^%^22east^%^22^%^3A-70.80209903627659^%^2C^%^22south^%^22^%^3A39.549453943310084^%^2C^%^22west^%^22^%^3A-77.00937442690159^%^7D^%^2C^%^22mapZoom^%^22^%^3A8^%^2C^%^22regionSelection^%^22^%^3A^%^5B^%^7B^%^22regionId^%^22^%^3A3148^%^2C^%^22regionType^%^22^%^3A4^%^7D^%^2C^%^7B^%^22regionId^%^22^%^3A2694^%^2C^%^22regionType^%^22^%^3A4^%^7D^%^5D^%^2C^%^22isMapVisible^%^22^%^3Atrue^%^2C^%^22filterState^%^22^%^3A^%^7B^%^22price^%^22^%^3A^%^7B^%^22max^%^22^%^3A250000^%^7D^%^2C^%^22isAllHomes^%^22^%^3A^%^7B^%^22value^%^22^%^3Atrue^%^7D^%^2C^%^22sortSelection^%^22^%^3A^%^7B^%^22value^%^22^%^3A^%^22days^%^22^%^7D^%^2C^%^22isLotLand^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isMiddleSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isHighSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22includeUnratedSchools^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isComingSoon^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isPublicSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isPrivateSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isElementarySchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isCharterSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^7D^%^2C^%^22isListVisible^%^22^%^3Atrue^%^7D&wants=^\{^%^22cat1^%^22:^\[^%^22mapResults^%^22^\]^\}&requestId=3",
# "authority: www.zillow.com",
# "accept: */*",
# "accept-language: en-US,en;q=0.9",
# "cache-control: no-cache",
# "cookie: JSESSIONID=97FD1EB701E102B7353E8EA4528843CE; zguid=24^|^%^24825bd6e9-4f90-46df-a475-4a9910b5847c; zgsession=1^|a6a5b7ca-c651-45a2-93c8-c5b66fea68d3; AWSALB=oQ3DGTMPgyQOTPA6zLmQ0liqJ1oax2QoQ5rUSCsORkWP52C7k6G8H1gZnlxOtgU/zzO503UHUnQ7tUeivhOnupv7aYI6+E5LxUZl4TeE0JyhvT3pZ6LYeC9iFbTw; AWSALBCORS=oQ3DGTMPgyQOTPA6zLmQ0liqJ1oax2QoQ5rUSCsORkWP52C7k6G8H1gZnlxOtgU/zzO503UHUnQ7tUeivhOnupv7aYI6+E5LxUZl4TeE0JyhvT3pZ6LYeC9iFbTw; search=6^|1689549806090^%^7Crect^%^3D42.92311815473404^%^252C-70.80209903627659^%^252C39.62142250427077^%^252C-77.00937442690159^%^26rid^%^3D2694^%^26disp^%^3Dmap^%^26mdm^%^3Dauto^%^26p^%^3D1^%^26sort^%^3Ddays^%^26z^%^3D1^%^26listPriceActive^%^3D1^%^26type^%^3Dhouse^%^252Ccondo^%^252Capartment_duplex^%^252Cmobile^%^252Ctownhouse^%^26lt^%^3Dfsba^%^252Cfsbo^%^252Cfore^%^252Cnew^%^252Cauction^%^26price^%^3D0-250000^%^26fs^%^3D1^%^26fr^%^3D0^%^26mmm^%^3D0^%^26rs^%^3D0^%^26ah^%^3D0^%^26singlestory^%^3D0^%^26housing-connector^%^3D0^%^26abo^%^3D0^%^26garage^%^3D0^%^26pool^%^3D0^%^26ac^%^3D0^%^26waterfront^%^3D0^%^26finished^%^3D0^%^26unfinished^%^3D0^%^26cityview^%^3D0^%^26mountainview^%^3D0^%^26parkview^%^3D0^%^26waterview^%^3D0^%^26hoadata^%^3D1^%^26zillow-owned^%^3D0^%^263dhome^%^3D0^%^26featuredMultiFamilyBuilding^%^3D0^%^26commuteMode^%^3Ddriving^%^26commuteTimeOfDay^%^3Dnow^%^09^%^092694^%^09^%^09^%^09^%^09^%^09^%^09",
# "pragma: no-cache",
# "sec-ch-ua: ^\^"Not.A/Brand^\^";v=^\^"8^\^", ^\^"Chromium^\^";v=^\^"114^\^", ^\^"Google Chrome^\^";v=^\^"114^\^"",
# "sec-ch-ua-mobile: ?0",
# "sec-ch-ua-platform: ^\^"Windows^\^"",
# "sec-fetch-dest: empty",
# "sec-fetch-mode: cors",
# "sec-fetch-site: same-origin",
# "user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
# "x-kl-ajax-request: Ajax_Request",
# --compressed

View File

@@ -0,0 +1,4 @@
pandas
requests
fake-useragent
beautifulsoup4

1713
Server/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,30 +4,30 @@
"description": "Discord RSS News Bot", "description": "Discord RSS News Bot",
"main": "index.js", "main": "index.js",
"dependencies": { "dependencies": {
"@discordjs/builders": "~1.4.0", "@discordjs/builders": "^1.6.3",
"@discordjs/rest": "~1.5.0", "@discordjs/rest": "^1.7.1",
"axios": "~1.3.4", "axios": "^1.4.0",
"chatgpt": "~4.7.2", "chatgpt": "^5.2.4",
"cookie-parser": "~1.4.4", "cookie-parser": "^1.4.6",
"debug": "~2.6.9", "debug": "^4.3.4",
"discord-api-types": "~0.37.35", "discord-api-types": "^0.37.42",
"discord.js": "~14.7.1", "discord.js": "^14.11.0",
"dotenv": "~16.0.3", "dotenv": "^16.0.3",
"ejs": "~2.6.1", "ejs": "^3.1.9",
"express": "~4.18.2", "express": "^4.18.2",
"fs": "~0.0.1-security", "fs": "^0.0.1-security",
"gpt-3-encoder": "~1.1.4", "gpt-3-encoder": "^1.1.4",
"http-errors": "~1.6.3", "http-errors": "*",
"jsdoc": "~3.6.7", "jsdoc": "^4.0.2",
"jsonfile": "~6.1.0", "jsonfile": "^6.1.0",
"morgan": "~1.9.1", "morgan": "^1.10.0",
"mysql": "~2.18.1", "mysql": "^2.18.1",
"node-html-markdown": "~1.3.0", "node-html-markdown": "^1.3.0",
"node-html-parser": "~6.1.5", "node-html-parser": "^6.1.5",
"openai": "~3.1.0", "openai": "^3.2.1",
"parse-files": "~0.1.1", "parse-files": "^0.1.1",
"rss-parser": "~3.12.0", "rss-parser": "^3.13.0",
"user-agents": "~1.0.1303" "user-agents": "^1.0.1393"
}, },
"scripts": { "scripts": {
"test": "echo \"Error: no test specified\" && exit 1", "test": "echo \"Error: no test specified\" && exit 1",

View File

@@ -28,4 +28,8 @@ router.get('/nodeInfo', nodesController.getNodeInfo);
// Client checkin with the server to update information // Client checkin with the server to update information
router.post('/nodeCheckIn', nodesController.nodeCheckIn); router.post('/nodeCheckIn', nodesController.nodeCheckIn);
// TODO Need to authenticate this request
// Request a particular client to join a particular channel listening to a particular preset
router.post('/joinServer', nodesController.requestNodeJoinServer);
module.exports = router; module.exports = router;

View File

@@ -0,0 +1,46 @@
const { SlashCommandBuilder, SlashCommandStringOption } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const { BufferToJson } = require("../utilities/utils");
const log = new DebugBuilder("server", "customSlashCommandBuilder");
const { getAllNodes, getAllNodesSync } = require("../utilities/mysqlHandler");
exports.customSlashCommandBuilder = class customSlashCommandBuilder extends SlashCommandBuilder {
constructor() {
super();
}
async addAllSystemPresetOptions() {
const nodeObjects = await new Promise((recordResolve, recordReject) => {
getAllNodes((nodeRows) => {
recordResolve(nodeRows);
});
});
log.DEBUG("Node objects: ", nodeObjects);
var presetsAvailable = [];
for (const nodeObject of nodeObjects) {
log.DEBUG("Node object: ", nodeObject);
for (const presetName in nodeObject.nearbySystems) presetsAvailable.push(nodeObject.nearbySystems[presetName]);
}
log.DEBUG("All Presets available: ", presetsAvailable);
// Remove duplicates
presetsAvailable = [...new Set(presetsAvailable)];
log.DEBUG("DeDuped Presets available: ", presetsAvailable);
this.addStringOption(option => option.setName("preset").setRequired(true).setDescription("The channels"));
for (const preset of presetsAvailable){
log.DEBUG("Preset: ", preset);
this.options[0].addChoices({
'name': String(preset),
'value': String(preset)
});
}
log.DEBUG("Preset Options: ", this);
return this;
}
}

View File

@@ -22,6 +22,7 @@ exports.deploy = (clientId, guildIDs) => {
// Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment // Grab the SlashCommandBuilder#toJSON() output of each command's data for deployment
for (const file of commandFiles) { for (const file of commandFiles) {
const command = require(`${path.resolve(commandsPath, file)}`); const command = require(`${path.resolve(commandsPath, file)}`);
log.VERBOSE('Deploying Command: ', command);
commands.push(command.data.toJSON()); commands.push(command.data.toJSON());
} }

View File

@@ -3,6 +3,7 @@ const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("server", "httpRequests"); const log = new DebugBuilder("server", "httpRequests");
// Modules // Modules
const http = require("http"); const http = require("http");
const { isJsonString } = require("./utils.js");
exports.requestOptions = class requestOptions { exports.requestOptions = class requestOptions {
/** /**
@@ -39,7 +40,7 @@ exports.sendHttpRequest = function sendHttpRequest(requestOptions, data, callbac
res.on('data', (data) => { res.on('data', (data) => {
const responseObject = { const responseObject = {
"statusCode": res.statusCode, "statusCode": res.statusCode,
"body": (requestOptions.method === "POST") ? JSON.parse(data) : data.toString() "body": (isJsonString(data.toString)) ? JSON.parse(data) : data.toString()
}; };
log.DEBUG("Response Object: ", responseObject); log.DEBUG("Response Object: ", responseObject);
callback(responseObject); callback(responseObject);

View File

@@ -1,6 +1,11 @@
require('dotenv').config(); require('dotenv').config();
const mysql = require('mysql'); const mysql = require('mysql');
const utils = require('./utils'); const utils = require('./utils');
const { nodeObject, clientObject, connectionObject } = require("./recordHelper");
const { DebugBuilder } = require("../utilities/debugBuilder");
const { BufferToJson, getClientObjectByClientID } = require("../utilities/utils");
const log = new DebugBuilder("server", "mysSQLHandler");
const connection = mysql.createPool({ const connection = mysql.createPool({
host: process.env.NODE_DB_HOST, host: process.env.NODE_DB_HOST,
@@ -10,24 +15,93 @@ const connection = mysql.createPool({
}); });
const nodesTable = `${process.env.NODE_DB_NAME}.nodes`; const nodesTable = `${process.env.NODE_DB_NAME}.nodes`;
const nodeConnectionsTable = `${process.env.NODE_DB_NAME}.node_connections`;
/**
* Return a node object from a single SQL row
*
* @param {object} row The row to convert to a node object
* @returns {nodeObject} The converted node object to be used downstream
*/
function returnNodeObjectFromRow(row) {
return new nodeObject({
_id: row.id,
_name: row.name,
_ip: row.ip,
_port: row.port,
_location: row.location,
_nearbySystems: BufferToJson(row.nearbySystems),
_online: (row.online === 1) ? true : false,
});
}
/**
* Wrapper to convert an array of rows to an array of nodeObjects
*
* @param {array} rows The array of SQL results to be converted into node objects
* @returns {array} An array of node objects
*/
function returnNodeObjectFromRows(rows) {
var i = 0;
for (var row of rows){
log.DEBUG("Row: ", row);
rows[i] = returnNodeObjectFromRow(row);
i += 1;
}
log.DEBUG("Converted Objects from Rows: ", rows);
return rows;
}
/**
* Returns a connection object from an SQL row
*
* @param {*} row The SQL row to convert to a connection object
* @returns {connectionObject}
*/
async function returnConnectionObjectFromRow(row) {
if (Array.isArray(row)) row = row[0]
log.DEBUG("Connection row: ", row);
return new connectionObject({
_connection_id: row.connection_id,
_node: await getNodeInfoFromId(row.id),
_client_object: await getClientObjectByClientID(row.discord_client_id)
});
}
/** Get all nodes the server knows about regardless of status /** Get all nodes the server knows about regardless of status
* @param {*} callback Callback function * @param {*} callback Callback function
*/ */
exports.getAllNodes = (callback) => { exports.getAllNodes = (callback) => {
const sqlQuery = `SELECT * FROM ${nodesTable}` const sqlQuery = `SELECT * FROM ${nodesTable}`
runSQL(sqlQuery, (rows) => { runSQL(sqlQuery, (rows) => {
return callback(rows); if(!rows || rows.length == 0) callback(undefined);
return callback(returnNodeObjectFromRows(rows));
}) })
} }
/**
* Get all Nodes synchronously **May not be working**
*
* @returns
*/
exports.getAllNodesSync = async () => {
const sqlQuery = `SELECT * FROM ${nodesTable}`
const rows = await runSQL(sqlQuery);
console.log("Rows: ", rows);
return returnNodeObjectFromRows(rows);
}
/** Get all nodes that have the online status set true (are online) /** Get all nodes that have the online status set true (are online)
* @param callback Callback function * @param callback Callback function
*/ */
exports.getOnlineNodes = (callback) => { exports.getOnlineNodes = (callback) => {
const sqlQuery = `SELECT * FROM ${nodesTable} WHERE online = 1;` const sqlQuery = `SELECT * FROM ${nodesTable} WHERE online = 1;`
runSQL(sqlQuery, (rows) => { runSQL(sqlQuery, (rows) => {
return callback(rows); return callback(returnNodeObjectFromRows(rows));
}) })
} }
@@ -35,45 +109,60 @@ exports.getOnlineNodes = (callback) => {
* @param nodeId The ID of the node * @param nodeId The ID of the node
* @param callback Callback function * @param callback Callback function
*/ */
exports.getNodeInfoFromId = (nodeId, callback) => { async function getNodeInfoFromId(nodeId, callback = undefined) {
if (!nodeId) throw new Error("No node ID given when trying to fetch node");
log.DEBUG("Getting node from ID: ", nodeId);
const sqlQuery = `SELECT * FROM ${nodesTable} WHERE id = ${nodeId}` const sqlQuery = `SELECT * FROM ${nodesTable} WHERE id = ${nodeId}`
runSQL(sqlQuery, (rows) => {
// Call back the first (and theoretically only) row const sqlResponse = await new Promise((recordResolve, recordReject) => {
// Specify 0 so downstream functions don't have to worry about it runSQL(sqlQuery, (rows) => {
return callback(rows[0]); recordResolve(rows);
}) })
});
// Call back the first (and theoretically only) row
// Specify 0 so downstream functions don't have to worry about it
return (callback) ? callback(returnNodeObjectFromRow(sqlResponse[0])) : returnNodeObjectFromRow(sqlResponse[0]);
} }
exports.getNodeInfoFromId = getNodeInfoFromId
/** Add a new node to the DB /** Add a new node to the DB
* @param nodeObject Node information object * @param nodeObject Node information object
* @param callback Callback function * @param callback Callback function
*/ */
exports.addNewNode = (nodeObject, callback) => { exports.addNewNode = async (nodeObject, callback) => {
if (!nodeObject.name) throw new Error("No name provided"); if (!nodeObject.name) throw new Error("No name provided");
const name = nodeObject.name, const name = nodeObject.name,
ip = nodeObject.ip, ip = nodeObject.ip,
port = nodeObject.port, port = nodeObject.port,
location = nodeObject.location, location = nodeObject.location,
nearbySystems = utils.JsonToBuffer(nodeObject.nearbySystems), nearbySystems = utils.JsonToBuffer(nodeObject.nearbySystems),
online = nodeObject.online; online = nodeObject.online,
const sqlQuery = `INSERT INTO ${nodesTable} (name, ip, port, location, nearbySystems, online) VALUES ('${name}', '${ip}', ${port}, '${location}', '${nearbySystems}', ${online})`; connected = 0;
const sqlQuery = `INSERT INTO ${nodesTable} (name, ip, port, location, nearbySystems, online, connected) VALUES ('${name}', '${ip}', ${port}, '${location}', '${nearbySystems}', ${online}, ${connected})`;
runSQL(sqlQuery, (rows) => { const sqlResponse = await new Promise((recordResolve, recordReject) => {
return callback(rows); runSQL(sqlQuery, (rows) => {
}) recordResolve(rows);
})
});
// Call back the first (and theoretically only) row
// Specify 0 so downstream functions don't have to worry about it
return (callback) ? callback(returnNodeObjectFromRow(sqlResponse)) : returnNodeObjectFromRow(sqlResponse);
} }
/** Update the known info on a node /** Update the known info on a node
* @param nodeObject Node information object * @param nodeObject Node information object
* @param callback Callback function * @param callback Callback function
*/ */
exports.updateNodeInfo = (nodeObject, callback) => { exports.updateNodeInfo = async (nodeObject, callback = undefined) => {
if(!nodeObject.id) throw new Error("Attempted to updated node without providing ID", nodeObject); if(!nodeObject.id) throw new Error("Attempted to updated node without providing ID", nodeObject);
const name = nodeObject.name, const name = nodeObject.name,
ip = nodeObject.ip, ip = nodeObject.ip,
port = nodeObject.port, port = nodeObject.port,
location = nodeObject.location, location = nodeObject.location,
online = nodeObject.online; online = nodeObject.online
let queryParams = [], let queryParams = [],
nearbySystems = nodeObject.nearbySystems; nearbySystems = nodeObject.nearbySystems;
@@ -91,7 +180,7 @@ exports.updateNodeInfo = (nodeObject, callback) => {
} }
let sqlQuery = `UPDATE ${nodesTable} SET` let sqlQuery = `UPDATE ${nodesTable} SET`
if (!queryParams || queryParams.length === 0) return callback(undefined); if (!queryParams || queryParams.length === 0) return (callback) ? callback(undefined) : undefined;
if (queryParams.length === 1) { if (queryParams.length === 1) {
sqlQuery = `${sqlQuery} ${queryParams[0]}` sqlQuery = `${sqlQuery} ${queryParams[0]}`
} else { } else {
@@ -110,21 +199,167 @@ exports.updateNodeInfo = (nodeObject, callback) => {
sqlQuery = `${sqlQuery} WHERE id = ${nodeObject.id};` sqlQuery = `${sqlQuery} WHERE id = ${nodeObject.id};`
runSQL(sqlQuery, (rows) => { const sqlResponse = await new Promise((recordResolve, recordReject) => {
if (rows.affectedRows === 1) return callback(true); runSQL(sqlQuery, (rows) => {
else return callback(rows); recordResolve(rows);
}) })
});
if (sqlResponse.affectedRows === 1) return (callback) ? callback(true) : true;
else return (callback) ? callback(returnNodeObjectFromRows(sqlResponse)) : returnNodeObjectFromRows(sqlResponse);
}
/**
* Add a new connection to the DB when a bot has been connected to the server
*
* @param {*} nodeObject The node object that is being used for this connection
* @param {*} clientId The client ID Object being used for this connection
* @param {*} callback [OPTIONAL] The callback function to be called with the results, will return otherwise
*/
exports.addNodeConnection = (nodeObject, clientObject, callback = undefined) => {
if (!nodeObject.id || !clientObject.clientId) throw new Error("Tried to add a connection without a client and/or node ID");
const sqlQuery = `INSERT INTO ${nodeConnectionsTable} (id, discord_client_id) VALUES (${nodeObject.id}, '${clientObject.clientId}')`;
const sqlResponse = new Promise((recordResolve, recordReject) => {
runSQL(sqlQuery, (rows) => {
recordResolve(rows);
})
});
if (!sqlResponse) throw new Error("No result from added connection");
return (callback) ? callback(true) : true;
}
/**
* Check what node is connected with a given client ID object
*
* @param {*} clientId The client ID object used to search for a connected node
* @param {*} callback [OPTIONAL] The callback function to be called with the results, return will be used otherwise
*/
exports.checkNodeConnectionByClientId = async (clientId, callback = undefined) => {
if (!clientId.clientId) throw new Error("Tried to check a connection without a client ID");
const sqlQuery = `SELECT * FROM ${nodeConnectionsTable} WHERE discord_client_id = '${clientId.clientId}'`;
const sqlResponse = await new Promise((recordResolve, recordReject) => {
runSQL(sqlQuery, (rows) => {
recordResolve(rows);
})
});
log.VERBOSE("SQL Response from checking connection: ", sqlResponse);
if (!sqlResponse) return (callback) ? callback(undefined) : undefined;
const newNodeObject = await getNodeInfoFromId(sqlResponse[0].id);
log.DEBUG("Node Object from SQL Response: ", newNodeObject);
return (callback) ? callback(newNodeObject) : newNodeObject;
}
/**
* Get a connection by node ID
*
* @param {*} nodeId The ID to search for a connection with
* @param {*} callback [OPTIONAL] The callback function to be called with the results, return will be used otherwise
* @returns {connectionObject}
*/
exports.getConnectionByNodeId = async (nodeId, callback = undefined) => {
const sqlQuery = `SELECT * FROM ${nodeConnectionsTable} WHERE id = '${nodeId}'`;
const sqlResponse = await new Promise((recordResolve, recordReject) => {
runSQL(sqlQuery, (rows) => {
recordResolve(rows);
})
});
log.VERBOSE("SQL Response from checking connection: ", sqlResponse);
if (!sqlResponse | sqlResponse.length == 0) return (callback) ? callback(undefined) : undefined;
const newConnectionObject = await returnConnectionObjectFromRow(sqlResponse)
log.DEBUG("Connection Object from SQL Response: ", newConnectionObject);
return (callback) ? callback(newConnectionObject) : newConnectionObject;
}
/**
* Remove a node connection by the node
*
* @param {*} nodeId The node ID of the node to remove connections of
* @param {*} callback [OPTIONAL] The callback function to callback with the results, return will be used otherwise
* @returns
*/
exports.removeNodeConnectionByNodeId = async (nodeId, callback = undefined) => {
const sqlQuery = `DELETE FROM ${nodeConnectionsTable} WHERE id = '${nodeId}'`;
const sqlResponse = await new Promise((recordResolve, recordReject) => {
runSQL(sqlQuery, (rows) => {
recordResolve(rows);
})
});
log.VERBOSE("SQL Response from removing connection: ", sqlResponse);
if (!sqlResponse) return (callback) ? callback(undefined) : undefined;
return (callback) ? callback(sqlResponse) : sqlResponse;
}
/**
* Gets all connected nodes
*
* @param {*} callback [OPTIONAL] The callback function to callback with the results, return will be used otherwise
* @returns {nodeObject}
*/
exports.getConnectedNodes = async (callback = undefined) => {
const sqlQuery = `SELECT * FROM ${nodeConnectionsTable}`;
const sqlResponse = await new Promise((recordResolve, recordReject) => {
runSQL(sqlQuery, (rows) => {
recordResolve(rows);
})
});
log.VERBOSE("SQL Response from checking connection: ", sqlResponse);
if (!sqlResponse) return (callback) ? callback(undefined) : undefined;
var nodeObjects = []
for (const row of sqlResponse) {
const newNodeObject = await getNodeInfoFromId(row.id);
log.DEBUG("Node Object from SQL Response: ", newNodeObject);
nodeObjects.push(newNodeObject);
}
return (callback) ? callback(nodeObjects) : nodeObjects;
}
/**
* Returns all connections
*
* @param {*} callback [OPTIONAL] The callback function to callback with the results, return will be used otherwise
* @returns {connectionObject}
*/
exports.getAllConnections = async (callback = undefined) => {
const sqlQuery = `SELECT * FROM ${nodeConnectionsTable}`;
const sqlResponse = await new Promise((recordResolve, recordReject) => {
runSQL(sqlQuery, (rows) => {
recordResolve(rows);
})
});
log.VERBOSE("SQL Response from checking connection: ", sqlResponse);
if (!sqlResponse) return (callback) ? callback(undefined) : undefined;
var connectionObjects = []
for (const row of sqlResponse) {
connectionObjects.push(await returnConnectionObjectFromRow(row));
}
return (callback) ? callback(connectionObjects) : connectionObjects;
} }
// Function to run and handle SQL errors // Function to run and handle SQL errors
function runSQL(sqlQuery, callback, error = (err) => { function runSQL(sqlQuery, callback = undefined, error = (err) => {
console.log(err); console.log(err);
throw err; throw err;
}) { }) {
connection.query(sqlQuery, (err, rows) => { connection.query(sqlQuery, (err, rows) => {
if (err) return error(err); if (err) return error(err);
//console.log('The rows are:', rows); //console.log('The rows are:', rows);
return callback(rows); return (callback) ? callback(rows) : rows
}) })
} }

View File

@@ -109,8 +109,10 @@ class nodeObject {
* @param {*} param0._ip The IP that the master can contact the node at * @param {*} param0._ip The IP that the master can contact the node at
* @param {*} param0._port The port that the client is listening on * @param {*} param0._port The port that the client is listening on
* @param {*} param0._location The physical location of the node * @param {*} param0._location The physical location of the node
* @param {*} param0._online An integer representation of the online status of the bot, ie 0=off, 1=on * @param {*} param0._online True/False if the node is online or offline
* @param {*} param0._nearbySystems An object array of nearby systems * @param {*} param0._connected True/False if the bot is connected to discord or not
* @param {*} param0._connection The connection Object associated with the node, null if not checked, undefined if none exists
* @param {*} param0._nearbySystems An object array of nearby systems
*/ */
constructor({ _id = null, _name = null, _ip = null, _port = null, _location = null, _nearbySystems = null, _online = null }) { constructor({ _id = null, _name = null, _ip = null, _port = null, _location = null, _nearbySystems = null, _online = null }) {
this.id = _id; this.id = _id;
@@ -123,4 +125,42 @@ class nodeObject {
} }
} }
exports.nodeObject = nodeObject; exports.nodeObject = nodeObject;
/**
* This object represents a discord bot's client information
*/
class clientObject {
/**
*
* @param {*} param0._discord_id The discord id from the node, as seen when right clicking -> copy ID
* @param {*} param0._name The name of the bot associated with the IDs
* @param {*} param0._client_id The client ID of the bot needed to connect to Discord
*/
constructor({_discord_id = null, _name = null, _client_id = null,}) {
this.discordId = _discord_id;
this.name = _name;
this.clientId = _client_id;
}
}
exports.clientObject = clientObject;
/**
* This object represents a discord node connection
*/
class connectionObject {
/**
*
* @param {*} param0._connection_id The connection ID associated with the connection in the database
* @param {*} param0._node The node associated with the connection
* @param {*} param0._client_object The client object associated with the connection
*/
constructor({_connection_id = null, _node = null, _client_object}) {
this.connectionId = _connection_id;
this.node = _node;
this.clientObject = _client_object;
}
}
exports.connectionObject = connectionObject;

View File

@@ -1,3 +1,10 @@
// Debug
const { DebugBuilder } = require("../utilities/debugBuilder");
const { clientObject } = require("./recordHelper");
const { readFileSync } = require('fs');
const log = new DebugBuilder("server", "utils");
const path = require('path');
// Convert a JSON object to a buffer for the DB // Convert a JSON object to a buffer for the DB
exports.JsonToBuffer = (jsonObject) => { exports.JsonToBuffer = (jsonObject) => {
return Buffer.from(JSON.stringify(jsonObject)) return Buffer.from(JSON.stringify(jsonObject))
@@ -8,6 +15,46 @@ exports.BufferToJson = (buffer) => {
return JSON.parse(buffer.toString()); return JSON.parse(buffer.toString());
} }
/**
* **DISUSED**
*
* @param {string} presetName The present name to sanitize
* @returns {string} The sanitized preset name to be used elsewhere
*/
exports.SanitizePresetName = (presetName) => {
return String(presetName).toLowerCase().replace(/[\W_]+/g,"-")
}
/**
* Get online, offline and total members in a guild. Optionally a group can be specified to get members' statuses.
*
* @param interaction Discord interaction object
* @param param0.roleName {OPTIONAL} The role name to check the members in; Defaults to 'Bots'
*/
exports.getMembersInRole = async (interaction, roleName = "Bots" ) => {
log.DEBUG("Fetching all members");
var guild = await interaction.client.guilds.fetch({ guild: interaction.guild.id, cache: false }); //cache all members in the server
await guild.members.fetch({cache: false});
await guild.roles.fetch({cache: false});
log.VERBOSE("Guild: ", guild);
const role = await guild.roles.cache.find(role => role.name === roleName); //the role to check
log.DEBUG("Role to check members from: ", role);
log.DEBUG("Members of role: ", role.members);
// This is not working, can't get the status of the users, rest of join is untested
const onlineMembers = await role.members.filter(member => member.voice.channel !== null);
const offlineMembers = await role.members.filter(member => member.voice.channel === null);
const allMembers = await role.members;
log.VERBOSE("All members: ", allMembers, onlineMembers, offlineMembers)
return {
'online': onlineMembers,
'offline': offlineMembers,
'all': allMembers
}
}
/** Find a key in an object by its value /** Find a key in an object by its value
* *
* @param {*} object The object to search * @param {*} object The object to search
@@ -15,5 +62,58 @@ exports.BufferToJson = (buffer) => {
* @returns The key of the object that contains the value * @returns The key of the object that contains the value
*/ */
exports.getKeyByArrayValue = (object, value) => { exports.getKeyByArrayValue = (object, value) => {
return Object.keys(object).find(key => object[key].includes(value)); if (typeof value == "string") return Object.keys(object).find(key => object[key].includes(value));
} const valueKey = Object.keys(value)[0];
return Object.keys(object).find(key => (object[key][valueKey] == value[valueKey]));
}
/**
* Check to see if the input is a valid JSON string
*
* @param {*} str The string to check for valud JSON
* @returns {true|false}
*/
exports.isJsonString = (str) => {
try {
JSON.parse(str);
} catch (e) {
return false;
}
return true;
}
/**
* Get all client IDs from the saved JSON file
*
* @returns Object of Client IDs
*/
exports.getAllClientIds = () => {
const jsonClientIds = JSON.parse(readFileSync(path.resolve(__dirname, '../clientIds.json')));
var clientObjects = [];
for (const jsonClientId of Object.keys(jsonClientIds)){
clientObjects.push(new clientObject({
_discord_id: jsonClientId,
_name: jsonClientIds[jsonClientId].name,
_client_id: jsonClientIds[jsonClientId].id
}))
}
return clientObjects;
}
/**
* Gets a client object froma discord client ID
*
* @param {*} clientId The discord client ID to get the client object of
* @returns {clientObject|undefined}
*/
exports.getClientObjectByClientID = (clientId) => {
const clientObjects = this.getAllClientIds();
log.DEBUG("All client IDs: ", clientObjects);
for (const clientObject of clientObjects){
if (clientObject.clientId == clientId) {
log.DEBUG("Found client ID from given ID: ", clientObject);
return clientObject
}
}
return undefined
}