Init WIP Bot
This commit is contained in:
47
Client/app.js
Normal file
47
Client/app.js
Normal 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
74
Client/bin/www
Normal 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();
|
||||
}
|
||||
17
Client/config/clientConfig.js
Normal file
17
Client/config/clientConfig.js
Normal 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
|
||||
}
|
||||
16
Client/config/core.js.default
Normal file
16
Client/config/core.js.default
Normal 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
3
Client/config/modes.js
Normal file
@@ -0,0 +1,3 @@
|
||||
exports.digitalModes = ["p25"];
|
||||
|
||||
exports.analogModes = ["nbfm"];
|
||||
1
Client/config/radioPresets.json
Normal file
1
Client/config/radioPresets.json
Normal file
@@ -0,0 +1 @@
|
||||
{"Westchester Cty. Simulcast":{"frequencies":[470575000,470375000,470525000,470575000,470550000],"mode":"p25","trunkFile":"trunk.tsv"}}
|
||||
0
Client/controllers/botController.js
Normal file
0
Client/controllers/botController.js
Normal file
101
Client/controllers/clientController.js
Normal file
101
Client/controllers/clientController.js
Normal 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
69
Client/discord-bot/app.js
Normal 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();
|
||||
7
Client/discord-bot/config/botConfig.json
Normal file
7
Client/discord-bot/config/botConfig.json
Normal 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)"
|
||||
}
|
||||
37
Client/discord-bot/controllers/audioController.js
Normal file
37
Client/discord-bot/controllers/audioController.js
Normal 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;
|
||||
}
|
||||
6
Client/discord-bot/controllers/ping.js
Normal file
6
Client/discord-bot/controllers/ping.js
Normal 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!");
|
||||
}
|
||||
62
Client/discord-bot/controllers/voiceController.js
Normal file
62
Client/discord-bot/controllers/voiceController.js
Normal 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
2907
Client/discord-bot/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
25
Client/discord-bot/package.json
Normal file
25
Client/discord-bot/package.json
Normal 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"
|
||||
}
|
||||
36
Client/discord-bot/utilities/configHandler.js
Normal file
36
Client/discord-bot/utilities/configHandler.js
Normal 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;
|
||||
}
|
||||
5
Client/discord-bot/utilities/messageHandler.js
Normal file
5
Client/discord-bot/utilities/messageHandler.js
Normal 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);
|
||||
}
|
||||
45
Client/discord-bot/utilities/registerCommands.js
Normal file
45
Client/discord-bot/utilities/registerCommands.js
Normal 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
1448
Client/package-lock.json
generated
Normal file
File diff suppressed because it is too large
Load Diff
17
Client/package.json
Normal file
17
Client/package.json
Normal 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"
|
||||
}
|
||||
}
|
||||
8
Client/public/stylesheets/style.css
Normal file
8
Client/public/stylesheets/style.css
Normal 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
39
Client/routes/bot.js
Normal 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
41
Client/routes/client.js
Normal 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
9
Client/routes/index.js
Normal 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;
|
||||
48
Client/utilities/httpRequests.js
Normal file
48
Client/utilities/httpRequests.js
Normal 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()
|
||||
}
|
||||
41
Client/utilities/updateConfig.js
Normal file
41
Client/utilities/updateConfig.js
Normal 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);
|
||||
});
|
||||
}
|
||||
125
Client/utilities/updatePresets.js
Normal file
125
Client/utilities/updatePresets.js
Normal 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
3
Client/views/error.ejs
Normal file
@@ -0,0 +1,3 @@
|
||||
<h1><%= message %></h1>
|
||||
<h2><%= error.status %></h2>
|
||||
<pre><%= error.stack %></pre>
|
||||
11
Client/views/index.ejs
Normal file
11
Client/views/index.ejs
Normal 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>
|
||||
Reference in New Issue
Block a user