Init WIP Bot

This commit is contained in:
Logan Cusano
2022-12-11 06:09:25 -05:00
parent f1f4cb7750
commit 4e1b82c557
43 changed files with 6766 additions and 0 deletions

47
Client/app.js Normal file
View File

@@ -0,0 +1,47 @@
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes/index');
var botController = require('./routes/bot');
var clientController = require('./routes/client');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
app.use('/', indexRouter);
// Discord bot control route
app.use('/bot', botController);
// Local client control route
app.use("/client", clientController);
// catch 404 and forward to error handler
app.use(function(req, res, next) {
next(createError(404));
});
// error handler
app.use(function(err, req, res, next) {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;

74
Client/bin/www Normal file
View File

@@ -0,0 +1,74 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
const app = require('../app');
const debug = require('debug')('client:server');
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;
debug('Listening on ' + bind);
// check in with the server to add this node or come back online
clientController.checkIn();
}

View File

@@ -0,0 +1,17 @@
// Core config settings for the node, these are the settings that are checked with the server
exports.clientConfig = {
"id": 13,
"name": "boilin balls in the hall",
"ip": "172.16.100.150",
"port": 3001,
"location": "the house",
"nearbySystems": ["Westchester Cty. Simulcast"],
"online": true
}
// Configuration for the connection to the server
exports.serverConfig = {
"ip": "127.0.0.1",
"hostname": "localhost",
"port": 3000
}

View File

@@ -0,0 +1,16 @@
// 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
}

3
Client/config/modes.js Normal file
View File

@@ -0,0 +1,3 @@
exports.digitalModes = ["p25"];
exports.analogModes = ["nbfm"];

View File

@@ -0,0 +1 @@
{"Westchester Cty. Simulcast":{"frequencies":[470575000,470375000,470525000,470575000,470550000],"mode":"p25","trunkFile":"trunk.tsv"}}

View File

View File

@@ -0,0 +1,101 @@
// Debug output
const debug = require('debug')('client:clientController');
// Configs
const config = require("../config/clientConfig");
const modes = require("../config/modes");
// Utilities
const updateConfig = require("../utilities/updateConfig");
const updatePreset = require("../utilities/updatePresets");
const requests = require("../utilities/httpRequests");
/**
* Check the body for the required fields to update or add a preset
* @param req Express req from the endpoint controller
* @param res Express res from the endpoint controller
* @param callback The callback function to call when this function completes
* @returns {*}
*/
function checkBodyForPresetFields(req, res, callback) {
if (!req.body?.systemName) return res.status(403).json({"message": "No system in the request"});
if (!req.body?.frequencies && Array.isArray(req.body.frequencies)) return res.status(403).json({"message": "No frequencies in the request or type is not an array"});
if (!req.body?.mode && typeof req.body.mode === "string") return res.status(403).json({"message": "No mode in the request"});
if (!req.body?.trunkFile) {
if (modes.digitalModes.includes(req.body.mode)) return res.status(403).json({"message": "No trunk file in the request but digital mode specified. If you are not using a trunk file for this frequency make sure to specify 'none' for trunk file in the request"})
// If there is a value keep it but if not, add nothing so the system can update that key (if needed)
req.body.trunkFile = req.body.trunkFile ?? "none";
}
return callback();
}
/** Check in with the server
* If the bot has a saved ID, check in with the server to update any information or just check back in
* If the bot does not have a saved ID, it will attempt to request a new ID from the server
*/
exports.checkIn = async () => {
let reqOptions;
// 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) {
// ID was not found in the config, creating a new node
reqOptions = new requests.requestOptions("/nodes/newNode", "POST");
delete config.clientConfig.id;
requests.sendHttpRequest(reqOptions, JSON.stringify(config.clientConfig), (responseObject) => {
// Update the client's ID if the server accepted it
if (responseObject.statusCode === 202) {
config.clientConfig.id = responseObject.body.nodeId;
updateConfig.updateId(responseObject.body.nodeId);
}
});
}
else {
// ID is in the config, checking in with the server
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
}
});
}
}
/** Controller for the /client/requestCheckIn endpoint
* This is the endpoint wrapper to queue a check in
*/
exports.requestCheckIn = async (req, res) => {
this.checkIn();
return res.sendStatus(200);
}
/** Controller for the /client/presets endpoint
* This is the endpoint wrapper to get the presets object
*/
exports.getPresets = async (req, res) => {
return res.status(200).json(updatePreset.getPresets());
}
/** Controller for the /client/updatePreset endpoint
* This is the endpoint wrapper to update the selected preset (must include the whole object for that preset otherwise it will be rejected)
*/
exports.updatePreset = async (req, res) => {
checkBodyForPresetFields(req, res, () => {
updatePreset.updatePreset(req.body.systemName, () => {
return res.sendStatus(200);
}, {frequencies: req.body.frequencies, mode: req.body.mode, trunkFile: req.body.trunkFile});
})
}
/**
* Adds a new preset to the client
*/
exports.addNewPreset = async (req, res) => {
checkBodyForPresetFields(req, res, () => {
updatePreset.addNewPreset(req.body.systemName, req.body.frequencies, req.body.mode, () => {
return res.sendStatus(200);
}, req.body.trunkFile);
});
}

