1 Commits

Author SHA1 Message Date
Logan Cusano
77deb3ba2b Initial recording scraper 2023-06-17 17:33:24 -04:00
76 changed files with 2603 additions and 3775 deletions

1
.gitignore vendored
View File

@@ -6,6 +6,5 @@ node_modules/
*.log
*.txt
*.env
*.wav
!requirements.txt
*testOP25Dir/

View File

@@ -17,7 +17,4 @@ SERVER_HOSTNAME=""
SERVER_PORT=3000
# Configuration of the local OP25 application
#OP25_BIN_PATH=""
# Logfile location config
#LOG_LOCATION=""
OP25_BIN_PATH=""

1
Client/.gitignore vendored
View File

@@ -1 +0,0 @@
*radioPresets.json

View File

@@ -17,7 +17,7 @@ var { attachRadioSessionToRequest } = require('./controllers/radioController');
const log = new DebugBuilder("client", "app");
var app = express();
var port = process.env.CLIENT_PORT || '3010';
var port = process.env.HTTP_PORT || '3010';
// view engine setup
app.set('views', path.join(__dirname, 'views'));
@@ -98,4 +98,4 @@ log.DEBUG(`Starting HTTP Server`);
runHTTPServer();
log.DEBUG("Checking in with the master server")
checkIn(true);
checkIn();

View File