69
Client/discord-bot/app.js Normal file
View File

@@ -0,0 +1,69 @@
//Config
import { getTOKEN, getGuildID, getApplicationID } from './utilities/configHandler.js';
// Commands
import ping from './controllers/ping.js';
import { join, leave } from './controllers/voiceController.js';
// Debug
import Debug from 'debug';
const debug = Debug("bot:app");
// Modules
import { Client, GatewayIntentBits } from 'discord.js';
// Utilities
import registerCommands from './utilities/registerCommands.js';
// Create the Discord client
const client = new Client({
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildVoiceStates
]
});
// When the client is connected and ready
client.on('ready', () =>{
debug(`${client.user.tag} is ready`)
console.log(`${client.user.tag} is ready`)
});
/*
* Saved For later
client.on('messageCreate', (message) => {
debug(`Message Sent by: ${message.author.tag}\n\t'${message.content}'`);
console.log(`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);
break;
case "leave":
leave(interaction);
break;
default:
interaction.reply({ content: 'Command not found, try one that exists', fetchReply: true })
.then((message) => console.log(`Reply sent with content ${message.content}`))
.catch(console.error);
}
}
})
function loginBot(){
client.login(getTOKEN());
}
function main(){
registerCommands(() => {
loginBot();
});
}
main();

View File

@@ -0,0 +1,7 @@
{
"TOKEN": "OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY",
"ApplicationID": "943742040255115304",
"GuildID": "367396189529833472",
"DeviceID": "25",
"DeviceName": "VoiceMeeter Aux Output (VB-Audio VoiceMeeter AUX VAIO)"
}

View File

@@ -0,0 +1,37 @@
// Config
import { getDeviceID, getDeviceName } from '../utilities/configHandler.js'
// Modules
import portAudio from 'naudiodon';
import {createAudioResource} from "@discordjs/voice";
export function getAudioDevice({deviceName = undefined, deviceId = undefined}){
const deviceList = getAudioDevices();
if (!deviceName && !deviceId) throw new Error("No device given");
if (deviceName) return deviceList.find(device => device.name === deviceName);
if (deviceId) return deviceList.find(device => device.id === deviceId);
}
export function getAudioDevices(){
const deviceList = portAudio.getDevices();
console.log("Devices:", deviceList);
return deviceList;
}
export function createAudioInstance() {
//const resource = createAudioResource();
const selectedDevice = getAudioDevice({deviceId: getDeviceID()});//{deviceName: "VoiceMeeter VAIO3 Output (VB-Au"});
console.log(selectedDevice);
// Create an instance of AudioIO with outOptions (defaults are as below), which will return a WritableStream
const audioInstance = new portAudio.AudioIO({
inOptions: {
channelCount: 2,
sampleFormat: portAudio.SampleFormat16Bit,
sampleRate: 44100,
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: 0 // 44100 / 1000 * 120 / 2 // Get 120ms of audio
}
});
//audioInstance.start();
return audioInstance;
}

View File

@@ -0,0 +1,6 @@
// Utilities
import { replyToInteraction } from '../utilities/messageHandler.js';
export default function ping(interaction) {
return replyToInteraction(interaction, "Pong! I have Aids and now you do too!");
}

View File

@@ -0,0 +1,62 @@
// Modules
import {
joinVoiceChannel,
getVoiceConnection,
createAudioResource,
createAudioPlayer,
NoSubscriberBehavior, StreamType
} from '@discordjs/voice';
import OpusEncoderPkg from "@discordjs/opus";
const { OpusEncoder } = OpusEncoderPkg;
// Utilities
import { replyToInteraction } from '../utilities/messageHandler.js';
import { createAudioInstance } from "./audioController.js";
/**
* Join the specified voice channel
*
* @param interaction Message interaction from discord
*/
export function join(interaction){
const voiceChannel = interaction.options.getChannel('voicechannel');
const voiceConnection = joinVoiceChannel({
channelId: voiceChannel.id,
guildId: interaction.guildId,
adapterCreator: interaction.guild.voiceAdapterCreator,
selfMute: false,
selfDeaf: false,
});
replyToInteraction(interaction, `Ok, Joining ${voiceChannel.name}`);
// Declare the encoder
const encoder = new OpusEncoder(44100, 2);
const player = createAudioPlayer({
behaviors: {
noSubscriber: NoSubscriberBehavior.Play,
},
});
const audioInstance = createAudioInstance();
const audioResource = createAudioResource(audioInstance, { inputType: StreamType.Raw });
audioInstance.start();
//audioInstance.on('data', buffer => {
// Do on buffer event
//})
player.play(audioResource);
voiceConnection.subscribe(player);
}
/**
* If in a voice channel for the specified guild, leave
*
* @param interaction Message interaction from discord
*/
export function leave(interaction){
const guildId = interaction.guild.id;
const voiceConnection = getVoiceConnection(guildId);
if (!voiceConnection) return replyToInteraction(interaction, "Not in a voice channel.");
voiceConnection.destroy();
return replyToInteraction(interaction, `Goodbye`);
}

2907
Client/discord-bot/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

View File

@@ -0,0 +1,25 @@
{
"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",
"naudiodon": "^2.3.6",
"node-gyp": "^9.3.0",
"libsodium-wrappers": "^0.7.10"
},
"type": "module"
}

View File

@@ -0,0 +1,36 @@
import { readFileSync } from 'fs';
export function getConfig() {
return JSON.parse(readFileSync("./config/botConfig.json"));
}
export function getTOKEN() {
const parsedJSON = getConfig();
return parsedJSON.TOKEN;
}
export function getGuildID() {
const parsedJSON = getConfig();
parsedJSON.GuildID = BigInt(parsedJSON.GuildID);
//console.log("Guild ID: ", parsedJSON.GuildID);
return parsedJSON.GuildID;
}
export function getApplicationID() {
const parsedJSON = getConfig();
parsedJSON.ApplicationID = BigInt(parsedJSON.ApplicationID);
//console.log("Application ID: ", parsedJSON.ApplicationID);
return parsedJSON.ApplicationID;
}
export function getDeviceID(){
const parsedJSON = getConfig();
//console.log("Device ID: ", parseInt(parsedJSON.DeviceID));
return parseInt(parsedJSON.DeviceID);
}
export function getDeviceName(){
const parsedJSON = getConfig();
//console.log("Device Name: ", parseInt(parsedJSON.DeviceName));
return parsedJSON.DeviceName;
}

View File

@@ -0,0 +1,5 @@
export function replyToInteraction(interaction, message){
interaction.reply({ content: message, fetchReply: true })
.then((message) => console.log(`Reply sent with content ${message.content}`))
.catch(console.error);
}

View File

@@ -0,0 +1,45 @@
import {SlashCommandBuilder} from "@discordjs/builders";
import {REST} from "@discordjs/rest";
import {getApplicationID, getGuildID, getTOKEN} from "./configHandler.js";
import { Routes, ChannelType } from "discord.js";
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();
export default async function registerCommands(callback){
const commands = [
pingCommand,
joinCommand,
leaveCommand
];
try {
const rest = new REST({ version: '10' }).setToken(getTOKEN());
const clientID = getApplicationID();
const guildID = getGuildID();
await rest.put(Routes.applicationGuildCommands(clientID, guildID), {
body: commands,
});
callback();
} catch (err) {
console.log(err);
}
}

1448
Client/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

17
Client/package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "client",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"ejs": "~2.6.1",
"express": "~4.16.1",
"http-errors": "~1.6.3",
"morgan": "~1.9.1",
"replace-in-file": "~6.3.5"
}
}

View File

@@ -0,0 +1,8 @@
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
color: #00B7FF;
}

39
Client/routes/bot.js Normal file
View File