@@ -0,0 +1 @@
{"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,18 +0,0 @@
{
"Default P25 System Name": {
"frequencies": [
155344000,
155444000,
155555000
],
"mode": "p25",
"trunkFile": "trunk.tsv"
},
"Default NBFM System": {
"frequencies": [
154690000
],
"mode": "nbfm",
"trunkFile": "none"
}
}

View File

@@ -10,7 +10,6 @@ const { closeProcessWrapper } = require("../utilities/utilities");
// Global vars
let pythonProcess;
let recordingProcess;
/**
* Get Status of the discord process
@@ -36,12 +35,12 @@ exports.joinServer = async (req, res) => {
log.INFO("Join requested to: ", deviceId, channelId, clientId, presetName, NGThreshold);
if (process.platform === "win32") {
log.DEBUG("Starting Windows Python");
pythonProcess = await spawn('python.exe', [resolve(__dirname, "../pdab/main.py"), deviceId, channelId, clientId, '-n', NGThreshold, '-p', presetName ], { cwd: resolve(__dirname, "../pdab/").toString() });
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, '-p', presetName ], { cwd: resolve(__dirname, "../pdab/") });
pythonProcess = await spawn('python3', [resolve(__dirname, "../pdab/main.py"), deviceId, channelId, clientId,'-n', NGThreshold ], { cwd: resolve(__dirname, "../pdab/") });
}
log.VERBOSE("Python Process: ", pythonProcess);
@@ -82,78 +81,4 @@ exports.leaveServer = async (req, res) => {
pythonProcess = await closeProcessWrapper(pythonProcess);
return res.sendStatus(202);
}
/**
* Start a recording of what the bot is listening to, if it's currently connected
*
* @param {*} req
* @param {*} res
*/
exports.startRecording = async (req, res) => {
log.INFO("Starting recording")
//if (pythonProcess === undefined) return res.sendStatus(204);
if (!recordingProcess === undefined) return res.sendStatus(202);
const deviceId = process.env.AUDIO_DEVICE_ID;
const filename = "./recordings/" + new Date().toJSON().slice(0,10) + ".wav";
// Joining the server to record
log.INFO("Start recording: ", deviceId, filename);
if (process.platform === "win32") {
log.DEBUG("Starting Windows Python");
recordingProcess = await spawn('python', [resolve(__dirname, "../pdab/recorder.py"), deviceId, filename ], { cwd: resolve(__dirname, "../pdab/").toString() });
}
else {
log.DEBUG("Starting Linux Python");
recordingProcess = await spawn('python3', [resolve(__dirname, "../pdab/recorder.py"), deviceId, filename ], { cwd: resolve(__dirname, "../pdab/") });
}
await this.getProcessOutput(recordingProcess);
return res.sendStatus(200);
}
/**
* Stop the recording if the bot is currently recording
*
* @param {*} req
* @param {*} res
*/
exports.stopRecording = async (req, res) => {
log.INFO("Stopping recording the server");
if (!recordingProcess) return res.sendStatus(202)
recordingProcess = await closeProcessWrapper(recordingProcess);
return res.sendStatus(200);
}
/**
* Get the output of a running process
*
* @param {*} runningProcess
* @returns
*/
exports.getProcessOutput = async (runningProcess) => {
let fullOutput;
runningProcess.stdout.setEncoding('utf8');
runningProcess.stdout.on("data", (data) => {
botLog.VERBOSE("From Process: ", data);
fullOutput += data.toString();
});
runningProcess.stderr.on('data', (data) => {
botLog.VERBOSE(`stderr: ${data}`);
fullOutput += data.toString();
});
runningProcess.on('close', (code) => {
log.DEBUG(`child process exited with code ${code}`);
log.VERBOSE("Full output from bot: ", fullOutput);
});
runningProcess.on("error", (code, signal) => {
log.ERROR("Error from the process: ", code, signal);
});
}

View File

@@ -5,14 +5,13 @@ const log = new DebugBuilder("client", "clientController");
require('dotenv').config();
const modes = require("../config/modes");
// Modules
const { executeAsyncConsoleCommand, BufferToJson, nodeObject } = require("../utilities/utilities");
const { executeAsyncConsoleCommand, nodeObject } = require("../utilities/utilities");
// Utilities
const { getFullConfig } = require("../utilities/configHandler");
const { updateId, updateConfig, updateClientConfig } = require("../utilities/updateConfig");
const { updateId, updateConfig } = require("../utilities/updateConfig");
const { updatePreset, addNewPreset, getPresets, removePreset } = require("../utilities/updatePresets");
const { onHttpError, requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
var runningClientConfig = getFullConfig()
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
@@ -22,11 +21,11 @@ var runningClientConfig = getFullConfig()
* @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?.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 (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";
}
@@ -34,7 +33,7 @@ function checkBodyForPresetFields(req, res, callback) {
return callback();
}
async function checkLocalIP() {
async function checkLocalIP() {
if (process.platform === "win32") {
// Windows
var networkConfig = await executeAsyncConsoleCommand("ipconfig");
@@ -68,98 +67,62 @@ async function checkLocalIP() {
* Checks the config file for all required fields or gets and updates the required fields
*/
exports.checkConfig = async function checkConfig() {
if (!runningClientConfig.id || runningClientConfig.id == 0 || runningClientConfig.id == '0') {
await updateId(0);
runningClientConfig.id = 0;
}
if (!runningClientConfig.ip) {
const ipAddr = await checkLocalIP();
await updateConfig('CLIENT_IP', ipAddr);
updateConfig('ip', ipAddr);
runningClientConfig.ip = ipAddr;
}
if (!runningClientConfig.name) {
if(!runningClientConfig.name) {
const lastOctet = await String(checkLocalIP()).spit('.')[-1];
const name = `Radio-Node-${lastOctet}`;
await updateConfig('CLIENT_NAME', name);
updateConfig('name', name);
runningClientConfig.name = name;
}
if (!runningClientConfig.port) {
if(!runningClientConfig.port) {
const port = 3010;
await updateConfig('CLIENT_PORT', port);
updateConfig('port', port);
runningClientConfig.port = port;
}
}
/** Check in with the server
* If the bot has a saved ID, check in with the server to get any updated information or just check back in
* 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
*
* @param {boolean} update If set to true, the client will update the server to it's config, instead of taking the server's config
*/
exports.checkIn = async (update = false) => {
exports.checkIn = async () => {
let reqOptions;
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
try {
if (!runningClientConfig?.id || runningClientConfig.id == 0) {
if (runningClientConfig.id === 0) {
// ID was not found in the config, creating a new node
reqOptions = new requestOptions("/nodes/newNode", "POST");
sendHttpRequest(reqOptions, JSON.stringify(runningClientConfig), async (responseObject) => {
// Check if the server responded
if (!responseObject) {
log.WARN("Server did not respond to checkIn. Will wait 60 seconds then try again");
setTimeout(() => {
// Run itself again to see if the server is up now
this.checkIn();
}, 60000);
return
}
// Update the client's ID if the server accepted its
reqOptions = new requestOptions("/nodes/newNode", "POST");
sendHttpRequest(reqOptions, JSON.stringify(), (responseObject) => {
// Update the client's ID if the server accepted it
if (responseObject.statusCode === 202) {
runningClientConfig.id = responseObject.body.nodeId;
log.DEBUG("Response object from new node: ", responseObject, runningClientConfig);
await updateId(runningClientConfig.id);
updateId(responseObject.body.nodeId);
}
if (responseObject.statusCode >= 300) {
// Server threw an error
log.DEBUG("HTTP Error: ", responseObject, await BufferToJson(responseObject.body));
await onHttpError(responseObject.statusCode);
onHttpError(responseObject.statusCode);
}
});
}
else {
// ID is in the config, checking in with the server
if (update) reqOptions = new requestOptions(`/nodes/${runningClientConfig.id}`, "PUT");
else reqOptions = new requestOptions(`/nodes/nodeCheckIn/${runningClientConfig.id}`, "POST");
reqOptions = new requestOptions("/nodes/nodeCheckIn", "POST");
sendHttpRequest(reqOptions, JSON.stringify(runningClientConfig), (responseObject) => {
log.DEBUG("Check In Respose: ", responseObject);
// Check if the server responded
if (!responseObject) {
log.WARN("Server did not respond to checkIn. Will wait 60 seconds then try again");
setTimeout(() => {
// Run itself again to see if the server is up now
this.checkIn();
}, 60000);
return
}
if (responseObject.statusCode === 202) {
log.DEBUG("Updated keys: ", responseObject.body.updatedKeys)
// Server accepted an update
}
if (responseObject.statusCode === 200) {
// Server accepted the response but there were no keys to be updated
if (!update){
const tempUpdatedConfig = updateClientConfig(responseObject.body);
if (!tempUpdatedConfig.length > 0) return;
}
}
if (responseObject.statusCode >= 300) {
// Server threw an error
@@ -181,30 +144,11 @@ exports.requestCheckIn = async (req, res) => {
return res.sendStatus(200);
}
/**
* Express JS Wrapper for checking and updating client config
* @param {*} req
* @param {*} res
* @returns
*/
exports.updateClientConfigWrapper = async (req, res) => {
// Convert the online status to a boolean to be worked with
log.DEBUG("REQ Body: ", req.body);
const updatedKeys = await updateClientConfig(req.body);
if (updatedKeys) {
log.DEBUG("Keys have been updated, updating running config and checking in with the server: ", updatedKeys);
runningClientConfig = await getFullConfig();
await this.checkIn(true);
}
res.status(200).json(updatedKeys);
}
/** Controller for the /client/presets endpoint
* This is the endpoint wrapper to get the presets object
*/
exports.getPresets = async (req, res) => {
runningClientConfig.nearbySystems = getPresets();
return res.status(200).json(runningClientConfig.nearbySystems);
return res.status(200).json(getPresets());
}
/** Controller for the /client/updatePreset endpoint
@@ -213,10 +157,8 @@ exports.getPresets = async (req, res) => {
exports.updatePreset = async (req, res) => {
checkBodyForPresetFields(req, res, () => {
updatePreset(req.body.systemName, () => {
runningClientConfig.nearbySystems = getPresets();
this.checkIn(true);
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});
})
}
@@ -226,8 +168,6 @@ exports.updatePreset = async (req, res) => {
exports.addNewPreset = async (req, res) => {
checkBodyForPresetFields(req, res, () => {
addNewPreset(req.body.systemName, req.body.frequencies, req.body.mode, () => {
runningClientConfig.nearbySystems = getPresets();
this.checkIn(true);
return res.sendStatus(200);
}, req.body.trunkFile);
});
@@ -238,10 +178,8 @@ exports.addNewPreset = async (req, res) => {
*/
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." })
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, () => {
runningClientConfig.nearbySystems = getPresets();
this.checkIn(true);
return res.sendStatus(200);
}, req.body.trunkFile);
});

1992
Client/package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -17,7 +17,7 @@
"express": "~4.16.1",
"http-errors": "~1.6.3",
"morgan": "~1.9.1",
"replace-in-file": "~7.0.1",
"replace-in-file": "~6.3.5",
"@discordjs/builders": "^1.4.0",
"@discordjs/rest": "^1.4.0",
"discord.js": "^14.7.1"

View File

@@ -1,5 +1,5 @@
import argparse, platform, os
from discord import Intents, Client, Member, opus, Activity, ActivityType
from discord import Intents, Client, Member, opus
from discord.ext import commands
from NoiseGatev2 import NoiseGate
@@ -8,34 +8,31 @@ async def load_opus():
# Check the system type and load the correct library
# Linux ARM AARCH64 running 32bit OS
processor = platform.machine()
script_dir = os.path.dirname(os.path.abspath(__file__))
print("Processor: ", processor)
if os.name == 'nt':
if processor == "AMD64":
opus.load_opus(os.path.join(script_dir, './opus/libopus_amd64.dll'))
if processor == "AMD64":
print(f"Loaded OPUS library for AMD64")
opus.load_opus('./opus/libopus_amd64.dll')
return "AMD64"
else:
if processor == "aarch64":
opus.load_opus(os.path.join(script_dir, './opus/libopus_aarcch64.so'))
if processor == "aarch64":
print(f"Loaded OPUS library for aarch64")
opus.load_opus('./opus/libopus_aarcch64.so')
return "aarch64"
elif processor == "armv7l":
opus.load_opus(os.path.join(script_dir, './opus/libopus_armv7l.so'))
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, presence="the radio"):
def main(clientId='OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY', channelId=367396189529833476, NGThreshold=50, deviceId=1):
intents = Intents.default()
client = commands.Bot(command_prefix='!', intents=intents)
client = commands.Bot(command_prefix='!', intents=intents)
@client.event
async def on_ready():
print(f'We have logged in as {client.user}')
# Set the presence of the bot (what it's listening to)
await client.change_presence(activity=Activity(type=ActivityType.listening, name=presence))
channelIdToJoin = client.get_channel(channelId)
print("Channel", channelIdToJoin)
@@ -58,24 +55,21 @@ def main(clientId='OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY',
client.run(clientId)
if __name__ == "__main__":
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")
parser.add_argument("-p", "--presence", type=str, help="What the bot should be listening to")
args = parser.parse_args()
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
if (not args.NGThreshold):
args.NGThreshold = 50
print("Arguments:", args)
print("Arguments:", args)
main(
clientId=args.clientId,
channelId=args.channelId,
NGThreshold=args.NGThreshold,
deviceId=args.deviceId,
presence=args.presence
)
main(
clientId=args.clientId,
channelId=args.channelId,
NGThreshold=args.NGThreshold,
deviceId=args.deviceId
)

View File

@@ -1,180 +0,0 @@
import pyaudio
import wave, logging, threading, time, queue, signal, argparse, audioop
from os import path, makedirs
logging.basicConfig(format="%(asctime)s: %(message)s", level=logging.INFO,datefmt="%H:%M:%S")
class DiscordRecorder:
def __init__(self, DEVICE_ID, CHUNK = 960, FORMAT = pyaudio.paInt16, CHANNELS = 2, RATE = 48000, FILENAME = "./recs/radio.wav"):
self.pa_instance = pyaudio.PyAudio()
self.DEVICE_ID = DEVICE_ID
self.CHUNK = CHUNK
self.FORMAT = FORMAT
self.CHANNELS = CHANNELS
self.RATE = RATE
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
self.FILENAME = FILENAME
self._check_file_path_exists()
self.queued_frames = queue.Queue()
self.stop_threads = threading.Event()
self.recording_thread = None
self.saving_thread = None
self.running_stream = None
# Wrapper to check if the given filepath (not file itself) exists
def _check_file_path_exists(self):
if not path.exists(path.dirname(self.FILENAME)):
makedirs(path.dirname(self.FILENAME), exist_ok=True)
# Wrapper for the recorder thread; Adds new data to the queue
def _recorder(self):
logging.info("* Recording Thread Starting")
while True:
try:
curr_buffer = bytearray(self.stream.stream.read(self.CHUNK))
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 self.queued_frames.put(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 self.queued_frames.put(curr_buffer)
except OSError as e:
LOGGER.warning(e)
pass
# check for stop
if self.stop_threads.is_set():
break
# Wrapper for saver thread; Saves the queue to the file
def _saver(self):
logging.info("* Saving Thread Starting")
while True:
if not self.queued_frames.empty():
dequeued_frames = []
for i in range(self.queued_frames.qsize()):
dequeued_frames.append(self.queued_frames.get())
if not path.isfile(self.FILENAME):
wf = wave.open(self.FILENAME, 'wb')
wf.setnchannels(self.CHANNELS)
wf.setsampwidth(self.pa_instance.get_sample_size(self.FORMAT))
wf.setframerate(self.RATE)
wf.writeframes(b''.join(dequeued_frames))
wf.close()
else:
read_file = wave.open(self.FILENAME, 'rb')
read_file_data = read_file.readframes(read_file.getnframes())
read_file.close()
wf = wave.open(self.FILENAME, 'wb')
wf.setnchannels(self.CHANNELS)
wf.setsampwidth(self.pa_instance.get_sample_size(self.FORMAT))
wf.setframerate(self.RATE)
wf.writeframes(read_file_data)
wf.writeframes(b''.join(dequeued_frames))
wf.close()
# check for stop
if self.stop_threads.is_set():
break
time.sleep(5)
# Start the recording function
def start_recording(self):
logging.info("* Recording")
self.running_stream = self.pa_instance.open(
input_device_index=self.DEVICE_ID,
format=self.FORMAT,
channels=self.CHANNELS,
rate=self.RATE,
input=True,
frames_per_buffer=self.CHUNK
)
self.recording_thread = threading.Thread(target=self._recorder)
self.recording_thread.start()
self.saving_thread = threading.Thread(target=self._saver)
self.saving_thread.start()
# Stop the recording function
def stop_recording(self):
self.stop_threads.set()
self.recording_thread.join()
self.saving_thread.join()
self.running_stream.stop_stream()
self.running_stream.close()
self.pa_instance.terminate()
logging.info("* Done recording")
class GracefulExitCatcher:
def __init__(self, stop_callback):
self.stop = False
# The function to run when the exit signal is caught
self.stop_callback = stop_callback
# Update what happens when these signals are caught
signal.signal(signal.SIGINT, self.exit_gracefully)
signal.signal(signal.SIGTERM, self.exit_gracefully)
def exit_gracefully(self, *args):
logging.info("* Stop signal caught...")
# Stop the main loop
self.stop = True
# Run the given callback function
self.stop_callback()
if __name__ == "__main__":
parser = argparse.ArgumentParser()
parser.add_argument("deviceId", type=int, help="The ID of the audio device to use")
parser.add_argument("filename", type=str, help="The filepath/filename of the output file")
args = parser.parse_args()
logging.debug("Arguments:", args)
recorder = DiscordRecorder(args.deviceId, FILENAME=args.filename)
exit_catcher = GracefulExitCatcher(recorder.stop_recording)
recorder.start_recording()
while not exit_catcher.stop:
time.sleep(1)

View File

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

View File

@@ -1,183 +0,0 @@
.node-card {
position: relative;
display: flex;
flex-direction: column;
min-width: 0;
word-wrap: break-word;
background-color: #fff;
background-clip: border-box;
border: 1px solid #eff0f2;
border-radius: 1rem;
margin-bottom: 24px;
box-shadow: 0 2px 3px #e4e8f0;
}
.avatar-md {
height: 4rem;
width: 4rem;
}
.rounded-circle {
border-radius: 50% !important;
}
.img-thumbnail {
padding: 0.25rem;
background-color: #f1f3f7;
border: 1px solid #eff0f2;
border-radius: 0.75rem;
}
.avatar-title {
align-items: center;
background-color: #3b76e1;
color: #fff;
display: flex;
font-weight: 500;
height: 100%;
justify-content: center;
width: 100%;
}
.bg-soft-primary {
background-color: rgba(59, 118, 225, .25) !important;
}
a {
text-decoration: none !important;
}
.badge-soft-danger {
color: #f56e6e !important;
background-color: rgba(245, 110, 110, .1);
}
.badge-soft-success {
color: #63ad6f !important;
background-color: rgba(99, 173, 111, .1);
}
.mb-0 {
margin-bottom: 0 !important;
}
.badge {
display: inline-block;
padding: 0.25em 0.6em;
font-size: 75%;
font-weight: 500;
line-height: 1;
color: #fff;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.75rem;
}
/* Info Card Section */
.info-card {
background-color: #fff;
border-radius: 10px;
border: none;
position: relative;
margin-bottom: 30px;
box-shadow: 0 0.46875rem 2.1875rem rgba(90, 97, 105, 0.1), 0 0.9375rem 1.40625rem rgba(90, 97, 105, 0.1), 0 0.25rem 0.53125rem rgba(90, 97, 105, 0.12), 0 0.125rem 0.1875rem rgba(90, 97, 105, 0.1);
}
.info-card .card-statistic .card-icon-large .bi {
font-size: 110px;
}
.info-card .card-statistic .card-icon {
text-align: center;
line-height: 50px;
margin-left: 15px;
color: #000;
position: absolute;
right: -5px;
top: 20px;
opacity: 0.1;
}
/* Info Card Background Colors */
.l-bg-cherry {
background: linear-gradient(to right, #493240, #f09) !important;
color: #fff;
}
.l-bg-blue-dark {
background: linear-gradient(to right, #373b44, #4286f4) !important;
color: #fff;
}
.l-bg-green-dark {
background: linear-gradient(to right, #0a504a, #38ef7d) !important;
color: #fff;
}
.l-bg-orange-dark {
background: linear-gradient(to right, #a86008, #ffba56) !important;
color: #fff;
}
.l-bg-cyan {
background: linear-gradient(135deg, #289cf5, #84c0ec) !important;
color: #fff;
}
.l-bg-green {
background: linear-gradient(135deg, #23bdb8 0%, #43e794 100%) !important;
color: #fff;
}
.l-bg-orange {
background: linear-gradient(to right, #f9900e, #ffba56) !important;
color: #fff;
}
/* Global Section */
.sidebar-container {
min-height: 95vh;
}
.sidebar {
position: fixed;
top: 5vh;
bottom: 0;
left: 0;
}
/* User table section */
.label {
border-radius: 3px;
font-size: 1.1em;
font-weight: 600;
}
.user-list tbody td .user-subhead {
font-size: 1em;
font-style: italic;
}
.table thead tr th {
text-transform: uppercase;
font-size: 0.875em;
}
.table thead tr th {
border-bottom: 2px solid #e7ebee;
}
.table tbody tr td:first-child {
font-size: 1.125em;
font-weight: 300;
}
.table tbody tr td {
font-size: 0.875em;
vertical-align: middle;
border-top: 1px solid #e7ebee;
padding: 12px 8px;
}

View File

@@ -1,402 +0,0 @@
$(document).ready(async () => {
console.log("Loading stored notifications...");
await loadStoredToasts();
console.log("Showing stored notifications...");
await showStoredToasts();
});
/**
* Gets all toasts stored in local storage
*
* @returns {Object} Object of toasts in storage
*/
function getStoredToasts() {
if (localStorage.getItem("toasts")) {
const storedToasts = JSON.parse(localStorage.getItem("toasts"));
console.log("LOADED STORED TOASTS: ", storedToasts);
navbarUpdateNotificationBellCount(storedToasts);
return storedToasts;
}
else return false
}
/**
* Adds a toast to storage, will not allow duplicates
*
* @param {Date} time The date object from when the toast was created
* @param {*} message The message of the toast
*/
function addToastToStorage(time, message) {
var toasts = [{ 'time': time, 'message': message }]
var storedToasts = getStoredToasts();
console.log("Adding new notification to storage: ", toasts);
if (storedToasts) {
toasts = toasts.concat(storedToasts);
console.log("Combined new and stored notifications: ", toasts);
toasts = toasts.filter((value, index, self) =>
index === self.findIndex((t) => (
t.time === value.time && t.message === value.message
))
)
}
console.log("Deduped stored notifications: ", toasts);
localStorage.setItem("toasts", JSON.stringify(toasts));
navbarUpdateNotificationBellCount(toasts);
}
/**
* Removes a toast from the local storage
*
* @param {Date} time The date object from when the toast was created
* @param {*} message The message of the toast
*/
function removeToastFromStorage(time, message) {
const toastToRemove = { 'time': time, 'message': message }
console.log("Toast to remove: ", toastToRemove);
var toasts = getStoredToasts();
console.log("Stored toasts: ", toasts);
if (toasts.indexOf(toastToRemove)) toasts.splice(toasts.indexOf(toastToRemove) - 1, 1)
console.log("Toasts with selected toast removed: ", toasts);
localStorage.setItem("toasts", JSON.stringify(toasts));
navbarUpdateNotificationBellCount(toasts);
}
/**
* Shows all stored toasts
*/
function showStoredToasts() {
const storedToasts = getStoredToasts();
if (!storedToasts) return
console.log("Loaded stored notifications to show: ", storedToasts);
for (const toast of storedToasts) {
const toastId = `${toast.time}-toast`;
console.log("Showing stored toast: ", toast, toastId);
const toastElement = bootstrap.Toast.getOrCreateInstance(document.getElementById(toastId));
toastElement.show();
}
}
/**
* Loads all toasts stored in the local storage into the DOM of the webpage
*/
function loadStoredToasts() {
const storedToasts = getStoredToasts();
if (!storedToasts) return
console.log("Loaded stored notifications: ", storedToasts);
for (const toast of storedToasts) {
createToast(toast.message, { time: toast.time })
}
}
/**
* Will update the count of notifications on the bell icon in the navbar
*
* @param {Array} storedToasts An array of stored toasts to be counted and updated in the navbar
*/
function navbarUpdateNotificationBellCount(storedToasts) {
const notificationBellIcon = document.getElementById("navbar-notification-bell");
var notificationBellCount = document.getElementById("notification-bell-icon-count");
if (!notificationBellCount) {
notificationBellCount = document.createElement('span');
notificationBellCount.id = "notification-bell-icon-count";
notificationBellCount.classList.add('badge');
notificationBellCount.classList.add('text-bg-secondary');
notificationBellCount.appendChild(document.createTextNode(storedToasts.length));
}
else notificationBellCount.innerHTML = storedToasts.length;
notificationBellIcon.appendChild(notificationBellCount);
}
/**
* Remove a frequency input from the DOM
*
* @param {string} system The system name to add the frequency to
* @param {string} inputId [OPTIONAL] The ID of input, this can be anything unique to this input. If this is not provided the number of frequencies will be used as the ID
*/
function addFrequencyInput(system, inputId = null) {
if (!inputId) inputId = $(`[id^="${system}_systemFreqRow_"]`).length;
// Create new input
var icon = document.createElement('i');
icon.classList.add('bi');
icon.classList.add('bi-x-circle');
icon.classList.add('text-black');
var remove = document.createElement('a');
remove.classList.add('align-middle');
remove.classList.add('float-left');
remove.href = '#'
remove.onclick = () => { removeFrequencyInput(`${system}_systemFreqRow_${inputId}`) }
remove.appendChild(icon);
var childColRemoveIcon = document.createElement('div');
childColRemoveIcon.classList.add('col-2');
childColRemoveIcon.appendChild(remove);
var input = document.createElement('input');
input.classList.add('form-control');
input.id = `${system}_systemFreq_${inputId}`;
input.type = 'text';
var childColInput = document.createElement('div');
childColInput.classList.add('col-10');
childColInput.appendChild(input);
var childRow = document.createElement('div');
childRow.classList.add("row");
childRow.classList.add("px-1");
childRow.appendChild(childColInput);
childRow.appendChild(childColRemoveIcon);
var colParent = document.createElement('div');
colParent.classList.add("col-md-6");
colParent.classList.add("mb-1");
colParent.id = `${system}_systemFreqRow_${inputId}`
colParent.appendChild(childRow);
document.getElementById(`frequencyRow_${system.replaceAll(" ", "_")}`).appendChild(colParent);
}
/**
* Add a toast element to the DOM
*
* @param {*} notificationMessage The message of the notification
* @param {Date} param1.time The date object for when the toast was created, blank if creating new
* @param {boolean} param1.showNow Show the toast now or just store it
* @returns
*/
function createToast(notificationMessage, { time = undefined, showNow = false } = {}) {
if (!time) time = new Date(Date.now());
else time = new Date(Date.parse(time));
const toastTitle = document.createElement('strong');
toastTitle.classList.add('me-auto');
toastTitle.appendChild(document.createTextNode("Server Notification"));
const toastTime = document.createElement('small');
toastTime.appendChild(document.createTextNode(time.toLocaleString()));
const toastClose = document.createElement('button');
toastClose.type = 'button';
toastClose.classList.add('btn-close');
toastClose.ariaLabel = 'Close';
toastClose.setAttribute('data-bs-dismiss', 'toast');
toastClose.onclick = () => { removeToastFromStorage(time.toISOString(), notificationMessage); };
const toastHeader = document.createElement('div');
toastHeader.classList.add('toast-header');
toastHeader.appendChild(toastTitle);
toastHeader.appendChild(toastTime);
toastHeader.appendChild(toastClose);
const toastMessage = document.createElement('p');
toastMessage.classList.add("px-2");
toastMessage.appendChild(document.createTextNode(notificationMessage));
const toastBody = document.createElement('div');
toastBody.classList.add('toast-body');
toastBody.appendChild(toastMessage);
const wrapperDiv = document.createElement('div');
wrapperDiv.classList.add('toast');
//wrapperDiv.classList.add('position-fixed');
wrapperDiv.id = `${time.toISOString()}-toast`;
wrapperDiv.role = 'alert';
wrapperDiv.ariaLive = 'assertive';
wrapperDiv.ariaAtomic = true;
wrapperDiv.setAttribute('data-bs-delay', "7500");
wrapperDiv.setAttribute('data-bs-animation', true);
wrapperDiv.appendChild(toastHeader);
wrapperDiv.appendChild(toastMessage);
document.getElementById("toastZone").appendChild(wrapperDiv);
addToastToStorage(time.toISOString(), notificationMessage);
if (showNow) {
const toastElement = bootstrap.Toast.getOrCreateInstance(document.getElementById(`${time.toISOString()}-toast`));
toastElement.show();
}
return;
}
function sendNodeHeartbeat(nodeId) {
const Http = new XMLHttpRequest();
const url = '/client/requestCheckIn' + nodeId;
Http.open("GET", url);
Http.send();
Http.onloadend = (e) => {
console.log(Http.responseText)
createToast(Http.responseText, { showNow: true });
}
}
function joinServer() {
const preset = document.getElementById("selectRadioPreset").value;
const clientId = document.getElementById("inputDiscordClientId").value;
const channelId = document.getElementById("inputDiscordChannelId").value;
const reqBody = {
'preset': preset,
'clientId': clientId,
'channelId': channelId
};
console.log(reqBody);
const Http = new XMLHttpRequest();
const url = '/bot/join';
Http.open("POST", url);
Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody));
Http.onloadend = (e) => {
const responseObject = JSON.parse(Http.responseText)
console.log(Http.status);
console.log(responseObject);
createToast(`${responseObject.name} will join shortly`);
location.reload();
}
}
function leaveServer() {
const reqBody = {};
const Http = new XMLHttpRequest();
const url = '/bot/leave';
Http.open("POST", url);
Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody));
Http.onloadend = (e) => {
const responseObject = Http.responseText;
console.log(Http.status);
console.log(responseObject);
createToast(`${responseObject} is leaving`, { showNow: true });
}
}
function saveNodeDetails() {
const nodeName = document.getElementById("inputNodeName").value;
const nodeIp = document.getElementById("inputNodeIp").value;
const nodePort = document.getElementById("inputOrgName").value;
const nodeLocation = document.getElementById("inputNodeLocation").value;
const reqBody = {
'name': nodeName,
'ip': nodeIp,
'port': nodePort,
'location': nodeLocation
}
console.log("Request Body: ", reqBody);
const Http = new XMLHttpRequest();
const url = '/client';
Http.open("PUT", url);
Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody));
Http.onloadend = (e) => {
const responseObject = JSON.parse(Http.responseText);
console.log(Http.status);
console.log(responseObject);
createToast(`Node Updated!`);
location.reload();
}
}
function addNewSystem() {
const nodeId = document.getElementById("nodeId").value;
const systemName = document.getElementById(`New System_systemName`).value;
const systemMode = document.getElementById(`New System_systemMode`).value;
const inputSystemFreqs = $(`[id^="New System_systemFreq_"]`);
let systemFreqs = [];
for (const inputFreq of inputSystemFreqs) {
systemFreqs.push(inputFreq.value);
}
const reqBody = {
'systemName': systemName,
'mode': systemMode,
'frequencies': systemFreqs
}
if (reqBody.mode == "p25") reqBody.trunkFile = 'none';
console.log("Request Body: ", reqBody);
const Http = new XMLHttpRequest();
const url = "/client/addPreset";
Http.open("POST", url);
Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody));
Http.onloadend = (e) => {
const responseObject = Http.responseText
console.log(Http.status);
console.log(responseObject);
createToast(`${systemName} Added!`);
location.reload();
}
}
function updateSystem(systemName) {
const nodeId = document.getElementById("nodeId").value;
const systemMode = document.getElementById(`${systemName}_systemMode`).value;
const inputSystemFreqs = $(`[id^="${systemName}_systemFreq_"]`);
let systemFreqs = [];
for (const inputFreq of inputSystemFreqs) {
systemFreqs.push(inputFreq.value);
}
const reqBody = {
'systemName': systemName,
'mode': systemMode,
'frequencies': systemFreqs
}
if (reqBody.mode == "p25") reqBody.trunkFile = 'none';
console.log("Request Body: ", reqBody);
const Http = new XMLHttpRequest();
const url = "/client/updatePreset";
Http.open("POST", url);
Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody));
Http.onloadend = (e) => {
const responseObject = Http.responseText;
console.log(Http.status);
console.log(responseObject);
createToast(`${systemName} Updated!`);
location.reload();
}
}
function removeSystem(systemName) {
const nodeId = document.getElementById("nodeId").value;
const reqBody = {
'systemName': systemName,
}
console.log("Request Body: ", reqBody);
const Http = new XMLHttpRequest();
const url = '/client/removePreset';
Http.open("POST", url);
Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody));
Http.onloadend = (e) => {
const responseObject = Http.responseText;
console.log(Http.status);
console.log(responseObject);
createToast(`${systemName} Removed!`);
location.reload();
}
}
function requestNodeUpdate() {
}
function removeFrequencyInput(elementId) {
const element = document.getElementById(elementId);
element.remove();
}

View File

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

View File

@@ -2,26 +2,13 @@
---
The client application communicates with the server through the provided API. Each client instance waits for join requests sent by users through Discord. Once a join request is received, the client uses the SDR application to tune into the specified radio preset. It then establishes a connection to Discord, allowing users to listen to the selected radio preset in real-time.
In addition to its interaction with the server, the client also has its own API and web application. This enables users to directly interface with the client, perform actions specific to the client application, and access relevant information about the connected SDR and radio presets.
Explanation here
## Requirements
---
### Hardware
- SBC
- [Orange Pi](https://www.amazon.com/dp/B0BN16ZLXB/r)
- [Raspberry Pi](https://www.canakit.com/raspberry-pi-4-4gb.html)
- [Rock Pi](https://www.okdo.com/us/p/okdo-rock-4-model-c-4gb-single-board-computer-rockchip-rk3399-t-arm-cortex-a72-cortex-a53/)
- SDR
- [Nooelec RTL-SDR v5 Bundle ](https://www.amazon.com/dp/B01GDN1T4S)
- [RTL-SDR Blog V3](https://www.amazon.com/dp/B0BMKB3L47)
- [Nooelec NESDR Mini](https://www.amazon.com/dp/B009U7WZCA)
- Proper Power Adapter (Sometimes comes in SBC Packs)
- SD Card (Sometimes comes in SBC Packs)
Requirements here (not modules, that will be installed with npm)

View File

@@ -15,9 +15,7 @@ router.get('/status', botController.getStatus);
*
* @param req The request sent from the master
* @param req.body.channelId The channel ID to join
* @param req.body.clientId The discord Client ID to use when connecting to the server
* @param req.body.presetName The name of the preset to start listening to
* @param req.body.NGThreshold [OPTIONAL] The noisegate threshold, this will default to 50
*/
router.post('/join', botController.joinServer);
@@ -29,20 +27,4 @@ router.post('/join', botController.joinServer);
*/
router.post('/leave', botController.leaveServer);
/** POST bot start recording
* If the bot is in a channel, it will start to record what it hears
*
* The status of the bot: 200 = starting to record, 202 = already recording, 204 = not in a server, 500 + JSON = encountered error
* @returns status
*/
router.post('/startRecording', botController.startRecording);
/** POST bot stop recording
* If the bot is recording, it will stop recording
*
* The status of the bot: 200 = will stop the recording, 204 = not currently recording, 500 + JSON = encountered error
* @returns status
*/
router.post('/stopRecording', botController.stopRecording);
module.exports = router;

View File

@@ -2,7 +2,7 @@
const express = require('express');
const router = express.Router();
// Controllers
const { requestCheckIn, getPresets, updatePreset, addNewPreset, removePreset, updateClient, updateClientConfigWrapper } = require("../controllers/clientController");
const { requestCheckIn, getPresets, updatePreset, addNewPreset, removePreset, updateClient } = require("../controllers/clientController");
/** GET Request a check in from the client
* Queue the client to check in with the server
@@ -16,16 +16,6 @@ router.get('/requestCheckIn', requestCheckIn);
*/
router.get('/presets', getPresets);
/**
* PUT An update to the running client config (not radio config)
* @param {number} req.body.id The ID given to the node to update
* @param {string} req.body.name The name of the node
* @param {string} req.body.ip The IP the server can contact the node on
* @param {number} req.body.port The port the server can contact the node on
* @param {string} req.body.location The physical location of the node
*/
router.put('/', updateClientConfigWrapper);
/** POST Update to preset
*
* @param req The request sent from the master

View File

@@ -1,11 +1,9 @@
var express = require('express');
var router = express.Router();
const { getFullConfig } = require('../utilities/configHandler');
/* GET home page. */
router.get('/', async function(req, res, next) {
const clientConfig = await getFullConfig();
res.render('index', { 'node': clientConfig });
router.get('/', function(req, res, next) {
res.render('index', { title: 'Express' });
});
module.exports = router;

View File

@@ -5,81 +5,24 @@ if [ "$EUID" -ne 0 ]
then echo "Please run as root"
exit
fi
# Prompt the user for reboot confirmation
read -p "This script will install all required components for the DRB client. Are you okay with rebooting afterward? If not, you will have to reboot later before running the applications to finish the installation. (Reboot?: y/n): " confirm
# Convert user input to lowercase for case-insensitive comparison
confirm="${confirm,,}"
if [[ "$confirm" != "y" && "$confirm" != "yes" ]]; then
echo "Script will not reboot."
should_reboot=false
else
echo "Script will reboot after completion."
should_reboot=true
fi
echo "----- Starting Radio Node Client Install Script -----"
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
# Copy the example env file
cp .env.example .env
ls -ld $SCRIPT_DIR | awk '{print $3}' >> ./config/installerName
# Copy the radio config example file
cp config/radioPresets.json.EXAMPLE config/radioPresets.json
# Setup user for service
useradd -M RadioNode
usermod -s -L RadioNode
echo "----- Collecting Setup Information -----"
# Create the .env file from the example
cp $SCRIPT_DIR/.env.example $SCRIPT_DIR/.env
# Ask the user for input and store in variables
echo " \\Client Config"
read -p " Enter Node Name: " nodeName
read -p " Enter Node IP: " nodeIP
read -p " Enter Node Port: " nodePort
read -p " Enter Node Location: " nodeLocation
read -p " Enter Audio Device ID: " audioDeviceID
echo " \\Server Config"
read -p " Enter Server IP (leave blank if using hostname): " serverIP
if [ -z "$serverIP" ]; then
read -p " Enter Server Hostname: " serverHostname
fi
read -p " Enter Server Port: " serverPort
# Change the ownership of the directory to the service user
chown RadioNode -R $SCRIPT_DIR
# Update the values in the env file using sed
sed -i "s/^AUDIO_DEVICE_ID=\".*\"$/AUDIO_DEVICE_ID=\"$audioDeviceID\"/" .env
sed -i "s/^CLIENT_NAME=\".*\"$/CLIENT_NAME=\"$nodeName\"/" .env
sed -i "s/^CLIENT_IP=\".*\"$/CLIENT_IP=\"$nodeIP\"/" .env
sed -i "s/^CLIENT_PORT=.*$/CLIENT_PORT=$nodePort/" .env
sed -i "s/^CLIENT_LOCATION=\".*\"$/CLIENT_LOCATION=\"$nodeLocation\"/" .env
if [ -z "$serverIP" ]; then
sed -i "s/^SERVER_HOSTNAME=\".*\"$/SERVER_HOSTNAME=\"$serverHostname\"/" .env
else
sed -i "s/^SERVER_IP=\".*\"$/SERVER_IP=\"$serverIP\"/" .env
fi
sed -i "s/^SERVER_PORT=\".*\"$/SERVER_PORT=\"$serverPort\"/" .env
echo "----- Config file has been updated -----"
# Display the updated values
echo "----- Start of Config File -----"
cat .env
echo "----- End of Config File -----"
echo "----- Getting Dependencies -----"
# Check for updates
apt-get update
# Install Node Repo
# Get the CPU architecture
cpu_arch=$(uname -m)
# Print the CPU architecture for verification
echo "Detected CPU Architecture: $cpu_arch"
# Check if the architecture is ARMv6
if [[ "$cpu_arch" == "armv6"* ]]; then
echo "----- CPU Architecture is ARMv6 or compatible. -----"
echo "----- CPU Architectre is not compatible with dependencies of this project, please use a newer CPU architecture -----"
exit
curl -fsSL https://deb.nodesource.com/setup_current.x | sudo -E bash -
# Update the system
@@ -87,41 +30,16 @@ apt-get update
apt-get upgrade -y
# Install the necessary packages
apt-get install -y nodejs portaudio19-dev libportaudio2 libpulse-dev pulseaudio apulse python3 python3-pip git
apt-get install -y nodejs portaudio19-dev libportaudio2 libpulse-dev pulseaudio apulse python3 python3-pip
# Ensure pulse audio is running
pulseaudio
# Install the node packages from the project
npm i
# Install the python packages needed for the bot
pip install -r ./pdab/requirements.txt
echo "----- Setting up Pulse Audio -----"
# Ensure pulse audio is running as system so the service can see the audio device
systemctl --global disable pulseaudio.service pulseaudio.socket
# Update the PulseAudio config to disable autospawning
sed -i 's/autospawn = .*$/autospawn = no/' /etc/pulse/client.conf
# Add the system PulseAudio service
echo "[Unit]
Description=PulseAudio system server
[Service]
Type=notify
ExecStart=pulseaudio --daemonize=no --system --realtime --log-target=journal
[Install]
WantedBy=multi-user.target" >> /etc/systemd/system/PulseAudio.service
# Add the root user to the pulse-access group
usermod -aG pulse-access root
usermod -aG pulse-access pi
# Enable the PulseAudio service
systemctl enable PulseAudio.service
echo "----- Setting up Radio Node Service -----"
pip install -r
# Setup bot service
echo "[Unit]
@@ -129,80 +47,32 @@ Description=Radio Node Service
After=network.target
[Service]
WorkingDirectory=$SCRIPT_DIR/
WorkingDirectory="$SCRIPT_DIR"
ExecStart=/usr/bin/node .
Restart=always
RestartDelay=10
Environment=\"DEBUG='client:*'\"
User=RadioNode
Environment=DEBUG='client:*'
[Install]
WantedBy=multi-user.target" >> /etc/systemd/system/RadioNode.service
# Enable the Radio Node service
systemctl enable RadioNode.service
echo "----- Setting up Radio Node Update Service -----"
# Setup bot update service
echo "[Unit]
Description=Radio Node Updater Service
After=network.target
[Service]
WorkingDirectory=$SCRIPT_DIR/
WorkingDirectory="$SCRIPT_DIR"
ExecStart=/usr/bin/bash update.sh
Restart=on-failure
User=RadioNode
[Install]
WantedBy=multi-user.target" >> /etc/systemd/system/RadioNodeUpdater.service
# Install OP25
echo "----- Installing OP25 from Source -----"
# Clone the OP25 Git
cd /opt/
git clone https://github.com/boatbod/op25.git
cd op25
# Enable the service
systemctl enable RadioNode.service
# Run the OP25 install script
bash ./install.sh
# Create the config file for the client or user to update later
cp /opt/op25/op25/gr-op25_repeater/apps/p25_rtl_example.json /opt/op25/op25/gr-op25_repeater/apps/radioNodeOP25Config.json
# Create the OP25 service
echo "[Unit]
Description=OP25 Service
After=network.target
[Service]
WorkingDirectory=/opt/op25/op25/gr-op25_repeater/apps
ExecStart=./multi_rx.py -c radioNodeOP25Config.json
Restart=always
[Install]
WantedBy=multi-user.target" >> /etc/systemd/system/OP25.service
echo "----- OP25 Setup Complete -----"
# Enable the OP25 service, don't start it though as the user needs to config
systemctl enable OP25.service
echo "----- OP25 Enabled; Please ensure to update the configuration and start the service -----"
# Move back to the directory that the user started in (might not be needed?)
cd $SCRIPT_DIR
echo "----- Setup Complete! -----"
# Reboot if the user confirmed earlier
if [ "$should_reboot" = true ]; then
echo "To configure the app, please go to http://$nodeIP:$nodePort"
echo "Thank you for joining the network!"
# Prompt user to press any key before rebooting
read -rsp $'System will now reboot, press any key to continue or Ctrl+C to cancel...\n' -n1 key
echo "Rebooting..."
reboot
else
echo "To configure the app, please go to http://$nodeIP:$nodePort"
echo "Thank you for joining the network!"
echo "Please restart your device to complete the installation"
fi
# Start the service
systemctl start RadioNode.service

View File

@@ -17,12 +17,10 @@ echo "<!-- UPDATING ---!>"
# Stop any running service
systemctl stop RadioNode
# Get the owner of the current working directory
cwd_owner=$(stat -c '%U' .)
# Update the git Repo as the owner of the current working directory
sudo su -l $cwd_owner -c 'git fetch'
sudo su -l $cwd_owner -c 'git pull'
# Update the git Repo
installUser=$(cat ./config/installerName)
sudo su -l $installUser -c 'git fetch -a -p'
sudo su -l $installUser -c 'git pull'
# Install any new libraries
npm i

View File

@@ -2,8 +2,6 @@
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client", "configController");
// Modules
const { nodeObject } = require("./utilities.js");
const { getPresets } = require("../utilities/updatePresets");
const { readFileSync } = require('fs');
const path = require("path");
require('dotenv').config();
@@ -35,8 +33,4 @@ function getDeviceName(){
log.DEBUG("Device Name: ", DeviceName);
return DeviceName;
}
exports.getDeviceName = getDeviceID;
exports.getFullConfig = () => {
return 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()});
}
exports.getDeviceName = getDeviceID;

View File

@@ -4,7 +4,6 @@ const debug = require('debug');
require('dotenv').config();
// Modules
const { writeFile } = require('fs');
const { inspect } = require('util');
const logLocation = process.env.LOG_LOCATION;
@@ -35,31 +34,31 @@ exports.DebugBuilder = class DebugBuilder {
this.INFO = (...messageParts) => {
const _info = debug(`${appName}:${fileName}:INFO`);
_info(messageParts);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:INFO\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
writeToLog(`${appName}:${fileName}:INFO\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
}
this.DEBUG = (...messageParts) => {
const _debug = debug(`${appName}:${fileName}:DEBUG`);
_debug(messageParts);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:DEBUG\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
writeToLog(`${appName}:${fileName}:DEBUG\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
}
this.VERBOSE = (...messageParts) => {
const _verbose = debug(`${appName}:${fileName}:VERBOSE`);
_verbose(messageParts);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:VERBOSE\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
writeToLog(`${appName}:${fileName}:VERBOSE\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
}
this.WARN = (...messageParts) => {
const _warn = debug(`${appName}:${fileName}:WARNING`);
_warn(messageParts);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:WARNING\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
writeToLog(`${appName}:${fileName}:WARNING\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
}
this.ERROR = (...messageParts) => {
const _error = debug(`${appName}:${fileName}:ERROR`);
_error(messageParts);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:ERROR\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
writeToLog(`${appName}:${fileName}:ERROR\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
if (process.env.EXIT_ON_ERROR && process.env.EXIT_ON_ERROR > 0) {
writeToLog("!--- EXITING ---!", appName);
setTimeout(process.exit, process.env.EXIT_ON_ERROR_DELAY ?? 0);

View File

@@ -5,16 +5,11 @@ const log = new DebugBuilder("client", "httpRequests");
require('dotenv').config();
// Modules
const http = require("http");
const { isJsonString } = require("./utilities.js");
exports.requestOptions = class requestOptions {
constructor(path, method, hostname = undefined, headers = undefined, port = undefined) {
if (["POST", "PUT"].includes(method)){
log.VERBOSE("Hostname Vars: ", hostname, process.env.SERVER_HOSTNAME, process.env.SERVER_IP);
if (hostname) this.hostname = hostname;
if (!this.hostname && process.env.SERVER_HOSTNAME) this.hostname = process.env.SERVER_HOSTNAME;
if (!this.hostname && process.env.SERVER_IP) this.hostname = process.env.SERVER_IP;
if (!this.hostname) throw new Error("No server hostname / IP was given when creating a request");
constructor(path, method, hostname = undefined, headers = undefined, port = undefined) {
if (method === "POST"){
this.hostname = hostname ?? process.env.SERVER_HOSTNAME ?? process.env.SERVER_IP;
this.path = path;
this.port = port ?? process.env.SERVER_PORT;
this.method = method;
@@ -36,26 +31,31 @@ exports.sendHttpRequest = function sendHttpRequest(requestOptions, data, callbac
// Create the request
const req = http.request(requestOptions, res => {
res.on('data', (data) => {
const responseObject = {
"statusCode": res.statusCode,
"body": (isJsonString(data.toString())) ? JSON.parse(data.toString()) : data.toString()
};
log.VERBOSE("Response Object: ", responseObject);
callback(responseObject);
if (res.statusCode >= 200 && res.statusCode <= 299) {
const responseObject = {
"statusCode": res.statusCode,
"body": JSON.parse(data)
};
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 => {
if (err.code === "ECONNREFUSED"){
// Bot refused connection, assumed offline
log.WARN("Connection Refused");
}
else log.ERROR('Error: ', err.message, err);
callback(undefined);
log.ERROR('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();
req.write(data)
req.end()
}
exports.onHttpError = function onHttpError(httpStatusCode) {

View File

@@ -3,16 +3,15 @@ const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("client", "updateConfig");
// Modules
const replace = require('replace-in-file');
const { getFullConfig } = require("./configHandler.js");
class Options {
constructor(key, updatedValue) {
this.files = "./.env";
// 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
if (Array(["string", "number"]).includes(typeof updatedValue)) this.to = `${key}="${updatedValue}",`;
else this.to = `${key}=${updatedValue}`;
else this.to = `${key}=${updatedValue},`;
}
}
@@ -21,67 +20,7 @@ class Options {
* @param updatedId The updated ID assigned to the bot
*/
exports.updateId = (updatedId) => {
this.updateConfig('CLIENT_ID', updatedId);
}
/**
* Wrapper to update any or all keys in the client config
*
* @param {Object} configObject Object with what keys you wish to update (node object format, will be converted)
* @param {number} configObject.id The ID given to the node to update
* @param {string} configObject.name The name of the node
* @param {string} configObject.ip The IP the server can contact the node on
* @param {number} configObject.port The port the server can contact the node on
* @param {string} configObject.location The physical location of the node
* @returns
*/
exports.updateClientConfig = (configObject) => {
const runningConfig = getFullConfig();
var updatedKeys = []
const configKeys = Object.keys(configObject);
if (configKeys.includes("id")) {
if (runningConfig.id != configObject.id) {
this.updateConfig('CLIENT_ID', configObject.id);
updatedKeys.push({'CLIENT_ID': configObject.id});
process.env.CLIENT_ID = configObject.id;
log.DEBUG("Updated ID to: ", configObject.id);
}
}
if (configKeys.includes("name")) {
if (runningConfig.name != configObject.name) {
this.updateConfig('CLIENT_NAME', configObject.name);
updatedKeys.push({'CLIENT_NAME': configObject.name});
process.env.CLIENT_NAME = configObject.name;
log.DEBUG("Updated name to: ", configObject.name);
}
}
if (configKeys.includes("ip")) {
if (runningConfig.ip != configObject.ip) {
this.updateConfig('CLIENT_IP', configObject.ip);
updatedKeys.push({'CLIENT_IP': configObject.ip});
process.env.CLIENT_IP = configObject.ip;
log.DEBUG("Updated ip to: ", configObject.ip);
}
}
if (configKeys.includes("port")) {
if (runningConfig.port != configObject.port) {
this.updateConfig('CLIENT_PORT', configObject.port);
updatedKeys.push({'CLIENT_PORT': configObject.port});
process.env.CLIENT_PORT = configObject.port;
log.DEBUG("Updated port to: ", configObject.port);
}
}
if (configKeys.includes("location")) {
if (runningConfig.location != configObject.location) {
this.updateConfig('CLIENT_LOCATION', configObject.location);
updatedKeys.push({'CLIENT_LOCATION': configObject.location});
process.env.CLIENT_LOCATION = configObject.location;
log.DEBUG("Updated location to: ", configObject.location);
}
}
return updatedKeys;
this.updateConfig('id', updatedId);
}
/**
@@ -98,6 +37,7 @@ exports.updateConfig = function updateConfig(key, value) {
})
}
/**
* Wrapper to write changes to the file
* @param options An instance of the Objects class specified to the key being updated
@@ -106,7 +46,7 @@ exports.updateConfig = function updateConfig(key, value) {
function updateConfigFile(options, callback){
replace(options, (error, changedFiles) => {
if (error) return console.error('Error occurred:', error);
log.VERBOSE('Modified files:', changedFiles);
log.DEBUG('Modified files:', changedFiles);
callback(changedFiles);
});
}

View File

@@ -17,7 +17,7 @@ function writePresets(presets, callback = undefined) {
// Error checking
if (err) throw err;
log.DEBUG("Write Complete");
if (callback) callback(); else return
if (callback) callback()
});
}
@@ -71,9 +71,8 @@ function convertFrequencyToHertz(frequency){
*/
exports.getPresets = function getPresets() {
const presetDir = path.resolve("./config/radioPresets.json");
log.DEBUG(`Getting presets from directory: '${presetDir}'`);
if (fs.existsSync(presetDir)) return JSON.parse(fs.readFileSync(presetDir));
else return {};
log.DEBUG(`Getting presets from directory: '${presetDir}'`);
return JSON.parse(fs.readFileSync(presetDir));
}
/**

View File

@@ -25,15 +25,17 @@ exports.nodeObject = class nodeObject {
* @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 }) {
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;
}
}
@@ -259,24 +261,4 @@ function convertRadioPresetsToOP25Config(presetName){
log.DEBUG(updatedOP25Config);
return updatedOP25Config;
}
// Convert a buffer from the DB to JSON object
exports.BufferToJson = (buffer) => {
return JSON.parse(buffer.toString());
}
/**
* 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;
}
}

View File

@@ -1,169 +1,11 @@
<!DOCTYPE html>
<html lang="en" data-bs-theme="auto">
<head>
<meta name="viewport" content="width=device-width, initial-scale=1">
<title>'<%=node.name%>' - Configuration</title>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.3.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="/res/css/main.css">
</head>
<body>
<%- include('partials/navbar.ejs') %>
<div aria-live="polite" aria-atomic="true" class="position-relative">
<!-- Position it: -->
<!-- - `.toast-container` for spacing between toasts -->
<!-- - `top-0` & `end-0` to position the toasts in the upper right corner -->
<!-- - `.p-3` to prevent the toasts from sticking to the edge of the container -->
<div class="toast-container top-0 end-0 p-3 max" id="toastZone">
</div>
</div>
<div class="container">
<div class="card mb-4">
<div class="card-header">
<p>
<span class="fs-2 fw-semibold">
Node Details
</span>
</p>
</div>
<div class="card-body">
<div class="col-md-12 pt-2">
<label class="small mb-1" for="nodeStatus">Online Status:</label>
<span class="badge badge-soft-success mb-0 align-middle fs-6" id="nodeStatus">Online</span>
<br>
<div class="py-2"></div>
<!-- Join Server button-->
<a type="button" class="btn btn-info text-white" data-bs-toggle="modal" data-bs-target="#joinModal"
href="#">Join Server</a>
<!-- Leave Server button -->
<a type="button" class="btn btn-danger" href="#" onclick="leaveServer()">Leave Server</a>
<!-- Checkin with client button -->
<a type="button" class="btn btn-secondary" href="#" onclick="sendNodeHeartbeat('<%=node.id%>')">Check-in
with Node</a>
<!-- Update Client button -->
<a type="button" class="btn btn-warning disabled" href="#"
onclick="requestNodeUpdate('<%=node.id%>')">Update Node</a>
</div>
<hr>
<form>
<div class="row gx-3 mb-3">
<div class="col-md-6">
<label class="small mb-1" for="nodeId">Node ID (this is the assigned Node ID and cannot be
changed)</label>
<input class="form-control" id="nodeId" type="text" value="<%=node.id%>" disabled></input>
</div>
</div>
<div class="row gx-3 mb-3">
<div class="col-md-12">
<label class="small mb-1" for="inputNodeName">Node Name:</label>
<input class="form-control" id="inputNodeName" type="text" value="<%=node.name%>"></input>
</div>
</div>
<div class="row gx-3 mb-3">
<div class="col-md-4">
<label class="small mb-1" for="inputNodeIp">Node IP Address (that the server can
contact):</label>
<input class="form-control" id="inputNodeIp" type="text" value="<%=node.ip%>"></input>
</div>
<div class="col-md-2">
<label class="small mb-1" for="inputOrgName">Node Port (with the API):</label>
<input class="form-control" id="inputOrgName" type="number" value="<%=node.port%>"></input>
</div>
</div>
<div class="mb-3">
<label class="small mb-1" for="inputNodeLocation">Node Location (physical location):</label>
<input class="form-control" id="inputNodeLocation" type="location" value="<%=node.location%>"></input>
</div>
<h4>
Nearby Systems
</h4>
<hr>
<div class="row">
<div class="col-lg-12">
<div class="main-box no-header clearfix">
<div class="main-box-body clearfix">
<div class="table-responsive">
<table class="table user-list <% if(!node.online) { %>disabled<% } %>">
<thead>
<tr>
<th><span>System Name</span></th>
<th><span>Frequencies</span></th>
<th><span>Protocol</span></th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
<% for(const system in node.nearbySystems){ %>
<tr>
<td>
<%= system %>
</td>
<td>
<% if(node.nearbySystems[system].frequencies.length> 1) { %>
<ul>
<% for(const frequency of node.nearbySystems[system].frequencies) { %>
<li>
<%=frequency%> MHz
</li>
<% } %>
</ul>
<% } else { const frequency=node.nearbySystems[system].frequencies[0] %>
<%=frequency%> MHz
<% } %>
</td>
<td>
<span class="label label-default text-uppercase">
<%= node.nearbySystems[system].mode %>
</span>
</td>
<td>
<a href="#" class="table-link text-info label" data-bs-toggle="modal"
data-bs-target="#updateSystemModal_<%=system.replaceAll(" ", " _")%>">
<i class="bi bi-pencil"></i>
</a>
<a class="table-link text-danger label" onclick="removeSystem('<%=system%>')">
<i class="bi bi-trash"></i>
</a>
</td>
</tr>
<% // Update system modal %>
<%- include("partials/modifySystemModal.ejs", {'system': system, 'frequencies' :
node.nearbySystems[system].frequencies, 'mode' : node.nearbySystems[system].mode}) %>
<% } %>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Save changes button-->
<button class="btn btn-primary" type="button" onclick="saveNodeDetails()">Save changes</button>
<!-- Button trigger modal -->
<button type="button" class="btn btn-primary float-right" data-bs-toggle="modal"
data-bs-target="#updateSystemModal_New_System">Add New System</button>
</form>
</div>
</div>
</div>
<% // new System Modal %>
<%- include("partials/modifySystemModal.ejs", {'system': "New System" , 'frequencies' : [], 'mode' : '' }) %>
<% // Join Server Modal %>
<%- include("partials/joinModal.ejs", {'node': node}) %>
</body>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz" crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js"
integrity="sha512-3gJwYpMe3QewGELv8k/BX9vcqhryRdzRMxVfq6ngyWXwo03GFEzjsUm8Q7RZcHPHksttq7/GFoxjCVUjkjvPdw=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="/res/js/node.js"></script>
</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>

View File

@@ -1,44 +0,0 @@
<div class="modal fade" id="joinModal" tabindex="-1" aria-labelledby="joinModal" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="joinModal">Join Node <%=node.id%> to a Discord Server</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="container">
<div class="card mb-4">
<div class="card-body">
<form>
<div class="row gx-3 mb-3">
<div class="col-md-12">
<label class="small mb-1" for="inputDiscordClientId">Discord Client ID:</label>
<input class="form-control" id="inputDiscordClientId" type="text" value="" required></input>
</div>
</div>
<div class="row gx-3 mb-3">
<div class="col-md-6">
<label class="small mb-1" for="inputDiscordChannelId">Discord Channel ID:</label>
<input class="form-control" id="inputDiscordChannelId" type="text" value="" required></input>
</div>
<div class="col-md-6">
<label class="small mb-1" for="selectRadioPreset">Selected Preset:</label>
<select class="custom-select" id="selectRadioPreset">
<% for(const system in node.nearbySystems) { %>
<option value="<%=system%>"><%=system%></option>
<% } %>
</select>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="joinServer()">Join</button>
</div>
</div>
</div>
</div>

View File

@@ -1,61 +0,0 @@
<div class="modal fade" id="updateSystemModal_<%=system.replaceAll(" ", "_")%>" tabindex="-1" aria-labelledby="updateSystemModal_<%=system.replaceAll(" ", "_")%>"
aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="updateSystemModal_<%=system.replaceAll(" ", "_")%>"><%if (!system == "New System") {%>Update<%} else {%>Add a<%}%> <%=system%></h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="card mb-4">
<div class="card-body">
<form>
<div class="row gx-3 mb-3">
<label class="small mb-1 fs-6" for="systemName">System Name</label>
<input class="form-control" id="<%=system%>_systemName" type="text" value="<%if (system != "New System") {%><%= system %><%} else {%>Local Radio System<%}%>"></input>
</div>
<div class="row gx-3 mb-3" id="frequencyRow_<%=system.replaceAll(" ", "_")%>">
<label class="small mb-1 fs-6" for="systemFreq">Frequencies</label>
<% for(const frequency of frequencies) { %>
<div class="col-md-6 mb-1" id="<%=system%>_systemFreqRow_<%=frequency%>">
<div class="row px-1">
<div class="col-10">
<input class="form-control" id="<%=system%>_systemFreq_<%=frequency%>" type="text" value="<%= frequency %>"></input>
</div>
<div class="col-2">
<a class="align-middle float-left" href="#" onclick="removeFrequencyInput('<%=system%>_systemFreqRow_<%=frequency%>')"><i class="bi bi-x-circle text-black"></i></a>
</div>
</div>
</div>
<% } %>
</div>
<button type="button" class="btn btn-info text-white" onclick="addFrequencyInput('<%=system%>')">Add Frequency</button>
<hr>
<div class="row gx-3 mb-3">
<div class="col-md-6">
<label class="small mb-1 fs-6" for="<%=system%>_systemMode">Mode</label>
<br>
<select class="custom-select" id="<%=system%>_systemMode">
<option value="<%= mode ?? 'select' %>" selected><span class="text-uppercase"><%= mode ?? 'Select' %></span></option>
<% if(mode == "p25") { %>
<option value="nbfm">NBFM</option>
<% } else if (mode == "nbfm") { %>
<option value="p25">P25</option>
<% } else { %>
<option value="nbfm">NBFM</option>
<option value="p25">P25</option>
<%}%>
</select>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="location.reload()">Close</button>
<button type="button" class="btn btn-primary" <%if(!system == "New System") {%>onclick="updateSystem('<%=system%>')"<%} else {%>onclick="addNewSystem('<%=system%>')"<%}%>>Save changes</button>
</div>
</div>
</div>
</div>

View File

@@ -1,42 +0,0 @@
<nav class="navbar fixed-top navbar-expand-lg bg-body-tertiary" data-bs-theme="dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Node Master</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<% /*
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
Dropdown
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">Action</a></li>
<li><a class="dropdown-item" href="#">Another action</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li><a class="dropdown-item" href="#">Something else here</a></li>
</ul>
</li>
*/%>
<li class="nav-item">
<a class="nav-link" id="navbar-notification-bell" onclick="showStoredToasts()"><i class="bi bi-bell-fill"></i></a>
</li>
</ul>
<form class="d-flex" role="search">
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
</div>
</div>
</nav>

View File

@@ -1,52 +0,0 @@
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "give-role");
module.exports = {
data: new SlashCommandBuilder()
.setName('give-role')
.setDescription('Use this command to give a role you have to another member.')
.addUserOption(option =>
option.setName('user')
.setDescription('The user you wish to give the role to ')
.setRequired(true))
.addRoleOption(option =>
option.setName('role')
.setDescription('The role you wish to give the selected user')
.setRequired(true)),
example: "give-role",
isPrivileged: false,
requiresTokens: false,
defaultTokenUsage: 0,
deferInitialReply: true,
/*async autocomplete(interaction) {
const focusedValue = interaction.options.getFocused();
},*/
async execute(interaction) {
try{
// The role to give to the user
const selectedRole = interaction.options.getRole('role');
// The user who should be given the role
var selectedUser = interaction.options.getUser("user");
selectedUser = interaction.guild.members.cache.get(selectedUser.id);
// The user who initiated the command
const initUser = interaction.member;
log.DEBUG("Give Role DEBUG: ", initUser, selectedRole, selectedUser);
// Check if the user has the role selected
if (!initUser.roles.cache.find(role => role.name === selectedRole.name)) return await interaction.editReply(`Sorry ${initUser}, you don't have the group ${selectedRole} and thus you cannot give it to ${selectedUser}`);
// Give the selected user the role and let both the user and the initiator know
await selectedUser.roles.add(selectedRole);
return await interaction.editReply(`Ok ${initUser}, ${selectedUser} has been given the ${selectedRole} role!`)
}catch(err){
log.ERROR(err)
//await interaction.reply(err.toString());
}
}
};

View File

@@ -49,9 +49,9 @@ module.exports = {
const helpEmbed = new EmmeliaEmbedBuilder()
.setColor(0x0099FF)
.setTitle(`Help`)
.setDescription(`**General Commands**\n\n${generalCommandText}`)
.addFields(
.setTitle(`Help`)
.addFields(
{ name: 'General Commands', value: `${generalCommandText}` },
{ name: 'Paid Commands', value: `${paidCommandText}` }
)
await interaction.reply({ embeds: [helpEmbed], ephemeral: true });

View File

@@ -1,54 +1,116 @@
// Modules
const { SlashCommandBuilder } = require('discord.js');
const { customSlashCommandBuilder } = require('../utilities/customSlashCommandBuilder');
const { DebugBuilder } = require("../utilities/debugBuilder");
const { filterAutocompleteValues, filterPresetsAvailable } = require("../utilities/utils");
const { getOnlineNodes, getAllConnections } = require("../utilities/mysqlHandler");
const { joinServerWrapper } = require("../controllers/adminController");
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 SlashCommandBuilder()
data: new customSlashCommandBuilder()
.setName('join')
.setDescription('Join the channel you are in with the preset you choose')
.addStringOption(option =>
option.setName("preset")
.setDescription("The preset you would like to listen to")
.setAutocomplete(true)
.setRequired(true)),
.addAllSystemPresetOptions(),
example: "join",
isPrivileged: false,
requiresTokens: false,
defaultTokenUsage: 0,
deferInitialReply: true,
async autocomplete(interaction) {
const nodeObjects = await new Promise((recordResolve, recordReject) => {
getOnlineNodes((nodeRows) => {
recordResolve(nodeRows);
});
});
const options = await filterPresetsAvailable(nodeObjects);
// Filter the results to what the user is entering
filterAutocompleteValues(interaction, options);
},
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;
if (!channelId) return interaction.editReply(`You need to be in a voice channel, ${interaction.user}`);
log.DEBUG(`Join requested by: ${interaction.user.username}, to: '${presetName}', in channel: ${channelId} / ${guildId}`);
const connections = await getAllConnections();
const onlineBots = await getMembersInRole(interaction);
log.DEBUG("Current Connections: ", connections);
log.DEBUG("Online Bots: ", onlineBots);
const selectedClientId = await joinServerWrapper(presetName, channelId, connections);
await interaction.editReply(`Ok, ${interaction.member}. **${selectedClientId.name}** is joining your channel.`);
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)

View File

@@ -1,15 +1,40 @@
// Modules
const { SlashCommandBuilder } = require('discord.js');
const { customSlashCommandBuilder } = require('../utilities/customSlashCommandBuilder');
const { DebugBuilder } = require("../utilities/debugBuilder");
const { getAllClientIds, getKeyByArrayValue, filterAutocompleteValues } = require("../utilities/utils");
const { getAllConnections } = require('../utilities/mysqlHandler');
const { leaveServerWrapper } = require('../controllers/adminController');
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 SlashCommandBuilder()
data: new customSlashCommandBuilder()
.setName('leave')
.setDescription('Disconnect a bot from the server')
.addStringOption(option =>
@@ -22,19 +47,25 @@ module.exports = {
requiresTokens: false,
defaultTokenUsage: 0,
deferInitialReply: true,
async autocomplete(interaction) {
async autocomplete(interaction) {
const focusedValue = interaction.options.getFocused();
const connections = await getAllConnections();
const options = connections.map(conn => conn.clientObject.name);
await filterAutocompleteValues(interaction, options);
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{
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

View File

@@ -7,7 +7,7 @@ const log = new DebugBuilder("server", "remove");
module.exports = {
data: new SlashCommandBuilder()
.setName('remove')
.setDescription('Remove an RSS source by it\'s title')
.setDescription('Remove an RSS source by it\' title')
.addStringOption(option =>
option.setName('title')
.setDescription('The title of the source to remove')

View File

@@ -1,37 +0,0 @@
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "start-record");
const { getAllConnections } = require("../utilities/mysqlHandler");
const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
module.exports = {
data: new SlashCommandBuilder()
.setName('start-record')
.setDescription('Starts recording all bots online'),
example: "start-record",
isPrivileged: false,
requiresTokens: false,
defaultTokenUsage: 0,
deferInitialReply: false,
async execute(interaction) {
try{
await interaction.reply(`Ok, ${interaction.member}. **Recording** will begin shorting.`);
// Get nodes online
getAllConnections((connections) => {
for (const connection of connections){
const reqOptions = new requestOptions("/bot/startRecording", "POST", connection.node.ip, connection.node.port);
sendHttpRequest(reqOptions, JSON.stringify({}), async (responseObj) => {
log.VERBOSE("Response Object from node: ", connection, responseObj);
if (!responseObj || !responseObj.statusCode == 202 || !responseObj.statusCode == 204) return false;
if (responseObj.statusCode >= 300) return log.ERROR(responseObj.body);
// Bot is recording
await interaction.channel.send(`**${connection.clientObject.name} is now recording**`);
});
}
});
}catch(err){
log.ERROR(err)
//await interaction.reply(err.toString());
}
}
};

View File

@@ -1,37 +0,0 @@
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const log = new DebugBuilder("server", "stop-record");
const { getAllConnections } = require("../utilities/mysqlHandler");
const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
module.exports = {
data: new SlashCommandBuilder()
.setName('stop-record')
.setDescription('Starts recording all bots online'),
example: "stop-record",
isPrivileged: false,
requiresTokens: false,
defaultTokenUsage: 0,
deferInitialReply: false,
async execute(interaction) {
try{
await interaction.reply(`Ok, ${interaction.member}. **Recording** will stop shorting.`);
// Get nodes online
getAllConnections((connections) => {
for (const connection of connections){
const reqOptions = new requestOptions("/bot/stopRecording", "POST", connection.node.ip, connection.node.port);
sendHttpRequest(reqOptions, JSON.stringify({}), async (responseObj) => {
log.VERBOSE("Response Object from node: ", connection, responseObj);
if (!responseObj || !responseObj.statusCode == 204) return false;
if (responseObj.statusCode >= 300) return log.ERROR(responseObj.body);
// Bot is recording
await interaction.channel.send(`**${connection.clientObject.name} has stopped recording**`);
});
}
});
}catch(err){
log.ERROR(err)
//await interaction.reply(err.toString());
}
}
};

View File

@@ -4,9 +4,9 @@ require('dotenv').config();
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("server", "adminController");
// Utilities
const { getAllClientIds } = require("../utilities/utils");
const { getOnlineNodes, updateNodeInfo, addNodeConnection, getConnectionByNodeId, getNodeInfoFromId, checkNodeConnectionByClientId, removeNodeConnectionByNodeId } = require("../utilities/mysqlHandler");
const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
const mysqlHandler = require("../utilities/mysqlHandler");
const utils = require("../utilities/utils");
const requests = require("../utilities/httpRequests");
/** Get the presets of all online nodes, can be used for functions
*
@@ -14,15 +14,32 @@ const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests")
* @returns {*} A list of the systems online
*/
async function getPresetsOfOnlineNodes(callback) {
getOnlineNodes((onlineNodes) => {
return callback(onlineNodes);
mysqlHandler.getOnlineNodes((onlineNodes) => {
let systems = {};
onlineNodes.forEach(onlineNode => {
systems[onlineNode.id] = utils.BufferToJson(onlineNode.nearbySystems);
});
return callback(systems);
});
}
async function requestNodeListenToPreset(preset, nodeId, callback) {
mysqlHandler.getNodeInfoFromId(nodeId, (nodeObject) =>{
reqOptions = new requests.requestOptions("/bot/join", "POST", nodeObject.ip, nodeObject.port);
requests.sendHttpRequest(reqOptions, JSON.stringify({
"channelID": process.env.DEFAULT_VOICE_CHANNEL_ID,
"presetName": preset
}), (responseObject) => {
return callback(responseObject)
});
})
}
async function getNodeBotStatus(nodeId, callback) {
getNodeInfoFromId(nodeId, (nodeObject) =>{
reqOptions = new requestOptions("/bot/status", "GET", nodeObject.ip, nodeObject.port, undefined, 5);
sendHttpRequest(reqOptions, JSON.stringify({}), (responseObject) => {
mysqlHandler.getNodeInfoFromId(nodeId, (nodeObject) =>{
reqOptions = new requests.requestOptions("/bot/status", "GET", nodeObject.ip, nodeObject.port, undefined, 5);
requests.sendHttpRequest(reqOptions, JSON.stringify({}), (responseObject) => {
if (responseObject === false) {
// Bot is joined
}
@@ -34,6 +51,24 @@ async function getNodeBotStatus(nodeId, callback) {
});
}
async function requestNodeLeaveServer(nodeId, callback) {
getNodeBotStatus(nodeId, (responseObject) => {
if (responseObject === false) {
// Bot is joined
mysqlHandler.getNodeInfoFromId(nodeId, (nodeObject) =>{
reqOptions = new requests.requestOptions("/bot/leave", "POST", nodeObject.ip, nodeObject.port);
requests.sendHttpRequest(reqOptions, JSON.stringify({}), (responseObject) => {
return callback(responseObject);
});
});
}
else {
// Bot is free
return callback(false);
}
})
}
/** Return to requests for the presets of all online nodes, cannot be used in functions
*
@@ -52,161 +87,43 @@ exports.getAvailablePresets = async (req, res) => {
*
* @param {*} req Express request parameter
* @var {*} req.body.preset The preset to join (REQ)
* @var {*} req.body.nodeId The specific node to join (OPT/REQ if more than one node has the preset)
* @var {*} req.body.clientId The ID of the client that we want to join with
* @var {*} req.body.channelId The channel Id of the discord channel to join
* @var {*} req.body.nodeId The specific node to join (OPT/REQ if more than one node has the preset)
* @param {*} res Express response parameter
*/
exports.joinPreset = async (req, res) => {
if (!req.body.preset) return res.status(400).json("No preset specified");
if (!req.body.nodeId) return res.status(400).json("No node ID specified");
if (!req.body.clientId) return res.status(400).json("No client ID specified");
if (!req.body.channelId) return res.status(400).json("No channel ID specified");
const preset = req.body.preset;
const nodeId = req.body.nodeId;
const clientId = req.body.clientId;
const channelId = req.body.channelId;
const joinedClient = await joinServerWrapper(preset, channelId, clientId, nodeId);
if (!joinedClient) return res.send(400).json("No joined client");
return res.status(200).json(joinedClient);
if (!req.body.preset) return res.status(400).json("No preset specified");
await getPresetsOfOnlineNodes((systems) => {
const systemsWithSelectedPreset = Object.values(systems).filter(nodePresets => nodePresets.includes(req.body.preset)).length
if (!systemsWithSelectedPreset) return res.status(400).json("No system online with that preset");
if (systemsWithSelectedPreset > 1) {
if (!req.body.nodeId) return res.status(175).json("Multiple locations with the selected channel, please specify a nodeID (nodeId)")
requestNodeListenToPreset(req.body.preset, req.body.nodeId, (responseObject) => {
if (responseObject === false) return res.status(400).json("Timeout reached");
return res.sendStatus(responseObject.statusCode);
});
}
else {
let nodeId;
if (!req.body.nodeId) nodeId = utils.getKeyByArrayValue(systems, req.body.preset);
else nodeId = req.body.nodeId;
requestNodeListenToPreset(req.body.preset, nodeId, (responseObject) => {
if (responseObject === false) return res.status(400).json("Timeout reached");
return res.sendStatus(responseObject.statusCode);
});
}
});
}
/** Request a node to join the server listening to a specific preset
*
* @param {*} req Express request parameter
* @param {*} res Express response parameter
* @var {*} req.body.nodeId The ID of the node to disconnect
*/
exports.leaveServer = async (req, res) => {
if (!req.body.nodeId) return res.status(400).json("No node ID specified");
if (!req.body.nodeId) return res.status(400).json("No nodeID specified");
const nodeId = req.body.nodeId;
const currentConnection = await getConnectionByNodeId(nodeId);
log.DEBUG("Current Connection for node: ", currentConnection);
if (!currentConnection) return res.status(400).json("Node is not connected")
await leaveServerWrapper(currentConnection.clientObject)
return res.status(200).json(currentConnection.clientObject.name);
}
/**
* * 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 {*} connections 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
* @param {number} nodeId [OPTIONAL] The node ID to join with (will join with another node if given node is not available)
* @returns
*/
async function joinServerWrapper(presetName, channelId, connections, nodeId = 0) {
// 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.presets.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 connections === 'string') {
for (const availableClientId of availableClientIds) {
if (availableClientId.discordId != connections ) selectedClientId = availableClientId;
}
}
else {
log.DEBUG("Open connections: ", connections);
for (const connection of connections) {
log.DEBUG("Used Client ID: ", connection);
availableClientIds = availableClientIds.filter(cid => cid.discordId != connection.clientObject.discordId);
}
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];
}
let selectedNode;
if (nodeId > 0) {
for(const availableNode of nodesCurrentlyAvailable){
if (availableNode.id == nodeId) selectedNode = availableNode;
}
}
if (!selectedNode) 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);
});
return selectedClientId;
}
exports.joinServerWrapper = joinServerWrapper;
/**
*
* @param {*} clientIdObject The client ID object for the node to leave the server. Either 'clientId'||'name' can be set.
* @returns
*/
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;
requestNodeLeaveServer(req.body.nodeId, (responseObject) => {
if (responseObject === false) return res.status(400).json("Bot not joined to server");
return res.sendStatus(responseObject.statusCode);
});
}

View File

@@ -2,66 +2,18 @@
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("server", "nodesController");
// Utilities
const { getAllNodes, addNewNode, updateNodeInfo, getNodeInfoFromId, getOnlineNodes } = require("../utilities/mysqlHandler");
const {getAllNodes, addNewNode, updateNodeInfo, getNodeInfoFromId, getOnlineNodes } = require("../utilities/mysqlHandler");
const utils = require("../utilities/utils");
const { sendHttpRequest, requestOptions } = require("../utilities/httpRequests.js");
const { nodeObject } = require("../utilities/recordHelper.js");
const { joinServerWrapper } = require("../commands/join");
const refreshInterval = process.env.NODE_MONITOR_REFRESH_INTERVAL ?? 1200000;
const digitalModes = ['p25'];
/**
* Check in with a singular node, mark it offline if it's offline and
*
* @param {*} node The node Object to check in with
*/
async function checkInWithNode(node) {
const reqOptions = new requestOptions("/client/requestCheckIn", "GET", node.ip, node.port)
sendHttpRequest(reqOptions, "", (responseObj) => {
if (responseObj) {
log.DEBUG("Response from: ", node.name, responseObj);
const onlineNode = new nodeObject({ _online: true, _id: node.id });
log.DEBUG("Node update object: ", onlineNode);
updateNodeInfo(onlineNode, (sqlResponse) => {
if (!sqlResponse) this.log.ERROR("No response from SQL object");
log.DEBUG("Updated node: ", sqlResponse);
return true
})
}
else {
log.DEBUG("No response from node, assuming it's offline");
const offlineNode = new nodeObject({ _online: false, _id: node.id });
log.DEBUG("Offline node update object: ", offlineNode);
updateNodeInfo(offlineNode, (sqlResponse) => {
if (!sqlResponse) this.log.ERROR("No response from SQL object");
log.DEBUG("Updated offline node: ", sqlResponse);
return false
})
}
})
}
exports.checkInWithNode = checkInWithNode;
/**
* Check in with all online nodes and mark any nodes that are actually offline
*/
async function checkInWithOnlineNodes() {
getOnlineNodes((nodes) => {
log.DEBUG("Online Nodes: ", nodes);
for (const node of nodes) {
checkInWithNode(node);
}
return;
});
}
exports.checkInWithOnlineNodes = checkInWithOnlineNodes;
/**
*
* @param {*} req Default express req from router
* @param {*} res Defualt express res from router
* @param {*} req
* @param {*} res
*/
exports.listAllNodes = async (req, res) => {
getAllNodes((allNodes) => {
@@ -71,13 +23,9 @@ exports.listAllNodes = async (req, res) => {
});
}
/**
* Add a new node to the storage
* @param {*} req Default express req from router
* @param {*} res Defualt express res from router
*/
// Add a new node to the storage
exports.newNode = async (req, res) => {
if (!req.body.name) return res.status(400).json("No name specified for new node");
if (!req.body.name) return res.send(400)
try {
// Try to add the new user with defaults if missing options
@@ -87,12 +35,12 @@ exports.newNode = async (req, res) => {
_port: req.body.port ?? null,
_location: req.body.location ?? null,
_nearbySystems: req.body.nearbySystems ?? null,
_online: (req.body.online == "true" || req.body.online == "True") ? true : false
_online: req.body.online ?? 0
});
addNewNode(newNode, (newNodeObject) => {
addNewNode(newNode, (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": newNodeObject.id });
res.status(202).json({"nodeId": queryResults.insertId});
})
}
catch (err) {
@@ -105,226 +53,54 @@ exports.newNode = async (req, res) => {
}
}
/** Get the known info for the node specified
*
* @param {*} req Default express req from router
* @param {*} res Defualt express res from router
*/
// Get the known info for the node specified
exports.getNodeInfo = async (req, res) => {
if (!req.params.id) return res.status(400).json("No id specified");
getNodeInfoFromId(req.params.id, (nodeInfo) => {
if (!req.query.id) return res.status(400).json("No id specified");
getNodeInfoFromId(req.query.id, (nodeInfo) => {
res.status(200).json(nodeInfo);
})
}
/** Adds a specific system/preset on a given node
*
* @param {*} req Default express req from router
* @param {*} res Defualt express res from router
* @param {*} req.params.nodeId The Node ID to add the preset/system to
* @param {*} req.body.systemName The name of the system to add
* @param {*} req.body.mode The radio mode of the preset
* @param {*} req.body.frequencies The frequencies of the preset
* @param {*} req.body.trunkFile The trunk file to use for digital stations
*/
exports.addNodeSystem = async (req, res) => {
if (!req.params.nodeId) return res.status(400).json("No id specified");
if (!req.body.systemName) return res.status(400).json("No system specified");
log.DEBUG("Adding system for node: ", req.params.nodeId, req.body);
getNodeInfoFromId(req.params.nodeId, (node) => {
const reqOptions = new requestOptions("/client/addPreset", "POST", node.ip, node.port);
const reqBody = {
'systemName': req.body.systemName,
'mode': req.body.mode,
'frequencies': req.body.frequencies,
}
if(digitalModes.includes(req.body.mode)) reqBody['trunkFile'] = req.body.trunkFile ?? 'none'
log.DEBUG("Request body for adding node system: ", reqBody, reqOptions);
sendHttpRequest(reqOptions, JSON.stringify(reqBody), async (responseObj) => {
if(responseObj){
// Good
log.DEBUG("Response from adding node system: ", reqBody, responseObj);
return res.sendStatus(200)
} else {
// Bad
log.DEBUG("No Response from adding Node system");
return res.status(400).json("No Response from adding Node, could be offline");
}
})
})
}
/** Updates a specific system/preset on a given node
*
* @param {*} req Default express req from router
* @param {*} res Defualt express res from router
* @param {*} req.params.nodeId The Node ID to update the preset/system on
* @param {*} req.body.systemName The name of the system to update
* @param {*} req.body.mode The radio mode of the preset to
* @param {*} req.body.frequencies The frequencies of the preset
* @param {*} req.body.trunkFile The trunk file to use for digital stations
*/
exports.updateNodeSystem = async (req, res) => {
if (!req.params.nodeId) return res.status(400).json("No id specified");
if (!req.body.systemName) return res.status(400).json("No system specified");
log.DEBUG("Updating system for node: ", req.params.nodeId, req.body);
getNodeInfoFromId(req.params.nodeId, (node) => {
const reqOptions = new requestOptions("/client/updatePreset", "POST", node.ip, node.port);
const reqBody = {
'systemName': req.body.systemName,
'mode': req.body.mode,
'frequencies': req.body.frequencies,
}
if(digitalModes.includes(req.body.mode)) reqBody['trunkFile'] = req.body.trunkFile ?? 'none'
log.DEBUG("Request body for updating node: ", reqBody, reqOptions);
sendHttpRequest(reqOptions, JSON.stringify(reqBody), async (responseObj) => {
if(responseObj){
// Good
log.DEBUG("Response from updating node system: ", reqBody, responseObj);
return res.sendStatus(200)
} else {
// Bad
log.DEBUG("No Response from updating Node system");
return res.status(400).json("No Response from updating Node, could be offline");
}
})
})
}
/** Deletes a specific system/preset from a given node
*
* @param {*} req Default express req from router
* @param {*} res Defualt express res from router
* @param {*} req.params.nodeId The Node ID to update the preset/system on
* @param {*} req.body.systemName The name of the system to update
*/
exports.removeNodeSystem = async (req, res) => {
if (!req.params.nodeId) return res.status(400).json("No id specified");
if (!req.body.systemName) return res.status(400).json("No system specified");
log.DEBUG("Updating system for node: ", req.params.nodeId, req.body);
getNodeInfoFromId(req.params.nodeId, (node) => {
const reqOptions = new requestOptions("/client/removePreset", "POST", node.ip, node.port);
const reqBody = {
'systemName': req.body.systemName
}
log.DEBUG("Request body for deleting preset: ", reqBody, reqOptions);
sendHttpRequest(reqOptions, JSON.stringify(reqBody), async (responseObj) => {
if(responseObj){
// Good
log.DEBUG("Response from deleting preset: ", reqBody, responseObj);
return res.sendStatus(200)
} else {
// Bad
log.DEBUG("No Response from deleting preset");
return res.status(400).json("No Response from deleting preset, could be offline");
}
})
})
}
/** Updates the information received from the client based on ID
*
* @param {*} req Default express req from router
* @param {*} res Defualt express res from router
*/
exports.updateExistingNode = async = (req, res) => {
if (!req.params.nodeId) return res.status(400).json("No id specified");
getNodeInfoFromId(req.params.nodeId, (nodeInfo) => {
let checkInObject = {};
// 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");
getNodeInfoFromId(req.body.id, (nodeInfo) => {
let checkInObject = new 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
log.DEBUG("REQ Body: ", req.body);
nodeInfo.online = nodeInfo.online !== 0;
var isObjectUpdated = false;
if (req.body.name && req.body.name != nodeInfo.name) {
checkInObject._name = req.body.name;
isObjectUpdated = true;
}
if (req.body.ip && req.body.ip != nodeInfo.ip) {
checkInObject._ip = req.body.ip;
isObjectUpdated = true;
}
if (req.body.port && req.body.port != nodeInfo.port) {
checkInObject._port = req.body.port;
isObjectUpdated = true;
}
if (req.body.location && req.body.location != nodeInfo.location) {
checkInObject._location = req.body.location;
isObjectUpdated = true;
}
if (req.body.nearbySystems && JSON.stringify(req.body.nearbySystems) !== JSON.stringify(nodeInfo.nearbySystems)) {
checkInObject._nearbySystems = req.body.nearbySystems;
isObjectUpdated = true;
}
if (req.body.online != nodeInfo.online || req.body.online && (req.body.online === "true") != nodeInfo.online) {
checkInObject._online = req.body.online;
isObjectUpdated = true;
}
if (req.body.name && req.body.name !== nodeInfo.name) checkInObject.name = req.body.name
if (req.body.ip && req.body.ip !== nodeInfo.ip) checkInObject.ip = req.body.ip
if (req.body.port && req.body.port !== nodeInfo.port) checkInObject.port = req.body.port
if (req.body.location && req.body.location !== nodeInfo.location) checkInObject.location = req.body.location
if (req.body.nearbySystems && JSON.stringify(req.body.nearbySystems) !== JSON.stringify(nodeInfo.nearbySystems)) checkInObject.nearbySystems = req.body.nearbySystems
if (req.body.online && req.body.online !== nodeInfo.online) checkInObject.online = req.body.online
// If no changes are made tell the client
if (!isObjectUpdated) return res.status(200).json("No keys updated");
if (Object.keys(checkInObject).length === 0) return res.status(200).json("No keys updated");
log.INFO("Updating the following keys for ID: ", req.params.nodeId, checkInObject);
log.INFO("Updating the following keys for ID: ", req.body.id, checkInObject);
// Adding the ID key to the body so that the client can double-check their ID
checkInObject.id = req.body.id;
updateNodeInfo(checkInObject, () => {
return res.status(202).json({"updatedKeys": checkInObject});
})
})
checkInObject._id = req.params.nodeId;
checkInObject = new nodeObject(checkInObject);
if (!nodeInfo) {
log.WARN("No existing node found with this ID, adding node: ", checkInObject);
addNewNode(checkInObject, async (newNode) => {
await checkInWithNode(newNode);
return res.status(201).json({ "updatedKeys": newNode });
});
}
else {
updateNodeInfo(checkInObject, async () => {
await checkInWithNode(nodeInfo);
return res.status(202).json({ "updatedKeys": checkInObject });
});
}
});
}
/** Allows the bots to check in and get any updates from the server
*
* @param {*} req Default express req from router
* @param {*} res Defualt express res from router
*/
exports.nodeCheckIn = async (req, res) => {
if (!req.params.nodeId) return res.status(400).json("No id specified");
getNodeInfoFromId(req.params.nodeId, (nodeInfo) => {
if (!nodeInfo) return this.newNode(req, res);
if (!nodeInfo.online) {
nodeInfo.online = true;
updateNodeInfo(nodeInfo, () => {
return res.status(200).json(nodeInfo);
})
}
else return res.status(200).json(nodeInfo);
});
}
/**
* Requests a specific node to check in with the server, if it's online
* Request the node to join the specified server/channel and listen to the specified resource
*
* @param {*} req Default express req from router
* @param {*} res Defualt express res from router
* @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.requestNodeCheckIn = async (req, res) => {
if (!req.params.nodeId) return res.status(400).json("No Node ID supplied in request");
const node = await getNodeInfoFromId(req.params.nodeId);
if (!node) return res.status(400).json("No Node with the ID given");
await checkInWithNode(node);
if (res) res.sendStatus(200);
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)
}
/**
@@ -332,27 +108,48 @@ exports.requestNodeCheckIn = async (req, res) => {
*/
exports.nodeMonitorService = class nodeMonitorService {
constructor() {
this.log = new DebugBuilder("server", "nodeMonitorService");
}
/**
* Start the node monitor service in the background
*/
async start() {
// Wait for the a portion of the refresh period before checking in with the nodes, so the rest of the bot can start
await new Promise(resolve => setTimeout(resolve, refreshInterval / 10));
async start(){
// Wait for the a portion of the refresh period before checking in with the nodes, so the rest of the bot can start
await new Promise(resolve => setTimeout(resolve, refreshInterval/10));
log.INFO("Starting Node Monitor Service");
// Check in before starting the infinite loop
await checkInWithOnlineNodes();
while (true) {
await this.checkInWithOnlineNodes();
while(true){
// Wait for the refresh interval, then wait for the posts to return, then wait a quarter of the refresh interval to make sure everything is cleared up
await new Promise(resolve => setTimeout(resolve, refreshInterval));
await checkInWithOnlineNodes();
await this.checkInWithOnlineNodes();
await new Promise(resolve => setTimeout(resolve, refreshInterval / 4));
continue;
}
}
}
async checkInWithOnlineNodes(){
getOnlineNodes((nodes) => {
log.DEBUG("Online Nodes: ", nodes);
for (const node of nodes) {
const reqOptions = new requestOptions("/client/requestCheckIn", "GET", node.ip, node.port)
const request = sendHttpRequest(reqOptions, "", (responseObj) => {
if (responseObj) {
log.DEBUG("Response from: ", node.name, responseObj);
}
else {
log.DEBUG("No response from node, assuming it's offline");
const offlineNode = new nodeObject({ _online: 0, _id: node.id });
log.DEBUG("Offline node update object: ", offlineNode);
updateNodeInfo(offlineNode, (sqlResponse) => {
if (!sqlResponse) log.ERROR("No response from SQL object");
log.DEBUG("Updated node: ", sqlResponse);
})
}
})
}
return;
});
}
}

View File

@@ -9,7 +9,7 @@ module.exports = {
name: Events.InteractionCreate,
async execute(interaction) {
const command = interaction.client.commands.get(interaction.commandName);
log.VERBOSE("Interaction created for command: ", command);
log.VERBOSE("Interaction for command: ", command);
// Execute autocomplete if the user is checking autocomplete
if (interaction.isAutocomplete()) {

View File

@@ -148,7 +148,7 @@ client.on('ready', () => {
runHTTPServer();
log.DEBUG("Starting Node Monitoring Service");
runNodeMonitorService();
//runNodeMonitorService();
log.DEBUG("Starting RSS watcher");
runRssService();

View File

@@ -5,7 +5,7 @@ const { FeedStorage, PostStorage } = require("./libStorage");
const libUtils = require("./libUtils");
const { DebugBuilder } = require("./utilities/debugBuilder");
const log = new DebugBuilder("server", "libCore");
const mysql = require("mysql2");
const mysql = require("mysql");
const UserAgent = require("user-agents");
process.env.USER_AGENT_STRING = new UserAgent({ platform: 'Win32' }).toString();

View File

@@ -8,7 +8,7 @@ const { RSSSourceRecord, RSSPostRecord } = require("./utilities/recordHelper");
// Storage Specific Modules
// MySQL
const mysql = require("mysql2");
const mysql = require("mysql");
const rssFeedsTable = process.env.DB_RSS_FEEDS_TABLE;
const rssPostsTable = process.env.DB_RSS_POSTS_TABLE;
@@ -480,11 +480,11 @@ exports.PostStorage = class PostStorage extends Storage {
}
savePost(_postObject, callback){
const tempCreationDate = returnMysqlTime();
const tempCreationDate = returnMysqlTime();
log.DEBUG("Saving Post Object:", _postObject);
if (!_postObject?.postId || !_postObject?.link) {
return callback(new Error("Post object malformed, check the object before saving it", _postObject), undefined)
}
log.DEBUG("Saving Post:", _postObject);
if (_postObject.link.length > 250) _postObject.link = _postObject.link.substring(0, 250);

View File

@@ -0,0 +1,40 @@
import scrapy
from scrapy.crawler import CrawlerProcess
class RecordingSpider(scrapy.Spider):
name = "recording-scraper"
start_urls = [
'https://radio.vpn.cusano.net/sdr/transmissions',
]
def parse(self, response):
print("ASDASDD")
print(response)
for row in response.css("tr"):
if row.css('td.py-1'):
links = row.css('a')
rows = row.css('td.py-1')
print(row)
yield {
'device': rows[0],
'date': rows[1],
'duration': rows[2],
"frequency": rows[3],
"link": links[0].attrib["href"],
}
next_page_url = response.css("a.page-link > a::attr(href)").extract_first()
if next_page_url is not None:
yield scrapy.Request(response.urljoin(next_page_url))
process = CrawlerProcess(
settings={
"FEEDS": {
"items.json": {"format": "json"},
},
}
)
process.crawl(RecordingSpider)
process.start() # the script will block here until the crawling is finished

View File

@@ -0,0 +1,3 @@
scrapy
fake-useragent
beautifulsoup4

150
Server/package-lock.json generated
View File

@@ -26,7 +26,7 @@
"jsdoc": "^4.0.2",
"jsonfile": "^6.1.0",
"morgan": "^1.10.0",
"mysql2": "^3.3.5",
"mysql": "^2.18.1",
"node-html-markdown": "^1.3.0",
"node-html-parser": "^6.1.5",
"openai": "^3.2.1",
@@ -489,6 +489,14 @@
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/bignumber.js": {
"version": "9.0.0",
"resolved": "https://registry.npmjs.org/bignumber.js/-/bignumber.js-9.0.0.tgz",
"integrity": "sha512-t/OYhhJ2SD+YGBQcjY8GzzDHEk9f3nerxjtfa6tlMXfe7frs/WozhvCNoGvpM0P3bNf3Gq5ZRMlGr5f3r4/N8A==",
"engines": {
"node": "*"
}
},
"node_modules/bluebird": {
"version": "3.7.2",
"resolved": "https://registry.npmjs.org/bluebird/-/bluebird-3.7.2.tgz",
@@ -728,6 +736,11 @@
"resolved": "https://registry.npmjs.org/cookie-signature/-/cookie-signature-1.0.6.tgz",
"integrity": "sha512-QADzlaHc8icV8I7vbaJXJwod9HWYp8uCqf1xa4OfNu1T7JVxQIrUgOWtHdNDtPiywmFbiS12VjotIXLrKM3orQ=="
},
"node_modules/core-util-is": {
"version": "1.0.3",
"resolved": "https://registry.npmjs.org/core-util-is/-/core-util-is-1.0.3.tgz",
"integrity": "sha512-ZQBvi1DcpJ4GDqanjucZ2Hj3wEO5pZDS89BWbkcrvdxksJorwUDDZamX9ldFkp9aw2lmBDLgkObEA4DWNJ9FYQ=="
},
"node_modules/css-select": {
"version": "5.1.0",
"resolved": "https://registry.npmjs.org/css-select/-/css-select-5.1.0.tgz",
@@ -792,14 +805,6 @@
"node": ">=0.4.0"
}
},
"node_modules/denque": {
"version": "2.1.0",
"resolved": "https://registry.npmjs.org/denque/-/denque-2.1.0.tgz",
"integrity": "sha512-HVQE3AAb/pxF8fQAoiqpvg9i3evqug3hoiwakOyZAwJm+6vZehbkYXZ0l4JxS+I3QxM97v5aaRNhj8v5oBhekw==",
"engines": {
"node": ">=0.10"
}
},
"node_modules/depd": {
"version": "2.0.0",
"resolved": "https://registry.npmjs.org/depd/-/depd-2.0.0.tgz",
@@ -1279,14 +1284,6 @@
"resolved": "https://registry.npmjs.org/function-bind/-/function-bind-1.1.1.tgz",
"integrity": "sha512-yIovAzMX49sF8Yl58fSCWJ5svSLuaibPxXQJFLmBObTuCr0Mf1KiPopGM9NiFjiYBCbfaa2Fh6breQ6ANVTI0A=="
},
"node_modules/generate-function": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/generate-function/-/generate-function-2.3.1.tgz",
"integrity": "sha512-eeB5GfMNeevm/GRYq20ShmsaGcmI81kIX2K9XQx5miC8KdHaC6Jm0qQ8ZNeGOi7wYB8OsdxKs+Y2oVuTFuVwKQ==",
"dependencies": {
"is-property": "^1.0.2"
}
},
"node_modules/get-intrinsic": {
"version": "1.2.1",
"resolved": "https://registry.npmjs.org/get-intrinsic/-/get-intrinsic-1.2.1.tgz",
@@ -1513,10 +1510,10 @@
"url": "https://github.com/sponsors/ljharb"
}
},
"node_modules/is-property": {
"version": "1.0.2",
"resolved": "https://registry.npmjs.org/is-property/-/is-property-1.0.2.tgz",
"integrity": "sha512-Ks/IoX00TtClbGQr4TWXemAnktAQvYB7HzcCxDGqEZU6oCmb2INHuOoKxbtR+HFkmYWBKv/dOZtGRiAjDhj92g=="
"node_modules/isarray": {
"version": "1.0.0",
"resolved": "https://registry.npmjs.org/isarray/-/isarray-1.0.0.tgz",
"integrity": "sha512-VLghIWNM6ELQzo7zwmcg0NmTVyWKYjvIeM83yjp0wRDTmUnrM678fQbcKBo6n2CJEF0szoG//ytg+TKla89ALQ=="
},
"node_modules/jake": {
"version": "10.8.7",
@@ -1665,11 +1662,6 @@
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw=="
},
"node_modules/long": {
"version": "5.2.3",
"resolved": "https://registry.npmjs.org/long/-/long-5.2.3.tgz",
"integrity": "sha512-lcHwpNoggQTObv5apGNCTdJrO69eHOZMi4BNC+rTLER8iHAqGrUVeLh/irVIM7zTw2bOXA8T6uNPeujwOLg/2Q=="
},
"node_modules/lru-cache": {
"version": "6.0.0",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-6.0.0.tgz",
@@ -1854,69 +1846,24 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz",
"integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w=="
},
"node_modules/mysql2": {
"version": "3.3.5",
"resolved": "https://registry.npmjs.org/mysql2/-/mysql2-3.3.5.tgz",
"integrity": "sha512-ZTQGAzxGeaX1PyeSiZFCgQ34uiXguaEpn3aTFN9Enm9JDnbwWo+4/CJnDdQZ3n0NaMeysi8vwtW/jNUb9VqVDw==",
"node_modules/mysql": {
"version": "2.18.1",
"resolved": "https://registry.npmjs.org/mysql/-/mysql-2.18.1.tgz",
"integrity": "sha512-Bca+gk2YWmqp2Uf6k5NFEurwY/0td0cpebAucFpY/3jhrwrVGuxU2uQFCHjU19SJfje0yQvi+rVWdq78hR5lig==",
"dependencies": {
"denque": "^2.1.0",
"generate-function": "^2.3.1",
"iconv-lite": "^0.6.3",
"long": "^5.2.1",
"lru-cache": "^8.0.0",
"named-placeholders": "^1.1.3",
"seq-queue": "^0.0.5",
"sqlstring": "^2.3.2"
"bignumber.js": "9.0.0",
"readable-stream": "2.3.7",
"safe-buffer": "5.1.2",
"sqlstring": "2.3.1"
},
"engines": {
"node": ">= 8.0"
}
},
"node_modules/mysql2/node_modules/iconv-lite": {
"version": "0.6.3",
"resolved": "https://registry.npmjs.org/iconv-lite/-/iconv-lite-0.6.3.tgz",
"integrity": "sha512-4fCk79wshMdzMp2rH06qWrJE4iolqLhCUH+OiuIgU++RB0+94NlDL81atO7GX55uUKueo0txHNtvEyI6D7WdMw==",
"dependencies": {
"safer-buffer": ">= 2.1.2 < 3.0.0"
},
"engines": {
"node": ">=0.10.0"
}
},
"node_modules/mysql2/node_modules/lru-cache": {
"version": "8.0.5",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-8.0.5.tgz",
"integrity": "sha512-MhWWlVnuab1RG5/zMRRcVGXZLCXrZTgfwMikgzCegsPnG62yDQo5JnqKkrK4jO5iKqDAZGItAqN5CtKBCBWRUA==",
"engines": {
"node": ">=16.14"
}
},
"node_modules/mysql2/node_modules/sqlstring": {
"version": "2.3.3",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.3.tgz",
"integrity": "sha512-qC9iz2FlN7DQl3+wjwn3802RTyjCx7sDvfQEXchwa6CWOx07/WVfh91gBmQ9fahw8snwGEWU3xGzOt4tFyHLxg==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/named-placeholders": {
"version": "1.1.3",
"resolved": "https://registry.npmjs.org/named-placeholders/-/named-placeholders-1.1.3.tgz",
"integrity": "sha512-eLoBxg6wE/rZkJPhU/xRX1WTpkFEwDJEN96oxFrTsqBdbT5ec295Q+CoHrL9IT0DipqKhmGcaZmwOt8OON5x1w==",
"dependencies": {
"lru-cache": "^7.14.1"
},
"engines": {
"node": ">=12.0.0"
}
},
"node_modules/named-placeholders/node_modules/lru-cache": {
"version": "7.18.3",
"resolved": "https://registry.npmjs.org/lru-cache/-/lru-cache-7.18.3.tgz",
"integrity": "sha512-jumlc0BIUrS3qJGgIkWZsyfAM7NCWiBcCDhnd+3NNM5KbBmLTgHVfWBcg6W+rLUsIpzpERPsvwUP7CckAQSOoA==",
"engines": {
"node": ">=12"
}
"node_modules/mysql/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/negotiator": {
"version": "0.6.3",
@@ -2262,6 +2209,11 @@
"node": ">=0.10.0"
}
},
"node_modules/process-nextick-args": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/process-nextick-args/-/process-nextick-args-2.0.1.tgz",
"integrity": "sha512-3ouUOpQhtgrbOa17J7+uxOTpITYWaGP7/AhoR3+A+/1e9skrzelGi/dXzEYyvbxubEF6Wn2ypscTKiKJFFn1ag=="
},
"node_modules/proxy-addr": {
"version": "2.0.7",
"resolved": "https://registry.npmjs.org/proxy-addr/-/proxy-addr-2.0.7.tgz",
@@ -2367,6 +2319,25 @@
"url": "https://github.com/sponsors/sindresorhus"
}
},
"node_modules/readable-stream": {
"version": "2.3.7",
"resolved": "https://registry.npmjs.org/readable-stream/-/readable-stream-2.3.7.tgz",
"integrity": "sha512-Ebho8K4jIbHAxnuxi7o42OrZgF/ZTNcsZj6nRKyUmkhLFq8CHItp/fy6hQZuZmP/n3yZ9VBUbp4zz/mX8hmYPw==",
"dependencies": {
"core-util-is": "~1.0.0",
"inherits": "~2.0.3",
"isarray": "~1.0.0",
"process-nextick-args": "~2.0.0",
"safe-buffer": "~5.1.1",
"string_decoder": "~1.1.1",
"util-deprecate": "~1.0.1"
}
},
"node_modules/readable-stream/node_modules/safe-buffer": {
"version": "5.1.2",
"resolved": "https://registry.npmjs.org/safe-buffer/-/safe-buffer-5.1.2.tgz",
"integrity": "sha512-Gd2UZBJDkXlY7GbJxfsE8/nvKkUEU1G38c1siN6QP6a9PT9MmHB8GnpscSmMJSoF8LOIrt8ud/wPtojys4G6+g=="
},
"node_modules/readable-web-to-node-stream": {
"version": "3.0.2",
"resolved": "https://registry.npmjs.org/readable-web-to-node-stream/-/readable-web-to-node-stream-3.0.2.tgz",
@@ -2504,11 +2475,6 @@
"resolved": "https://registry.npmjs.org/ms/-/ms-2.1.3.tgz",
"integrity": "sha512-6FlzubTLZG3J2a/NVCAleEhjzq5oxgHyaCU9yYXvcLsvoVaHJq/s5xXI6/XXP6tz7R9xAOtHnSO/tXtF3WRTlA=="
},
"node_modules/seq-queue": {
"version": "0.0.5",
"resolved": "https://registry.npmjs.org/seq-queue/-/seq-queue-0.0.5.tgz",
"integrity": "sha512-hr3Wtp/GZIc/6DAGPDcV4/9WoZhjrkXsi5B/07QgX8tsdc6ilr7BFM6PM6rbdAX1kFSDYeZGLipIZZKyQP0O5Q=="
},
"node_modules/serve-static": {
"version": "1.15.0",
"resolved": "https://registry.npmjs.org/serve-static/-/serve-static-1.15.0.tgz",
@@ -2580,6 +2546,14 @@
"node": "*"
}
},
"node_modules/sqlstring": {
"version": "2.3.1",
"resolved": "https://registry.npmjs.org/sqlstring/-/sqlstring-2.3.1.tgz",
"integrity": "sha512-ooAzh/7dxIG5+uDik1z/Rd1vli0+38izZhGzSa34FwR7IbelPWCCKSNIl8jlL/F7ERvy8CB2jNeM1E9i9mXMAQ==",
"engines": {
"node": ">= 0.6"
}
},
"node_modules/statuses": {
"version": "2.0.1",
"resolved": "https://registry.npmjs.org/statuses/-/statuses-2.0.1.tgz",

View File

@@ -21,7 +21,7 @@
"jsdoc": "^4.0.2",
"jsonfile": "^6.1.0",
"morgan": "^1.10.0",
"mysql2": "^3.3.5",
"mysql": "^2.18.1",
"node-html-markdown": "^1.3.0",
"node-html-parser": "^6.1.5",
"openai": "^3.2.1",

View File

@@ -1,184 +0,0 @@
.node-card {
position: relative;
display: flex;
flex-direction: column;
min-width: 0;
word-wrap: break-word;
background-color: #fff;
background-clip: border-box;
border: 1px solid #eff0f2;
border-radius: 1rem;
margin-bottom: 24px;
box-shadow: 0 2px 3px #e4e8f0;
}
.avatar-md {
height: 4rem;
width: 4rem;
}
.rounded-circle {
border-radius: 50% !important;
}
.img-thumbnail {
padding: 0.25rem;
background-color: #f1f3f7;
border: 1px solid #eff0f2;
border-radius: 0.75rem;
}
.avatar-title {
align-items: center;
background-color: #3b76e1;
color: #fff;
display: flex;
font-weight: 500;
height: 100%;
justify-content: center;
width: 100%;
}
.bg-soft-primary {
background-color: rgba(59, 118, 225, .25) !important;
}
a {
text-decoration: none !important;
}
.badge-soft-danger {
color: #f56e6e !important;
background-color: rgba(245, 110, 110, .1);
}
.badge-soft-success {
color: #63ad6f !important;
background-color: rgba(99, 173, 111, .1);
}
.mb-0 {
margin-bottom: 0 !important;
}
.badge {
display: inline-block;
padding: 0.25em 0.6em;
font-size: 75%;
font-weight: 500;
line-height: 1;
color: #fff;
text-align: center;
white-space: nowrap;
vertical-align: baseline;
border-radius: 0.75rem;
}
/* Info Card Section */
.info-card {
background-color: #fff;
border-radius: 10px;
border: none;
position: relative;
margin-bottom: 30px;
box-shadow: 0 0.46875rem 2.1875rem rgba(90, 97, 105, 0.1), 0 0.9375rem 1.40625rem rgba(90, 97, 105, 0.1), 0 0.25rem 0.53125rem rgba(90, 97, 105, 0.12), 0 0.125rem 0.1875rem rgba(90, 97, 105, 0.1);
min-height: 85%;
}
.info-card .card-statistic .card-icon-large .bi {
font-size: 110px;
}
.info-card .card-statistic .card-icon {
text-align: center;
line-height: 50px;
margin-left: 15px;
color: #000;
position: absolute;
right: -5px;
top: 20px;
opacity: 0.1;
}
/* Info Card Background Colors */
.l-bg-cherry {
background: linear-gradient(to right, #493240, #f09) !important;
color: #fff;
}
.l-bg-blue-dark {
background: linear-gradient(to right, #373b44, #4286f4) !important;
color: #fff;
}
.l-bg-green-dark {
background: linear-gradient(to right, #0a504a, #38ef7d) !important;
color: #fff;
}
.l-bg-orange-dark {
background: linear-gradient(to right, #a86008, #ffba56) !important;
color: #fff;
}
.l-bg-cyan {
background: linear-gradient(135deg, #289cf5, #84c0ec) !important;
color: #fff;
}
.l-bg-green {
background: linear-gradient(135deg, #23bdb8 0%, #43e794 100%) !important;
color: #fff;
}
.l-bg-orange {
background: linear-gradient(to right, #f9900e, #ffba56) !important;
color: #fff;
}
/* Global Section */
.sidebar-container {
min-height: 95vh;
}
.sidebar {
position: fixed;
top: 5vh;
bottom: 0;
left: 0;
}
/* User table section */
.label {
border-radius: 3px;
font-size: 1.1em;
font-weight: 600;
}
.user-list tbody td .user-subhead {
font-size: 1em;
font-style: italic;
}
.table thead tr th {
text-transform: uppercase;
font-size: 0.875em;
}
.table thead tr th {
border-bottom: 2px solid #e7ebee;
}
.table tbody tr td:first-child {
font-size: 1.125em;
font-weight: 300;
}
.table tbody tr td {
font-size: 0.875em;
vertical-align: middle;
border-top: 1px solid #e7ebee;
padding: 12px 8px;
}

View File

@@ -1,407 +0,0 @@
$(document).ready(async () => {
console.log("Loading stored notifications...");
await loadStoredToasts();
console.log("Showing stored notifications...");
await showStoredToasts();
});
/**
* Gets all toasts stored in local storage
*
* @returns {Object} Object of toasts in storage
*/
function getStoredToasts() {
if (localStorage.getItem("toasts")) {
const storedToasts = JSON.parse(localStorage.getItem("toasts"));
console.log("LOADED STORED TOASTS: ", storedToasts);
navbarUpdateNotificationBellCount(storedToasts);
return storedToasts;
}
else return false
}
/**
* Adds a toast to storage, will not allow duplicates
*
* @param {Date} time The date object from when the toast was created
* @param {*} message The message of the toast
*/
function addToastToStorage(time, message) {
var toasts = [{ 'time': time, 'message': message }]
var storedToasts = getStoredToasts();
console.log("Adding new notification to storage: ", toasts);
if (storedToasts) {
toasts = toasts.concat(storedToasts);
console.log("Combined new and stored notifications: ", toasts);
toasts = toasts.filter((value, index, self) =>
index === self.findIndex((t) => (
t.time === value.time && t.message === value.message
))
)
}
console.log("Deduped stored notifications: ", toasts);
localStorage.setItem("toasts", JSON.stringify(toasts));
navbarUpdateNotificationBellCount(toasts);
}
/**
* Removes a toast from the local storage
*
* @param {Date} time The date object from when the toast was created
* @param {*} message The message of the toast
*/
function removeToastFromStorage(time, message) {
const toastToRemove = { 'time': time, 'message': message }
console.log("Toast to remove: ", toastToRemove);
var toasts = getStoredToasts();
console.log("Stored toasts: ", toasts);
if (toasts.indexOf(toastToRemove)) toasts.splice(toasts.indexOf(toastToRemove) - 1, 1)
console.log("Toasts with selected toast removed: ", toasts);
localStorage.setItem("toasts", JSON.stringify(toasts));
navbarUpdateNotificationBellCount(toasts);
}
/**
* Shows all stored toasts
*/
function showStoredToasts() {
const storedToasts = getStoredToasts();
if (!storedToasts) return
console.log("Loaded stored notifications to show: ", storedToasts);
for (const toast of storedToasts) {
const toastId = `${toast.time}-toast`;
console.log("Showing stored toast: ", toast, toastId);
const toastElement = bootstrap.Toast.getOrCreateInstance(document.getElementById(toastId));
toastElement.show();
}
}
/**
* Loads all toasts stored in the local storage into the DOM of the webpage
*/
function loadStoredToasts() {
const storedToasts = getStoredToasts();
if (!storedToasts) return
console.log("Loaded stored notifications: ", storedToasts);
for (const toast of storedToasts) {
createToast(toast.message, { time: toast.time })
}
}
/**
* Will update the count of notifications on the bell icon in the navbar
*
* @param {Array} storedToasts An array of stored toasts to be counted and updated in the navbar
*/
function navbarUpdateNotificationBellCount(storedToasts) {
const notificationBellIcon = document.getElementById("navbar-notification-bell");
var notificationBellCount = document.getElementById("notification-bell-icon-count");
if (!notificationBellCount) {
notificationBellCount = document.createElement('span');
notificationBellCount.id = "notification-bell-icon-count";
notificationBellCount.classList.add('badge');
notificationBellCount.classList.add('text-bg-secondary');
notificationBellCount.appendChild(document.createTextNode(storedToasts.length));
}
else notificationBellCount.innerHTML = storedToasts.length;
notificationBellIcon.appendChild(notificationBellCount);
}
/**
* Remove a frequency input from the DOM
*
* @param {string} system The system name to add the frequency to
* @param {string} inputId [OPTIONAL] The ID of input, this can be anything unique to this input. If this is not provided the number of frequencies will be used as the ID
*/
function addFrequencyInput(system, inputId = null) {
if (!inputId) inputId = $(`[id^="${system}_systemFreqRow_"]`).length;
// Create new input
var icon = document.createElement('i');
icon.classList.add('bi');
icon.classList.add('bi-x-circle');
icon.classList.add('text-black');
var remove = document.createElement('a');
remove.classList.add('align-middle');
remove.classList.add('float-left');
remove.href = '#'
remove.onclick = () => { removeFrequencyInput(`${system}_systemFreqRow_${inputId}`) }
remove.appendChild(icon);
var childColRemoveIcon = document.createElement('div');
childColRemoveIcon.classList.add('col-2');
childColRemoveIcon.appendChild(remove);
var input = document.createElement('input');
input.classList.add('form-control');
input.id = `${system}_systemFreq_${inputId}`;
input.type = 'text';
var childColInput = document.createElement('div');
childColInput.classList.add('col-10');
childColInput.appendChild(input);
var childRow = document.createElement('div');
childRow.classList.add("row");
childRow.classList.add("px-1");
childRow.appendChild(childColInput);
childRow.appendChild(childColRemoveIcon);
var colParent = document.createElement('div');
colParent.classList.add("col-md-6");
colParent.classList.add("mb-1");
colParent.id = `${system}_systemFreqRow_${inputId}`
colParent.appendChild(childRow);
document.getElementById(`frequencyRow_${system.replaceAll(" ", "_")}`).appendChild(colParent);
}
/**
* Add a toast element to the DOM
*
* @param {*} notificationMessage The message of the notification
* @param {Date} param1.time The date object for when the toast was created, blank if creating new
* @param {boolean} param1.showNow Show the toast now or just store it
* @returns
*/
function createToast(notificationMessage, { time = undefined, showNow = false } = {}) {
if (!time) time = new Date(Date.now());
else time = new Date(Date.parse(time));
const toastTitle = document.createElement('strong');
toastTitle.classList.add('me-auto');
toastTitle.appendChild(document.createTextNode("Server Notification"));
const toastTime = document.createElement('small');
toastTime.appendChild(document.createTextNode(time.toLocaleString()));
const toastClose = document.createElement('button');
toastClose.type = 'button';
toastClose.classList.add('btn-close');
toastClose.ariaLabel = 'Close';
toastClose.setAttribute('data-bs-dismiss', 'toast');
toastClose.onclick = () => { removeToastFromStorage(time.toISOString(), notificationMessage); };
const toastHeader = document.createElement('div');
toastHeader.classList.add('toast-header');
toastHeader.appendChild(toastTitle);
toastHeader.appendChild(toastTime);
toastHeader.appendChild(toastClose);
const toastMessage = document.createElement('p');
toastMessage.classList.add("px-2");
toastMessage.appendChild(document.createTextNode(notificationMessage));
const toastBody = document.createElement('div');
toastBody.classList.add('toast-body');
toastBody.appendChild(toastMessage);
const wrapperDiv = document.createElement('div');
wrapperDiv.classList.add('toast');
//wrapperDiv.classList.add('position-fixed');
wrapperDiv.id = `${time.toISOString()}-toast`;
wrapperDiv.role = 'alert';
wrapperDiv.ariaLive = 'assertive';
wrapperDiv.ariaAtomic = true;
wrapperDiv.setAttribute('data-bs-delay', "7500");
wrapperDiv.setAttribute('data-bs-animation', true);
wrapperDiv.appendChild(toastHeader);
wrapperDiv.appendChild(toastMessage);
document.getElementById("toastZone").appendChild(wrapperDiv);
addToastToStorage(time.toISOString(), notificationMessage);
if (showNow) {
const toastElement = bootstrap.Toast.getOrCreateInstance(document.getElementById(`${time.toISOString()}-toast`));
toastElement.show();
}
return;
}
function sendNodeHeartbeat(nodeId) {
const Http = new XMLHttpRequest();
const url = '/nodes/nodeCheckIn/' + nodeId;
Http.open("GET", url);
Http.send();
Http.onloadend = (e) => {
console.log(Http.responseText)
createToast(Http.responseText, { showNow: true });
}
}
function joinServer() {
const preset = document.getElementById("selectRadioPreset").value;
const nodeId = document.getElementById("nodeId").value;
const clientId = document.getElementById("inputDiscordClientId").value;
const channelId = document.getElementById("inputDiscordChannelId").value;
const reqBody = {
'preset': preset,
'nodeId': nodeId,
'clientId': clientId,
'channelId': channelId
};
console.log(reqBody);
const Http = new XMLHttpRequest();
const url = '/admin/join';
Http.open("POST", url);
Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody));
Http.onloadend = (e) => {
const responseObject = JSON.parse(Http.responseText)
console.log(Http.status);
console.log(responseObject);
createToast(`${responseObject.name} will join shortly`, { showNow: true });
location.reload();
}
}
function leaveServer() {
const nodeId = document.getElementById("nodeId").value;
const reqBody = {
'nodeId': nodeId
};
const Http = new XMLHttpRequest();
const url = '/admin/leave';
Http.open("POST", url);
Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody));
Http.onloadend = (e) => {
const responseObject = JSON.parse(Http.responseText)
console.log(Http.status);
console.log(responseObject);
createToast(`${responseObject} is leaving`, { showNow: true });
}
}
function saveNodeDetails() {
const nodeId = document.getElementById("nodeId").value;
const nodeName = document.getElementById("inputNodeName").value;
const nodeIp = document.getElementById("inputNodeIp").value;
const nodePort = document.getElementById("inputOrgName").value;
const nodeLocation = document.getElementById("inputNodeLocation").value;
const reqBody = {
'id': nodeId,
'name': nodeName,
'ip': nodeIp,
'port': nodePort,
'location': nodeLocation
}
console.log("Request Body: ", reqBody);
const Http = new XMLHttpRequest();
const url = '/nodes/' + nodeId;
Http.open("PUT", url);
Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody));
Http.onloadend = (e) => {
const responseObject = JSON.parse(Http.responseText);
console.log(Http.status);
console.log(responseObject);
createToast(`Node Updated!`);
location.reload();
}
}
function addNewSystem() {
const nodeId = document.getElementById("nodeId").value;
const systemName = document.getElementById(`New System_systemName`).value;
const systemMode = document.getElementById(`New System_systemMode`).value;
const inputSystemFreqs = $(`[id^="New System_systemFreq_"]`);
let systemFreqs = [];
for (const inputFreq of inputSystemFreqs) {
systemFreqs.push(inputFreq.value);
}
const reqBody = {
'systemName': systemName,
'mode': systemMode,
'frequencies': systemFreqs
}
console.log("Request Body: ", reqBody);
const Http = new XMLHttpRequest();
const url = '/nodes/' + nodeId + "/systems";
Http.open("POST", url);
Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody));
Http.onloadend = (e) => {
const responseObject = Http.responseText
console.log(Http.status);
console.log(responseObject);
createToast(`${systemName} Added!`);
location.reload();
}
}
function updateSystem(systemName) {
const nodeId = document.getElementById("nodeId").value;
const systemMode = document.getElementById(`${systemName}_systemMode`).value;
const inputSystemFreqs = $(`[id^="${systemName}_systemFreq_"]`);
let systemFreqs = [];
for (const inputFreq of inputSystemFreqs) {
systemFreqs.push(inputFreq.value);
}
const reqBody = {
'systemName': systemName,
'mode': systemMode,
'frequencies': systemFreqs
}
console.log("Request Body: ", reqBody);
const Http = new XMLHttpRequest();
const url = '/nodes/' + nodeId + "/systems";
Http.open("PUT", url);
Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody));
Http.onloadend = (e) => {
const responseObject = Http.responseText;
console.log(Http.status);
console.log(responseObject);
createToast(`${systemName} Updated!`);
location.reload();
}
}
function removeSystem(systemName) {
const nodeId = document.getElementById("nodeId").value;
const reqBody = {
'systemName': systemName,
}
console.log("Request Body: ", reqBody);
const Http = new XMLHttpRequest();
const url = '/nodes/' + nodeId + "/systems";
Http.open("DELETE", url);
Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody));
Http.onloadend = (e) => {
const responseObject = Http.responseText;
console.log(Http.status);
console.log(responseObject);
createToast(`${systemName} Removed!`);
location.reload();
}
}
function requestNodeUpdate() {
}
function removeFrequencyInput(elementId) {
const element = document.getElementById(elementId);
element.remove();
}

View File

@@ -2,11 +2,7 @@
---
The server application acts as the central hub within Discord, providing various functionalities and serving as the main point of communication for the clients. Some of the key features and responsibilities of the server include:
- **RSS Feed Updates**: The server periodically updates text channels with RSS feed updates, keeping users informed about the latest news or information.
- **Server Management Functions / User Requests**: The server includes management functions that allow administrators to control and configure various aspects of the server environment. Users can interact with the server through Discord commands, which range from requesting specific radio presets to updating RSS feeds.
- **API and Web Front End**: The server exposes an API and web front end, providing an interface to view and control all the online clients. This allows users to monitor and manage the available radio presets, as well as perform various administrative tasks.
Overview here
## Requirements

View File

@@ -1,45 +1,31 @@
const libCore = require("../libCore");
var express = require('express');
var router = express.Router();
const { getAllNodes, getNodeInfoFromId, getAllConnections } = require("../utilities/mysqlHandler");
const { filterPresetsAvailable } = require("../utilities/utils");
/* GET home page. */
router.get('/', async (req, res) => {
var nodes = await new Promise((recordResolve, recordReject) => {
getAllNodes((nodeRows) => {
recordResolve(nodeRows);
});
router.get('/', (req, res) => {
var sources = libCore.getSources();
//res.render('index', { "sources": sources });
var htmlOutput = "";
sources.forEach(source => {
htmlOutput += `
<div style='margin-bottom:15px;'>
<div> Title: ${source.title} </div>
<div> Link: ${source.link} </div>
<div> category: ${source.category} </div>
</div>
<div>
<hr />
</div>
`
});
var connections = await getAllConnections();
var presets = await new Promise((recordResolve, recordReject) => {
getAllNodes((nodeRows) => {
recordResolve(filterPresetsAvailable(nodeRows));
});
});
//var sources = libCore.getSources();
return res.render('index', { 'page': 'index', 'nodes': nodes, 'connections': connections, 'presets': presets });
});
/* GET node controller page. */
router.get('/controller', async (req, res) => {
var nodes = await new Promise((recordResolve, recordReject) => {
getAllNodes((nodeRows) => {
recordResolve(nodeRows);
});
});
//var sources = libCore.getSources();
return res.render('controller', { 'nodes': nodes, 'page': 'controller' });
});
/* GET individual node page. */
router.get('/node/:id', async (req, res) => {
var node = await getNodeInfoFromId(req.params.id);
//var sources = libCore.getSources();
return res.render('node', { 'node': node, 'page': 'node' });
res.send(htmlOutput);
});
module.exports = router;

View File

@@ -5,22 +5,6 @@ const nodesController = require('../controllers/nodesController');
/* GET nodes the server knows */
router.get('/', nodesController.listAllNodes);
// TODO Need to authenticate this request
/* GET the information the server has on a particular node */
router.get('/:nodeId', nodesController.getNodeInfo);
// Update an existing node
router.put('/:nodeId', nodesController.updateExistingNode);
// Add a system to an existing node
router.post('/:nodeId/systems', nodesController.addNodeSystem);
// Update a system on an existing node
router.put('/:nodeId/systems', nodesController.updateNodeSystem);
// Delete a system from an existing node
router.delete('/:nodeId/systems', nodesController.removeNodeSystem);
// TODO Need to authenticate this request
/* POST a new node to the server
*
@@ -37,10 +21,15 @@ router.delete('/:nodeId/systems', nodesController.removeNodeSystem);
router.post('/newNode', nodesController.newNode);
// TODO Need to authenticate this request
// Client checkin with the server to update client information
router.post('/nodeCheckIn/:nodeId', nodesController.nodeCheckIn);
/* GET the information the server has on a particular node */
router.get('/nodeInfo', nodesController.getNodeInfo);
// Request a node to check in with the server
router.get('/nodeCheckIn/:nodeId', nodesController.requestNodeCheckIn);
// TODO Need to authenticate this request
// Client checkin with the server to update information
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;

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

@@ -4,7 +4,6 @@ const debug = require('debug');
require('dotenv').config();
// Modules
const { writeFile } = require('fs');
const { inspect } = require('util');
const logLocation = process.env.LOG_LOCATION;
@@ -35,31 +34,31 @@ exports.DebugBuilder = class DebugBuilder {
this.INFO = (...messageParts) => {
const _info = debug(`${appName}:${fileName}:INFO`);
_info(messageParts);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:INFO\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
writeToLog(`${appName}:${fileName}:INFO\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
}
this.DEBUG = (...messageParts) => {
const _debug = debug(`${appName}:${fileName}:DEBUG`);
_debug(messageParts);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:DEBUG\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
writeToLog(`${appName}:${fileName}:DEBUG\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
}
this.VERBOSE = (...messageParts) => {
const _verbose = debug(`${appName}:${fileName}:VERBOSE`);
_verbose(messageParts);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:VERBOSE\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
writeToLog(`${appName}:${fileName}:VERBOSE\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
}
this.WARN = (...messageParts) => {
const _warn = debug(`${appName}:${fileName}:WARNING`);
_warn(messageParts);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:WARNING\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
writeToLog(`${appName}:${fileName}:WARNING\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
}
this.ERROR = (...messageParts) => {
const _error = debug(`${appName}:${fileName}:ERROR`);
_error(messageParts);
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:ERROR\t-\t${messageParts.map((messagePart, index, array) => {return inspect(messagePart)})}`, appName);
writeToLog(`${appName}:${fileName}:ERROR\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
if (process.env.EXIT_ON_ERROR && process.env.EXIT_ON_ERROR > 0) {
writeToLog("!--- EXITING ---!", appName);
setTimeout(process.exit, process.env.EXIT_ON_ERROR_DELAY ?? 0);

View File

@@ -11,14 +11,14 @@ const path = require('node:path');
const { DebugBuilder } = require("./debugBuilder");
const log = new DebugBuilder("server", "deployCommands");
var commands = [];
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];
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)}`);
@@ -48,35 +48,3 @@ exports.deploy = (clientId, guildIDs) => {
})()
}
};
/**
* Remove all commands for a given bot in a given guild
*
* @param {*} clientId The client ID of the bot to remove commands from
* @param {*} guildId The ID of the guild to remove the bot commands from
*/
exports.removeAll = (clientId, guildId) => {
if (!Array.isArray(guildId)) guildIDs = [guildId];
log.DEBUG("Removing commands for: ", clientId, guildIDs);
commands = [];
const rest = new REST({ version: '10' }).setToken(token);
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

@@ -40,7 +40,7 @@ exports.sendHttpRequest = function sendHttpRequest(requestOptions, data, callbac
res.on('data', (data) => {
const responseObject = {
"statusCode": res.statusCode,
"body": (isJsonString(data.toString())) ? JSON.parse(data.toString()) : data.toString()
"body": (isJsonString(data.toString)) ? JSON.parse(data) : data.toString()
};
log.DEBUG("Response Object: ", responseObject);
callback(responseObject);

View File

@@ -1,5 +1,5 @@
require('dotenv').config();
const mysql = require('mysql2');
const mysql = require('mysql');
const utils = require('./utils');
const { nodeObject, clientObject, connectionObject } = require("./recordHelper");
const { DebugBuilder } = require("../utilities/debugBuilder");
@@ -24,8 +24,6 @@ const nodeConnectionsTable = `${process.env.NODE_DB_NAME}.node_connections`;
* @returns {nodeObject} The converted node object to be used downstream
*/
function returnNodeObjectFromRow(row) {
if (!isNaN(row.online)) row.online = Boolean(row.online);
else if (row.online == "true" || row.online == "false") row.online = (row.online == "true");
return new nodeObject({
_id: row.id,
_name: row.name,
@@ -33,7 +31,7 @@ function returnNodeObjectFromRow(row) {
_port: row.port,
_location: row.location,
_nearbySystems: BufferToJson(row.nearbySystems),
_online: row.online,
_online: (row.online === 1) ? true : false,
});
}
@@ -94,7 +92,7 @@ exports.getAllNodesSync = async () => {
console.log("Rows: ", rows);
return await returnNodeObjectFromRows(rows);
return returnNodeObjectFromRows(rows);
}
/** Get all nodes that have the online status set true (are online)
@@ -112,7 +110,7 @@ exports.getOnlineNodes = (callback) => {
* @param callback Callback function
*/
async function getNodeInfoFromId(nodeId, callback = undefined) {
if (!nodeId || nodeId == '0' || nodeId == 0 ) throw new Error("No node ID given when trying to fetch node");
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}`
@@ -124,8 +122,7 @@ exports.getOnlineNodes = (callback) => {
// Call back the first (and theoretically only) row
// Specify 0 so downstream functions don't have to worry about it
if (!sqlResponse.length > 0) return (callback) ? callback(false) : false;
return (callback) ? callback(await returnNodeObjectFromRow(sqlResponse[0])) : await returnNodeObjectFromRow(sqlResponse[0]);
return (callback) ? callback(returnNodeObjectFromRow(sqlResponse[0])) : returnNodeObjectFromRow(sqlResponse[0]);
}
exports.getNodeInfoFromId = getNodeInfoFromId
@@ -139,15 +136,10 @@ exports.addNewNode = async (nodeObject, callback) => {
ip = nodeObject.ip,
port = nodeObject.port,
location = nodeObject.location,
nearbySystems = utils.JsonToBuffer(nodeObject.nearbySystems ?? {})
var online = nodeObject.online;
if (typeof online === "boolean" || typeof online === "number") {
if (online) online = 1;
else online = 0;
}
const sqlQuery = `INSERT INTO ${nodesTable} (name, ip, port, location, nearbySystems, online) VALUES ('${name}', '${ip}', ${port}, '${location}', '${nearbySystems}', ${online})`;
nearbySystems = utils.JsonToBuffer(nodeObject.nearbySystems),
online = nodeObject.online,
connected = 0;
const sqlQuery = `INSERT INTO ${nodesTable} (name, ip, port, location, nearbySystems, online, connected) VALUES ('${name}', '${ip}', ${port}, '${location}', '${nearbySystems}', ${online}, ${connected})`;
const sqlResponse = await new Promise((recordResolve, recordReject) => {
runSQL(sqlQuery, (rows) => {
@@ -157,9 +149,7 @@ exports.addNewNode = async (nodeObject, callback) => {
// Call back the first (and theoretically only) row
// Specify 0 so downstream functions don't have to worry about it
const newNode = await this.getNodeInfoFromId(sqlResponse.insertId);
log.DEBUG("Added new node: ", newNode)
return (callback) ? callback(newNode) : newNode;
return (callback) ? callback(returnNodeObjectFromRow(sqlResponse)) : returnNodeObjectFromRow(sqlResponse);
}
/** Update the known info on a node
@@ -216,7 +206,7 @@ exports.updateNodeInfo = async (nodeObject, callback = undefined) => {
});
if (sqlResponse.affectedRows === 1) return (callback) ? callback(true) : true;
else return (callback) ? callback(await returnNodeObjectFromRows(sqlResponse)) : await returnNodeObjectFromRows(sqlResponse);
else return (callback) ? callback(returnNodeObjectFromRows(sqlResponse)) : returnNodeObjectFromRows(sqlResponse);
}
/**
@@ -363,12 +353,12 @@ exports.getAllConnections = async (callback = undefined) => {
// Function to run and handle SQL errors
function runSQL(sqlQuery, callback = undefined, error = (err) => {
log.ERROR(err);
console.log(err);
throw err;
}) {
connection.query(sqlQuery, (err, rows) => {
if (err) return error(err);
log.VERBOSE('Response for query: ', sqlQuery, rows);
//console.log('The rows are:', rows);
return (callback) ? callback(rows) : rows
})
}

View File

@@ -109,17 +109,18 @@ class nodeObject {
* @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 True/False if the node is online or offline
* @param {*} param0._online True/False if the node is online or offline
* @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 = undefined, _name = undefined, _ip = undefined, _port = undefined, _location = undefined, _nearbySystems = undefined, _online = undefined }) {
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.location = _location;
this.nearbySystems = _nearbySystems;
if (this.nearbySystems) this.presets = Object.keys(_nearbySystems);
this.online = _online;
}
}
@@ -136,7 +137,7 @@ class clientObject {
* @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 = undefined, _name = undefined, _client_id = undefined,}) {
constructor({_discord_id = null, _name = null, _client_id = null,}) {
this.discordId = _discord_id;
this.name = _name;
this.clientId = _client_id;
@@ -155,7 +156,7 @@ class connectionObject {
* @param {*} param0._node The node associated with the connection
* @param {*} param0._client_object The client object associated with the connection
*/
constructor({_connection_id = undefined, _node = undefined, _client_object}) {
constructor({_connection_id = null, _node = null, _client_object}) {
this.connectionId = _connection_id;
this.node = _node;
this.clientObject = _client_object;

View File

@@ -3,7 +3,6 @@ const { DebugBuilder } = require("../utilities/debugBuilder");
const { clientObject } = require("./recordHelper");
const { readFileSync } = require('fs');
const log = new DebugBuilder("server", "utils");
const logAC = new DebugBuilder("server", "command-autocorrect");
const path = require('path');
// Convert a JSON object to a buffer for the DB
@@ -23,7 +22,7 @@ exports.BufferToJson = (buffer) => {
* @returns {string} The sanitized preset name to be used elsewhere
*/
exports.SanitizePresetName = (presetName) => {
return String(presetName).toLowerCase().replace(/[\W_]+/g, "-")
return String(presetName).toLowerCase().replace(/[\W_]+/g,"-")
}
/**
@@ -32,16 +31,16 @@ exports.SanitizePresetName = (presetName) => {
* @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") => {
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 });
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);
@@ -50,8 +49,8 @@ exports.getMembersInRole = async (interaction, roleName = "Bots") => {
log.VERBOSE("All members: ", allMembers, onlineMembers, offlineMembers)
return {
'online': onlineMembers,
'offline': offlineMembers,
'online': onlineMembers,
'offline': offlineMembers,
'all': allMembers
}
}
@@ -64,9 +63,9 @@ exports.getMembersInRole = async (interaction, roleName = "Bots") => {
*/
exports.getKeyByArrayValue = (object, value) => {
if (typeof value == "string") return Object.keys(object).find(key => object[key].includes(value));
const valueKey = Object.keys(value)[0];
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
@@ -91,7 +90,7 @@ exports.isJsonString = (str) => {
exports.getAllClientIds = () => {
const jsonClientIds = JSON.parse(readFileSync(path.resolve(__dirname, '../clientIds.json')));
var clientObjects = [];
for (const jsonClientId of Object.keys(jsonClientIds)) {
for (const jsonClientId of Object.keys(jsonClientIds)){
clientObjects.push(new clientObject({
_discord_id: jsonClientId,
_name: jsonClientIds[jsonClientId].name,
@@ -110,54 +109,11 @@ exports.getAllClientIds = () => {
exports.getClientObjectByClientID = (clientId) => {
const clientObjects = this.getAllClientIds();
log.DEBUG("All client IDs: ", clientObjects);
for (const clientObject of clientObjects) {
for (const clientObject of clientObjects){
if (clientObject.clientId == clientId) {
log.DEBUG("Found client ID from given ID: ", clientObject);
return clientObject
}
}
return undefined
}
/**
* Wrapper to filter auto complete
*
* @param {*} interaction
* @param {*} options
*/
exports.filterAutocompleteValues = async (interaction, options) => {
// Get the command used
const command = interaction.command;
// Find values that start with what the user is entering
const focusedValue = interaction.options.getFocused();
const filtered = options.filter(preset => preset.startsWith(focusedValue));
// Give the query response to the user
logAC.DEBUG("Focused Value: ", command, focusedValue, options, filtered);
await interaction.respond(
filtered.map(option => ({ name: option, value: option })),
);
}
/**
* Filter an array of nodeObjects to get all unique presets within
*
* @param {Array} nodeObjects An array of nodeObjects to get the presets from
* @returns {Array} Presets available from given nodeObjects
*/
exports.filterPresetsAvailable = async (nodeObjects) => {
log.DEBUG("Node objects: ", nodeObjects);
var presetsAvailable = [];
for (const nodeObject of nodeObjects) {
log.DEBUG("Node object: ", nodeObject);
presetsAvailable.push.apply(presetsAvailable, nodeObject.presets);
}
log.DEBUG("All Presets available: ", presetsAvailable);
// Remove duplicates
presetsAvailable = [...new Set(presetsAvailable)];
log.DEBUG("DeDuped Presets available: ", presetsAvailable);
return presetsAvailable;
}

View File

@@ -1,11 +0,0 @@
<%- include('partials/htmlHead.ejs') %>
<div class="container">
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-4">
<% for(const node of nodes) {%>
<%- include('partials/nodeCard.ejs', {'node': node}) %>
<% } %>
</div>
</div>
<%- include('partials/bodyEnd.ejs') %>
<script src="/res/js/node.js"></script>
<%- include('partials/htmlFooter.ejs') %>

View File

@@ -1,64 +1,11 @@
<%- include('partials/htmlHead.ejs', {'page': page}) %>
<div class="container">
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-4 mt-2">
<%- include('partials/valueChip.ejs', {
'title': 'Nodes in the Network',
'bgColor': "orange-dark",
'value': nodes.length,
'progressPercent': false,
'icon': 'server'
}) %>
<%- include('partials/valueChip.ejs', {
'title': 'Nodes Online',
'bgColor': "green",
'value': nodes.filter(node => node.online).length,
'progressPercent': false,
'icon': 'cpu-fill'
}) %>
<%- include('partials/valueChip.ejs', {
'title': 'Nodes with Discord Connections',
'bgColor': "blue-dark",
'value': connections.length,
'progressPercent': false,
'icon': 'gear-wide-connected'
}) %>
</div>
<div class="my-4">
<p><h3><b>Current Connections</b></h3></p>
</div>
<hr>
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-4">
<% for(const conn of connections) { %>
<%- include('partials/connectionCard.ejs', {'connection': conn}) %>
<%}%>
</div>
<div class="my-4">
<p><h3><b>Online Nodes</b></h3></p>
</div>
<hr>
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-4">
<% for(const node of nodes.filter(node => node.online)) { %>
<%- include('partials/nodeCard.ejs', {'node': node}) %>
<%}%>
</div>
<div class="my-4">
<p><h3><b>Offline Nodes</b></h3></p>
</div>
<hr>
<div class="row row-cols-1 row-cols-md-2 row-cols-xl-4">
<% for(const node of nodes.filter(node => node.online == false)) { %>
<%- include('partials/nodeCard.ejs', {'node': node}) %>
<%}%>
</div>
</div>
<%- include('partials/bodyEnd.ejs') %>
<%- include('partials/htmlFooter.ejs') %>
<!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>

View File

@@ -1,140 +0,0 @@
<%- include('partials/htmlHead.ejs') %>
<div class="container">
<div class="card mb-4">
<div class="card-header">
<p>
<span class="fs-2 fw-semibold">
Node Details
</span>
</p>
</div>
<div class="card-body">
<div class="col-md-12 pt-2">
<label class="small mb-1" for="nodeStatus">Online Status:</label>
<% if(node.online){%> <span class="badge badge-soft-success mb-0 align-middle fs-6" id="nodeStatus">Online</span>
<% } else {%> <span class="badge badge-soft-danger mb-0 align-middle fs-6">Offline</span>
<% } %>
<br>
<div class="py-2"></div>
<!-- Join Server button-->
<a type="button" class="btn btn-info text-white<% if(!node.online) { %>disabled<% } %>" data-bs-toggle="modal" data-bs-target="#joinModal" href="#">Join Server</a>
<!-- Leave Server button -->
<a type="button" class="btn btn-danger <% if(!node.online) { %>disabled<% } %>" href="#" onclick="leaveServer()">Leave Server</a>
<!-- Checkin with client button -->
<a type="button" class="btn btn-secondary" href="#" onclick="sendNodeHeartbeat('<%=node.id%>')">Check-in with Node</a>
<!-- Update Client button -->
<a type="button" class="btn btn-warning disabled" href="#" onclick="requestNodeUpdate('<%=node.id%>')">Update Node</a>
</div>
<hr>
<form>
<div class="row gx-3 mb-3">
<div class="col-md-6">
<label class="small mb-1" for="nodeId">Node ID (this is the assigned Node ID and cannot be
changed)</label>
<input class="form-control" id="nodeId" type="text" value="<%=node.id%>" disabled></input>
</div>
</div>
<div class="row gx-3 mb-3">
<div class="col-md-12">
<label class="small mb-1" for="inputNodeName">Node Name:</label>
<input class="form-control" id="inputNodeName" type="text" value="<%=node.name%>"></input>
</div>
</div>
<div class="row gx-3 mb-3">
<div class="col-md-4">
<label class="small mb-1" for="inputNodeIp">Node IP Address (that the server can
contact):</label>
<input class="form-control" id="inputNodeIp" type="text" value="<%=node.ip%>"></input>
</div>
<div class="col-md-2">
<label class="small mb-1" for="inputOrgName">Node Port (with the API):</label>
<input class="form-control" id="inputOrgName" type="number" value="<%=node.port%>"></input>
</div>
</div>
<div class="mb-3">
<label class="small mb-1" for="inputNodeLocation">Node Location (physical location):</label>
<input class="form-control" id="inputNodeLocation" type="location" value="<%=node.location%>"></input>
</div>
<h4>
Nearby Systems
</h4>
<hr>
<div class="row">
<div class="col-lg-12">
<div class="main-box no-header clearfix">
<div class="main-box-body clearfix">
<div class="table-responsive">
<table class="table user-list <% if(!node.online) { %>disabled<% } %>">
<thead>
<tr>
<th><span>System Name</span></th>
<th><span>Frequencies</span></th>
<th><span>Protocol</span></th>
<th>&nbsp;</th>
</tr>
</thead>
<tbody>
<% for(const system in node.nearbySystems){ %>
<tr>
<td>
<%= system %>
</td>
<td>
<% if(node.nearbySystems[system].frequencies.length> 1) { %>
<ul>
<% for(const frequency of
node.nearbySystems[system].frequencies) { %>
<li>
<%=frequency%> MHz
</li>
<% } %>
</ul>
<% } else { const
frequency=node.nearbySystems[system].frequencies[0]
%>
<%=frequency%> MHz
<% } %>
</td>
<td>
<span class="label label-default text-uppercase">
<%= node.nearbySystems[system].mode %>
</span>
</td>
<td>
<a href="#" class="table-link text-info label"
data-bs-toggle="modal"
data-bs-target="#updateSystemModal_<%=system.replaceAll(" ", "_")%>">
<i class="bi bi-pencil"></i>
</a>
<a class="table-link text-danger label" onclick="removeSystem('<%=system%>')">
<i class="bi bi-trash"></i>
</a>
</td>
</tr>
<% // Update system modal %>
<%- include("partials/modifySystemModal.ejs", {'system': system, 'frequencies': node.nearbySystems[system].frequencies, 'mode': node.nearbySystems[system].mode}) %>
<% } %>
</tbody>
</table>
</div>
</div>
</div>
</div>
</div>
<!-- Save changes button-->
<button class="btn btn-primary <% if(!node.online) { %>disabled<% } %>" type="button" onclick="saveNodeDetails()">Save changes</button>
<!-- Button trigger modal -->
<button type="button" class="btn btn-primary float-right <% if(!node.online) { %>disabled<% } %>" data-bs-toggle="modal"
data-bs-target="#updateSystemModal_New_System">Add New System</button>
</form>
</div>
</div>
</div>
<% // new System Modal %>
<%- include("partials/modifySystemModal.ejs", {'system': "New System", 'frequencies': [], 'mode': ''}) %>
<% // Join Server Modal %>
<%- include("partials/joinModal.ejs", {'node': node}) %>
<%- include('partials/bodyEnd.ejs') %>
<%- include('partials/htmlFooter.ejs') %>

View File

@@ -1,14 +0,0 @@
</div>
</div>
</div>
</body>
<script src="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/js/bootstrap.bundle.min.js"
integrity="sha384-geWF76RCwLtnZ8qwWowPQNguL3RmwHVBC9FhGdlKrxdiJJigb/j/68SIy3Te4Bkz"
crossorigin="anonymous"></script>
<script src="https://cdnjs.cloudflare.com/ajax/libs/jquery/3.7.0/jquery.min.js"
integrity="sha512-3gJwYpMe3QewGELv8k/BX9vcqhryRdzRMxVfq6ngyWXwo03GFEzjsUm8Q7RZcHPHksttq7/GFoxjCVUjkjvPdw=="
crossorigin="anonymous" referrerpolicy="no-referrer"></script>
<script src="/res/js/node.js"></script>

View File

@@ -1,29 +0,0 @@
<div class="col-xl-3 col-sm-6">
<div class="card node-card">
<div class="card-body">
<div class="dropdown float-end">
<a class="text-muted dropdown-toggle font-size-16" href="#" role="button" data-bs-toggle="dropdown"
aria-haspopup="true">
<i class="bx bx-dots-horizontal-rounded"></i>
</a>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item node-action">Edit</a>
<a class="dropdown-item node-action" onclick="">Send Heartbeat</a>
</div>
</div>
<div class="d-flex align-items-center">
<div class="flex-1 ms-3">
<h5 class="font-size-16 mb-1"><a class="text-dark">
<%= connection.clientObject.name%>
</a></h5>
</div>
</div>
<div class="mt-3 pt-1">
<p class="text-muted mb-0">
<i class="bi bi-cpu-fill font-size-15 align-middle pe-2 text-primary"></i>
Node ID: <a href="/node/<%= connection.node.id %>"><%= connection.node.id %></a>
</p>
</div>
</div>
</div>
</div>

View File

@@ -1,22 +0,0 @@
<head>
<meta charset="utf-8">
<meta name="viewport" content="width=device-width, initial-scale=1">
<% switch (page) {
case "index":%>
<title>Node Dashboard</title>
<% break;
case "controller":%>
<title>Node Controller</title>
<% break;
case "node":%>
<title>Node Configuration</title>
<% break;
default:%>
<title>DRB_CnC Server</title>
<%break;
} %>
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.3.0/font/bootstrap-icons.css">
<link rel="stylesheet" href="/res/css/main.css">
</head>

View File

@@ -1 +0,0 @@
</html>

View File

@@ -1,6 +0,0 @@
<!doctype html>
<html lang="en" data-bs-theme="auto">
<%- include('head.ejs', {'page': page}) %>
<body>
<%- include('navbar.ejs') %>
<%- include('sidebar.ejs') %>

View File

@@ -1,44 +0,0 @@
<div class="modal fade" id="joinModal" tabindex="-1" aria-labelledby="joinModal" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="joinModal">Join Node <%=node.id%> to a Discord Server</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="container">
<div class="card mb-4">
<div class="card-body">
<form>
<div class="row gx-3 mb-3">
<div class="col-md-12">
<label class="small mb-1" for="inputDiscordClientId">Discord Client ID:</label>
<input class="form-control" id="inputDiscordClientId" type="text" value="" required></input>
</div>
</div>
<div class="row gx-3 mb-3">
<div class="col-md-6">
<label class="small mb-1" for="inputDiscordChannelId">Discord Channel ID:</label>
<input class="form-control" id="inputDiscordChannelId" type="text" value="" required></input>
</div>
<div class="col-md-6">
<label class="small mb-1" for="selectRadioPreset">Selected Preset:</label>
<select class="custom-select" id="selectRadioPreset">
<% for(const system in node.nearbySystems) { %>
<option value="<%=system%>"><%=system%></option>
<% } %>
</select>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="joinServer()">Join</button>
</div>
</div>
</div>
</div>

View File

@@ -1,61 +0,0 @@
<div class="modal fade" id="updateSystemModal_<%=system.replaceAll(" ", "_")%>" tabindex="-1" aria-labelledby="updateSystemModal_<%=system.replaceAll(" ", "_")%>"
aria-hidden="true" data-bs-backdrop="static" data-bs-keyboard="false">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="updateSystemModal_<%=system.replaceAll(" ", "_")%>"><%if (!system == "New System") {%>Update<%} else {%>Add a<%}%> <%=system%></h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="card mb-4">
<div class="card-body">
<form>
<div class="row gx-3 mb-3">
<label class="small mb-1 fs-6" for="systemName">System Name</label>
<input class="form-control" id="<%=system%>_systemName" type="text" value="<%if (system != "New System") {%><%= system %><%} else {%>Local Radio System<%}%>"></input>
</div>
<div class="row gx-3 mb-3" id="frequencyRow_<%=system.replaceAll(" ", "_")%>">
<label class="small mb-1 fs-6" for="systemFreq">Frequencies</label>
<% for(const frequency of frequencies) { %>
<div class="col-md-6 mb-1" id="<%=system%>_systemFreqRow_<%=frequency%>">
<div class="row px-1">
<div class="col-10">
<input class="form-control" id="<%=system%>_systemFreq_<%=frequency%>" type="text" value="<%= frequency %>"></input>
</div>
<div class="col-2">
<a class="align-middle float-left" href="#" onclick="removeFrequencyInput('<%=system%>_systemFreqRow_<%=frequency%>')"><i class="bi bi-x-circle text-black"></i></a>
</div>
</div>
</div>
<% } %>
</div>
<button type="button" class="btn btn-info text-white" onclick="addFrequencyInput('<%=system%>')">Add Frequency</button>
<hr>
<div class="row gx-3 mb-3">
<div class="col-md-6">
<label class="small mb-1 fs-6" for="<%=system%>_systemMode">Mode</label>
<br>
<select class="custom-select" id="<%=system%>_systemMode">
<option value="<%= mode ?? 'select' %>" selected><span class="text-uppercase"><%= mode ?? 'Select' %></span></option>
<% if(mode == "p25") { %>
<option value="nbfm">NBFM</option>
<% } else if (mode == "nbfm") { %>
<option value="p25">P25</option>
<% } else { %>
<option value="nbfm">NBFM</option>
<option value="p25">P25</option>
<%}%>
</select>
</div>
</div>
</form>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal" onclick="location.reload()">Close</button>
<button type="button" class="btn btn-primary" <%if(!system == "New System") {%>onclick="updateSystem('<%=system%>')"<%} else {%>onclick="addNewSystem('<%=system%>')"<%}%>>Save changes</button>
</div>
</div>
</div>
</div>

View File

@@ -1,42 +0,0 @@
<nav class="navbar fixed-top navbar-expand-lg bg-body-tertiary" data-bs-theme="dark">
<div class="container-fluid">
<a class="navbar-brand" href="#">Node Master</a>
<button class="navbar-toggler" type="button" data-bs-toggle="collapse" data-bs-target="#navbarSupportedContent"
aria-controls="navbarSupportedContent" aria-expanded="false" aria-label="Toggle navigation">
<span class="navbar-toggler-icon"></span>
</button>
<div class="collapse navbar-collapse" id="navbarSupportedContent">
<ul class="navbar-nav me-auto mb-2 mb-lg-0">
<% /*
<li class="nav-item">
<a class="nav-link active" aria-current="page" href="#">Home</a>
</li>
<li class="nav-item">
<a class="nav-link" href="#">Link</a>
</li>
<li class="nav-item dropdown">
<a class="nav-link dropdown-toggle" href="#" role="button" data-bs-toggle="dropdown"
aria-expanded="false">
Dropdown
</a>
<ul class="dropdown-menu">
<li><a class="dropdown-item" href="#">Action</a></li>
<li><a class="dropdown-item" href="#">Another action</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li><a class="dropdown-item" href="#">Something else here</a></li>
</ul>
</li>
*/%>
<li class="nav-item">
<a class="nav-link" id="navbar-notification-bell" onclick="showStoredToasts()"><i class="bi bi-bell-fill"></i></a>
</li>
</ul>
<form class="d-flex" role="search">
<input class="form-control me-2" type="search" placeholder="Search" aria-label="Search">
<button class="btn btn-outline-success" type="submit">Search</button>
</form>
</div>
</div>
</nav>

View File

@@ -1,48 +0,0 @@
<div class="col-xl-3 col-sm-6">
<div class="card node-card">
<div class="card-body">
<div class="dropdown float-end">
<a class="text-muted dropdown-toggle font-size-16" href="#" role="button" data-bs-toggle="dropdown"
aria-haspopup="true">
<i class="bx bx-dots-horizontal-rounded"></i>
</a>
<div class="dropdown-menu dropdown-menu-end">
<a class="dropdown-item node-action" href="/node/<%=node.id%>" >Edit</a>
<a class="dropdown-item node-action" href="#" onclick="sendNodeHeartbeat('<%=node.id%>')">Send Heartbeat</a>
</div>
</div>
<div class="d-flex align-items-center">
<div class="flex-1 ms-3">
<h5 class="font-size-16 mb-1"><a href="/node/<%=node.id%>" class="text-dark">
<%= node.name %>
</a></h5>
<% if(node.online){%> <span class="badge badge-soft-success mb-0">Online</span>
<% } else {%> <span class="badge badge-soft-danger mb-0">Offline</span>
<% } %>
</div>
</div>
<div class="mt-3 pt-1">
<p class="text-muted mb-0">
<i class="bi bi-geo font-size-15 align-middle pe-2 text-primary"></i>
<%= node.location %>
</p>
<p class="text-muted mb-0">
<i class="bi bi-phone font-size-15 align-middle pe-2 text-primary"></i>
<a href="#" target="_blank">
<%= node.ip %>:<%= node.port %>
</a>
</p>
<hr>
<p class="text-muted mb-0">
<i class="bi bi-broadcast-pin font-size-15 pe-2 text-primary"></i>
Nearby Systems:
<ul>
<% for(const system in node.nearbySystems){ %>
<li><%= system %></li>
<% } %>
</ul>
</p>
</div>
</div>
</div>
</div>

View File

@@ -1,48 +0,0 @@
<div class="container-fluid mt-5">
<div aria-live="polite" aria-atomic="true" class="position-relative">
<!-- Position it: -->
<!-- - `.toast-container` for spacing between toasts -->
<!-- - `top-0` & `end-0` to position the toasts in the upper right corner -->
<!-- - `.p-3` to prevent the toasts from sticking to the edge of the container -->
<div class="toast-container top-0 end-0 p-3 max" id="toastZone">
</div>
</div>
<div class="row flex-nowrap">
<div class="col-auto col-md-3 col-xl-2 px-sm-2 px-0 bg-dark sidebar-container">
<div
class="d-flex flex-column align-items-center align-items-sm-start px-3 pt-2 text-white sidebar">
<ul class="nav nav-pills flex-column mb-sm-auto mb-0 align-items-center align-items-sm-start" id="menu">
<li>
<a href="/" class="nav-link px-0 align-middle">
<i class="fs-4 bi-speedometer2"></i> <span class="ms-1 d-none d-sm-inline">Dashboard</span>
</a>
</li>
<li>
<a href="/controller" class="nav-link px-0 align-middle">
<i class="fs-4 bi-grid"></i> <span class="ms-1 d-none d-sm-inline">Controller</span>
</a>
</li>
</ul>
<hr>
<% /* <div class="dropdown pb-4 fixed-bottom px-3">
<a href="#" class="d-flex align-items-center text-white text-decoration-none dropdown-toggle"
id="dropdownUser1" data-bs-toggle="dropdown" aria-expanded="false">
<img src="https://github.com/mdo.png" alt="hugenerd" width="30" height="30"
class="rounded-circle">
<span class="d-none d-sm-inline mx-1">loser</span>
</a>
<ul class="dropdown-menu dropdown-menu-dark text-small shadow">
<li><a class="dropdown-item" href="#">New project...</a></li>
<li><a class="dropdown-item" href="#">Settings</a></li>
<li><a class="dropdown-item" href="#">Profile</a></li>
<li>
<hr class="dropdown-divider">
</li>
<li><a class="dropdown-item" href="#">Sign out</a></li>
</ul>
</div>
*/%>
</div>
</div>
<div class="col py-3">

View File

@@ -1,32 +0,0 @@
<div class="col-xl-3 col-lg-6">
<div class="info-card l-bg-<%=bgColor%>">
<div class="card-statistic p-4">
<div class="card-icon card-icon-large"><i class="bi bi-<%=icon%>"></i></div>
<div class="mb-4">
<h5 class="card-title mb-0">
<%=title%>
</h5>
</div>
<div class="row align-items-center mb-2 d-flex">
<div class="col-8">
<h2 class="d-flex align-items-center mb-0">
<%=value%>
</h2>
</div>
<% if (progressPercent) {%>
<div class="col-4 text-right">
<span>
<%=progressPercent%>%<i class="fa fa-arrow-up"></i>
</span>
</div>
<%}%>
</div>
<% if (progressPercent) {%>
<div class="progress mt-1 " data-height="8" style="height: 8px;">
<div class="progress-bar l-bg-cyan" role="progressbar" data-width="<%=progressPercent%>%" aria-valuenow="<%=progressPercent%>"
aria-valuemin="0" aria-valuemax="100" style="width: <%=progressPercent%>%;"></div>
</div>
<%}%>
</div>
</div>
</div>

View File

@@ -1,27 +1,32 @@
# Project Overview
This project is a multi-layered application consisting of client and server applications. Its main purpose is to enable the use of Software-Defined Radios (SDRs) and Raspberry Pi (or similar Single Board Computers) to listen to radio frequencies in Discord voice channels. The project is designed to provide a seamless integration between the SDR hardware and the server with Discord commands.
## Server Application
The server application acts as the central hub within Discord, providing various functionalities and serving as the main point of communication for the clients. Some of the key features and responsibilities of the server include:
- **RSS Feed Updates**: The server periodically updates text channels with RSS feed updates, keeping users informed about the latest news or information.
- **Server Management Functions / User Requests**: The server includes management functions that allow administrators to control and configure various aspects of the server environment. Users can interact with the server through Discord commands, which range from requesting specific radio presets to updating RSS feeds.
- **API and Web Front End**: The server exposes an API and web front end, providing an interface to view and control all the online clients. This allows users to monitor and manage the available radio presets, as well as perform various administrative tasks.
#### [Read more about the Server](https://git.vpn.cusano.net/logan/DRB-CnC/src/branch/master/Server)
# Discord Radio Bot: Command & Control
---
## Client Application
The client application communicates with the server through the provided API. Each client instance waits for join requests sent by users through Discord. Once a join request is received, the client uses the SDR application to tune into the specified radio preset. It then establishes a connection to Discord, allowing users to listen to the selected radio preset in real-time.
Project overview here
In addition to its interaction with the server, the client also has its own API and web application. This enables users to directly interface with the client, perform actions specific to the client application, and access relevant information about the connected SDR and radio presets.
## Requirements Overview
#### [Read more about the Client](https://git.vpn.cusano.net/logan/DRB-CnC/src/branch/master/Client)
---
---
### Server Requirements
#### Server: Discord Bot Requirements
### Client Requirements
#### Client: Discord Bot Requirements
## Troubleshooting
Check the [wiki](https://git.vpn.cusano.net/logan/DRB-CnC/wiki)
## Server
---
Explanation and overview here
## Client
---
Explanation and overview here
## Discord Bot
---
Explanation and overview here