@@ -0,0 +1,39 @@
var express = require('express');
var router = express.Router();
/* GET users listing.
router.get('/', function(req, res, next) {
res.send('respond with a resource');
});
*/
/** GET bot status
* Check to see if the bot is online and if so, if it is currently connected to anything
*
* The status of the bot: 200 = online, 202 = connected, 500 + JSON = encountered error
* @returns status
*/
/** POST bot join channel
* Join the channel specified listening to the specified freq/mode
*
* @param req The request sent from the master
* @param req.body.channelId The channel ID to join
* @param req.body.presetName The name of the preset to start listening to
*/
/** POST bot leave channel
* Will leave the channel it is currently listening to if any, otherwise it will just return that it is not connected
*
* The status of the bot: 200 = no change, 202 = changed successfully, 500 + JSON = encountered error
* @returns status
*/
/** POST change bot preset
* This will change the bot to the preset specified (if different from what is currently playing)
*
* The status of the bot: 200 = no change, 202 = changed successfully, 500 + JSON = encountered error
* @returns status
*/
module.exports = router;

41
Client/routes/client.js Normal file
View File

@@ -0,0 +1,41 @@
// Modules
const express = require('express');
const router = express.Router();
// Controllers
const clientController = require("../controllers/clientController");
/** GET Request a check in from the client
* Queue the client to check in with the server
*
* The status of the checkin request: 200 = Queued
*/
router.get('/requestCheckIn', clientController.requestCheckIn);
/** GET Object of all known presets
* Query the client to get all the known presets
*/
router.get('/presets', clientController.getPresets);
/** POST Update to preset
* Join the channel specified listening to the specified freq/mode
*
* @param req The request sent from the master
* @param {string} req.body.systemName The name of the system to be updated
* @param {Array} req.body.frequencies The frequencies array for the channel or channels to be listened to
* @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
*/
router.post('/updatePreset', clientController.updatePreset);
/** POST Add new preset
* Join the channel specified listening to the specified freq/mode
*
* @param req The request sent from the master
* @param {string} req.body.systemName The name of the system to be updated
* @param {Array} req.body.frequencies The frequencies array for the channel or channels to be listened to
* @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
*/
router.post('/addPreset', clientController.addNewPreset);
module.exports = router;

9
Client/routes/index.js Normal file
View File

@@ -0,0 +1,9 @@
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
module.exports = router;

View File

@@ -0,0 +1,48 @@
// Debug
const debug = require('debug')('client:httpRequests');
// Config
const config = require("../config/clientConfig");
// Modules
const http = require("http");
exports.requestOptions = class requestOptions {
constructor(path, method, hostname = undefined, headers = undefined, port = undefined) {
if (method === "POST"){
this.hostname = hostname ?? config.serverConfig.hostname
this.path = path
this.port = port ?? config.serverConfig.port
this.method = method
this.headers = headers ?? {
'Content-Type': 'application/json',
}
}
}
}
/**
* Send the HTTP request to the server
* @param requestOptions
* @param data
* @param callback
*/
exports.sendHttpRequest = function sendHttpRequest(requestOptions, data, callback){
debug("Sending a request to: ", requestOptions.hostname, requestOptions.port)
// Create the request
const req = http.request(requestOptions, res => {
res.on('data', (data) => {
const responseObject = {
"statusCode": res.statusCode,
"body": JSON.parse(data)
};
debug("Response Object: ", responseObject);
callback(responseObject);
})
}).on('error', err => {
debug('Error: ', err.message)
// TODO need to handle if the server is down
})
// Write the data to the request and send it
req.write(data)
req.end()
}

View File

@@ -0,0 +1,41 @@
// Debug
const debug = require('debug')('client:updateConfig');
// Modules
const replace = require('replace-in-file');
class Options {
constructor(key, updatedValue) {
this.files = "./config/clientConfig.js";
// A regex of the line containing the key in the config file
this.from = new RegExp(`"${key}": (.+),`, "g");
// 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}",`;
else this.to = `"${key}": ${updatedValue},`;
}
}
/**
* Wrapper to update the client's saved ID
* @param updatedId The updated ID assigned to the bot
*/
exports.updateId = (updatedId) => {
const options = new Options("id", updatedId);
updateConfigFile(options, (updatedFiles) => {
// Do Something
})
}
/**
* Wrapper to write changes to the file
* @param options An instance of the Objects class specified to the key being updated
* @param callback Callback when the files have been modified
*/
function updateConfigFile(options, callback){
replace(options, (error, changedFiles) => {
if (error) return console.error('Error occurred:', error);
debug('Modified files:', changedFiles);
callback(changedFiles);
});
}

View File

@@ -0,0 +1,125 @@
// Debug
const debug = require('debug')('client:updatePresets');
// Modules
const fs = require('fs');
/**
* Write the given presets to the JSON file
* @param presets The preset or presets to be written
* @param {function} callback The function to be called when this wrapper completes
*/
function writePresets(presets, callback = undefined) {
fs.writeFile("../config/radioPresets.json", JSON.stringify(presets), (err) => {
// Error checking
if (err) throw err;
debug("Write Complete");
if (callback) callback()
});
}
/**
* Wrapper to ensure each value in the array is in Hz format
* @param frequenciesArray
* @returns {*[]}
*/
function sanitizeFrequencies(frequenciesArray) {
let sanitizedFrequencyArray = [];
for (const freq of frequenciesArray) {
sanitizedFrequencyArray.push(convertFrequencyToHertz(freq));
}
debug(sanitizedFrequencyArray);
return sanitizedFrequencyArray;
}
/**
* Function to convert a string or a float into the integer type needed to be saved
* @param frequency Could be a string, number or float,
* @returns {number|number|*} Return the value to be saved in Hz format ("154875000" = 154.875MHz)
*/
function convertFrequencyToHertz(frequency){
// check if the passed value is a number
if(typeof frequency == 'number' && !isNaN(frequency)){
if (Number.isInteger(frequency)) {
debug(`${frequency} is integer.`);
// Check to see if the frequency has the correct length
if (frequency.toString().length >= 7 && frequency.toString().length <= 9) return frequency
}
else {
debug(`${frequency} is a float value.`);
// Convert to a string to remove the decimal in place and then correct the length
frequency = frequency.toString();
// One extra digit checked for the '.' included in the string
if (frequency.length >= 8 && frequency.length <= 10) return parseInt(frequency.replace(".", ""));
else if (frequency.length <= 7) {
// Check to see if the frequency is 1s, 10s or 100s of MHz
let zerosToBeRemoved = 3 - frequency.split(".")[0].length;
// Need to add more 0s since it was in MHz format
let neededZeros = (9 - frequency.length) - zerosToBeRemoved;
frequency = frequency.replace(".", "") + '0'.repeat(neededZeros)
return parseInt(frequency);
}
}
} else {
debug(`${frequency} is not a number`);
frequency = convertFrequencyToHertz(parseFloat(frequency));
return parseInt(frequency)
}
}
/**
* Gets the saved presets and returns a preset object
* @returns {any} The object containing the different systems the bot is near
*/
exports.getPresets = function getPresets() {
return JSON.parse(fs.readFileSync("../config/radioPresets.json"));
}
/**
* Adds a new preset to the radioPresets JSON file
*
* @param {string} systemName The name of the system being added
* @param {Array} frequencies The frequency or frequencies the SDR should tune to for this system
* @param {string} mode The listening mode the SDR should be using when listening to this frequency
* @param {function} callback The callback function to call when completed
* @param {string} trunkFile The file that contains all trunking information (if applicable to the selected listening mode)
*/
exports.addNewPreset = (systemName, frequencies, mode, callback, trunkFile = undefined) => {
const presets = this.getPresets();
// Create the preset for the new system
presets[systemName] = {
"frequencies": sanitizeFrequencies(frequencies),
"mode": mode,
"trunkFile": trunkFile ?? "none"
}
// Write the changes to the preset config file
writePresets(presets, callback);
}
/**
* Updates 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
* @param {Array} frequencies The frequency or frequencies the SDR should tune to for this system
* @param {string} mode The listening mode the SDR should be using when listening to this frequency
* @param {string} trunkFile The file that contains all trunking information (if applicable to the selected listening mode)
*/
exports.updatePreset = (systemName, callback, { frequencies = undefined, mode = undefined, trunkFile = undefined }) => {
const presets = this.getPresets();
// Check if a system name was passed
if (systemName in presets) {
// System name exists, checking to see if the keys are different
if(frequencies && frequencies !== presets[systemName].frequencies) presets[systemName].frequencies = sanitizeFrequencies(frequencies);
if(mode && mode !== presets[systemName].mode) presets[systemName].mode = mode;
if(trunkFile && trunkFile !== presets[systemName].trunkFile || trunkFile === "") presets[systemName].trunkFile = trunkFile ?? "none";
// Write the changes
writePresets(presets, callback);
}
}

3
Client/views/error.ejs Normal file
View File

@@ -0,0 +1,3 @@
<h1><%= message %></h1>
<h2><%= error.status %></h2>
<pre><%= error.stack %></pre>

11
Client/views/index.ejs Normal file
View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<h1><%= title %></h1>
<p>Welcome to <%= title %></p>
</body>
</html>

48
Server/app.js Normal file
View File

@@ -0,0 +1,48 @@
var createError = require('http-errors');
var express = require('express');
var path = require('path');
var cookieParser = require('cookie-parser');
var logger = require('morgan');
var indexRouter = require('./routes/index');
var nodesRouter = require('./routes/nodes');
var adminRouter = require('./routes/admin');
var app = express();
// view engine setup
app.set('views', path.join(__dirname, 'views'));
app.set('view engine', 'ejs');
app.use(logger('dev'));
app.use(express.json());
app.use(express.urlencoded({ extended: false }));
app.use(cookieParser());
app.use(express.static(path.join(__dirname, 'public')));
// Web Interface
app.use('/', indexRouter);
// Nodes API
app.use('/nodes', nodesRouter);
// Admin API
app.use('/admin', adminRouter);
// catch 404 and forward to error handler
app.use((req, res, next) => {
next(createError(404));
});
// error handler
app.use((err, req, res, next) => {
// set locals, only providing error in development
res.locals.message = err.message;
res.locals.error = req.app.get('env') === 'development' ? err : {};
// render the error page
res.status(err.status || 500);
res.render('error');
});
module.exports = app;

90
Server/bin/www Normal file
View File

@@ -0,0 +1,90 @@
#!/usr/bin/env node
/**
* Module dependencies.
*/
var app = require('../app');
var debug = require('debug')('server:server');
var http = require('http');
/**
* Get port from environment and store in Express.
*/
var port = normalizePort(process.env.PORT || '3000');
app.set('port', port);
/**
* Create HTTP server.
*/
var server = http.createServer(app);
/**
* Listen on provided port, on all network interfaces.
*/
server.listen(port);
server.on('error', onError);
server.on('listening', onListening);
/**
* Normalize a port into a number, string, or false.
*/
function normalizePort(val) {
var port = parseInt(val, 10);
if (isNaN(port)) {
// named pipe
return val;
}
if (port >= 0) {
// port number
return port;
}
return false;
}
/**
* Event listener for HTTP server "error" event.
*/
function onError(error) {
if (error.syscall !== 'listen') {
throw error;
}
var 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() {
var addr = server.address();
var bind = typeof addr === 'string'
? 'pipe ' + addr
: 'port ' + addr.port;
debug('Listening on ' + bind);
}

View File

@@ -0,0 +1,8 @@
const databaseConfig = {
database_host: '100.20.1.45',
database_user: 'DRB_CNC',
database_password: 'baMbC6IAl$Rn7$h0PS',
database_database: 'DRB_CNC'
}
module.exports = databaseConfig;

View File

@@ -0,0 +1,76 @@
const mysqlHander = require("../mysqlHandler");
const utils = require("../utils");
exports.listAllNodes = async (req, res) => {
mysqlHander.getAllNodes((allNodes) => {
res.status(200).json({
"nodes_online": allNodes
});
});
}
// Add a new node to the
exports.newNode = async (req, res) => {
if (!req.body.name) return res.send(400)
try {
// Try to add the new user with defaults if missing options
mysqlHander.addNewNode({
'name': req.body.name,
'ip': req.body.ip ?? null,
'port': req.body.port ?? null,
'location': req.body.location ?? null,
'nearbySystems': req.body.nearbySystems ?? null,
'online': req.body.online ?? 0
}, (queryResults) => {
// Send back a success if the user has been added and the ID for the client to keep track of
res.status(202).json({"nodeId": queryResults.insertId});
})
}
catch (err) {
// Catch any errors
if (err === "No name provided") {
return res.sendStatus(400);
}
else console.log(err)
return res.sendStatus(500);
}
}
// Get the known info for the node specified
exports.getNodeInfo = async (req, res) => {
if (!req.query.id) return res.status(400).json("No id specified");
mysqlHander.getNodeInfoFromId(req.query.id, (nodeInfo) => {
res.status(200).json(nodeInfo);
})
}
// Updates the information received from the client based on ID
exports.nodeCheckIn = async (req, res) => {
if (!req.body.id) return res.status(400).json("No id specified");
mysqlHander.getNodeInfoFromId(req.body.id, (nodeInfo) => {
let nodeObject = {};
// Convert the DB systems buffer to a JSON object to be worked with
nodeInfo.nearbySystems = utils.BufferToJson(nodeInfo.nearbySystems)
// Convert the online status to a boolean to be worked with
nodeInfo.online = nodeInfo.online !== 0;
if (req.body.name && req.body.name !== nodeInfo.name) nodeObject.name = req.body.name
if (req.body.ip && req.body.ip !== nodeInfo.ip) nodeObject.ip = req.body.ip
if (req.body.port && req.body.port !== nodeInfo.port) nodeObject.port = req.body.port
if (req.body.location && req.body.location !== nodeInfo.location) nodeObject.location = req.body.location
if (req.body.nearbySystems && JSON.stringify(req.body.nearbySystems) !== JSON.stringify(nodeInfo.nearbySystems)) nodeObject.nearbySystems = req.body.nearbySystems
if (req.body.online && req.body.online !== nodeInfo.online) nodeObject.online = req.body.online
// If no changes are made tell the client
if (Object.keys(nodeObject).length === 0) return res.status(200).json("No keys updated");
console.log("Updating the following keys for ID:", req.body.id, nodeObject);
// Adding the ID key to the body so that the client can double-check their ID
nodeObject.id = req.body.id;
mysqlHander.updateNodeInfo(nodeObject, () => {
return res.status(202).json({"updatedKeys": nodeObject});
})
})
}

121
Server/mysqlHandler.js Normal file
View File

@@ -0,0 +1,121 @@
const mysql = require('mysql');
const databaseConfig = require('./config/databaseConfig');
const utils = require('./utils');
const connection = mysql.createConnection({
host: databaseConfig.database_host,
user: databaseConfig.database_user,
password: databaseConfig.database_password,
database: databaseConfig.database_database
});
const nodesTable = `${databaseConfig.database_database}.nodes`;
connection.connect()
// Get all nodes the server knows about regardless of status
exports.getAllNodes = (callback) => {
const sqlQuery = `SELECT * FROM ${nodesTable}`
runSQL(sqlQuery, (rows) => {
callback(rows);
})
}
// Get all nodes that have the online status set true (are online)
exports.getOnlineNodes = (callback) => {
const sqlQuery = `SELECT * FROM ${nodesTable} WHERE online = 1;`
runSQL(sqlQuery, (rows) => {
callback(rows);
})
}
// Get info on a node based on ID
exports.getNodeInfoFromId = (nodeId, callback) => {
const sqlQuery = `SELECT * FROM ${nodesTable} WHERE id = ${nodeId}`
runSQL(sqlQuery, (rows) => {
// Call back the first (and theoretically only) row
// Specify 0 so downstream functions don't have to worry about it
callback(rows[0]);
})
}
// Add a new node to the DB
exports.addNewNode = (nodeObject, callback) => {
if (!nodeObject.name) throw new Error("No name provided");
const name = nodeObject.name,
ip = nodeObject.ip,
port = nodeObject.port,
location = nodeObject.location,
nearbySystems = utils.JsonToBuffer(nodeObject.nearbySystems),
online = nodeObject.online;
const sqlQuery = `INSERT INTO ${nodesTable} (name, ip, port, location, nearbySystems, online) VALUES ('${name}', '${ip}', ${port}, '${location}', '${nearbySystems}', ${online})`;
runSQL(sqlQuery, (rows) => {
callback(rows);
})
}
// Update the known info on a node
exports.updateNodeInfo = (nodeObject, callback) => {
const name = nodeObject.name,
ip = nodeObject.ip,
port = nodeObject.port,
location = nodeObject.location,
online = nodeObject.online;
let queryParams = [],
nearbySystems = nodeObject.nearbySystems;
if (name) queryParams.push(`name = '${name}'`);
if (ip) queryParams.push(`ip = '${ip}'`);
if (port) queryParams.push(`port = ${port}`);
if (location) queryParams.push(`location = '${location}'`);
if (nearbySystems) {
nearbySystems = utils.JsonToBuffer(nearbySystems)
queryParams.push(`nearbySystems = '${nearbySystems}'`);
}
if (typeof online === "boolean") {
if (online) queryParams.push(`online = 1`);
else queryParams.push(`online = 0`);
}
let sqlQuery = `UPDATE ${nodesTable} SET`
if (!queryParams || queryParams.length === 0) return callback(undefined);
if (queryParams.length === 1) {
sqlQuery = `${sqlQuery} ${queryParams[0]}`
} else {
let i = 0;
for (const param of queryParams) {
if (i === queryParams.length-1) {
sqlQuery = `${sqlQuery} ${param}`
i += 1;
}
else {
sqlQuery = `${sqlQuery} ${param},`
i += 1;
}
}
}
sqlQuery = `${sqlQuery} WHERE id = ${nodeObject.id};`
runSQL(sqlQuery, (rows) => {
if (rows.affectedRows === 1) callback(true);
else callback(rows);
})
}
// Function to run and handle SQL errors
function runSQL(sqlQuery, callback, error = (err) => {
console.log(err);
throw err;
}) {
connection.query(sqlQuery, (err, rows) => {
if (err) return error(err);
//console.log('The rows are:', rows);
return callback(rows);
})
}
exports.closeConnection = () => {
connection.end()
}

1071
Server/package-lock.json generated Normal file

File diff suppressed because it is too large Load Diff

17
Server/package.json Normal file
View File

@@ -0,0 +1,17 @@
{
"name": "server",
"version": "0.0.0",
"private": true,
"scripts": {
"start": "node ./bin/www"
},
"dependencies": {
"cookie-parser": "~1.4.4",
"debug": "~2.6.9",
"ejs": "~2.6.1",
"express": "~4.16.1",
"http-errors": "~1.6.3",
"morgan": "~1.9.1",
"mysql": "^2.18.1"
}
}

View File

@@ -0,0 +1,8 @@
body {
padding: 50px;
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
}
a {
color: #00B7FF;
}

16
Server/routes/admin.js Normal file
View File

@@ -0,0 +1,16 @@
var express = require('express');
var router = express.Router();
/* GET */
router.get('/', (req, res) => {
res.send('GET request to the admin')
})
/* POST */
router.post('/', (req, res) => {
console.log(req.body);
res.send('POST request to the post')
})
module.exports = router;

9
Server/routes/index.js Normal file
View File

@@ -0,0 +1,9 @@
var express = require('express');
var router = express.Router();
/* GET home page. */
router.get('/', (req, res, next) => {
res.render('index', { title: 'Express' });
});
module.exports = router;

31
Server/routes/nodes.js Normal file
View File

@@ -0,0 +1,31 @@
const express = require('express');
const router = express.Router();
const nodesController = require('../controllers/nodesController');
/* GET nodes the server knows */
router.get('/', nodesController.listAllNodes);
// TODO Need to authenticate this request
/* POST a new node to the server
*
* Will create a new DB entry for the node for the server to reference later
* Req. body: {
* "serverInfo": {"ip": "x.x.x.x", port: 0000}
* }
*
* Will return a token for the client to reference when the bot is making requests
* Res. body {
* "serverToken": ""
* }
*/
router.post('/newNode', nodesController.newNode);
// TODO Need to authenticate this request
/* GET the information the server has on a particular node */
router.get('/nodeInfo', nodesController.getNodeInfo);
// TODO Need to authenticate this request
// Client checkin with the server to update information
router.post('/nodeCheckIn', nodesController.nodeCheckIn);
module.exports = router;

9
Server/utils.js Normal file
View File

@@ -0,0 +1,9 @@
// Convert a JSON object to a buffer for the DB
exports.JsonToBuffer = (jsonObject) => {
return Buffer.from(JSON.stringify(jsonObject))
}
// Convert a buffer from the DB to JSON object
exports.BufferToJson = (buffer) => {
return JSON.parse(buffer.toString());
}

3
Server/views/error.ejs Normal file
View File

@@ -0,0 +1,3 @@
<h1><%= message %></h1>
<h2><%= error.status %></h2>
<pre><%= error.stack %></pre>

11
Server/views/index.ejs Normal file
View File

@@ -0,0 +1,11 @@
<!DOCTYPE html>
<html>
<head>
<title><%= title %></title>
<link rel='stylesheet' href='/stylesheets/style.css' />
</head>
<body>
<h1><%= title %></h1>
<p>Welcome to <%= title %></p>
</body>
</html>