1 Commits

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

1
.gitignore vendored
View File

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

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);
@@ -83,77 +82,3 @@ exports.leaveServer = async (req, res) => {
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,7 +5,7 @@ const log = new DebugBuilder("client", "clientController");
require('dotenv').config();
const modes = require("../config/modes");
// Modules
const { executeAsyncConsoleCommand, nodeObject, BufferToJson } = require("../utilities/utilities");
const { executeAsyncConsoleCommand, nodeObject } = require("../utilities/utilities");
// Utilities
const { updateId, updateConfig } = require("../utilities/updateConfig");
const { updatePreset, addNewPreset, getPresets, removePreset } = require("../utilities/updatePresets");
@@ -67,27 +67,22 @@ 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) {
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) {
const port = 3010;
await updateConfig('CLIENT_PORT', port);
updateConfig('port', port);
runningClientConfig.port = port;
}
@@ -100,34 +95,21 @@ exports.checkConfig = async function checkConfig() {
exports.checkIn = async () => {
let reqOptions;
await this.checkConfig();
runningClientConfig.online = true;
// 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
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);
}
});
@@ -136,19 +118,7 @@ exports.checkIn = async () => {
// ID is in the config, checking in with the server
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) {

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
@@ -25,7 +25,7 @@ async def load_opus():
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)
@@ -33,8 +33,6 @@ def main(clientId='OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY',
@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)
@@ -57,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):
if (not args.NGThreshold):
args.NGThreshold = 50
print("Arguments:", args)
print("Arguments:", args)
main(
main(
clientId=args.clientId,
channelId=args.channelId,
NGThreshold=args.NGThreshold,
deviceId=args.deviceId,
presence=args.presence
)
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

@@ -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

@@ -27,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

@@ -13,6 +13,9 @@ ls -ld $SCRIPT_DIR | awk '{print $3}' >> ./config/installerName
useradd -M RadioNode
usermod -s -L RadioNode
# Create the .env file from the example
cp $SCRIPT_DIR/.env.example $SCRIPT_DIR/.env
# Change the ownership of the directory to the service user
chown RadioNode -R $SCRIPT_DIR

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 (method === "POST"){
log.VERBOSE("Hostname Vars: ", hostname, process.env.SERVER_HOSTNAME, process.env.SERVER_IP);
if (hostname) this.hostname = hostname;
if (process.env.SERVER_HOSTNAME) this.hostname = process.env.SERVER_HOSTNAME;
if (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");
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) => {
if (res.statusCode >= 200 && res.statusCode <= 299) {
const responseObject = {
"statusCode": res.statusCode,
"body": (isJsonString(data.toString())) ? JSON.parse(data.toString()) : data.toString()
"body": JSON.parse(data)
};
log.VERBOSE("Response Object: ", responseObject);
log.DEBUG("Response Object: ", responseObject);
callback(responseObject);
}
if (res.statusCode >= 300) {
const responseObject = {
"statusCode": res.statusCode,
"body": data
};
log.DEBUG("Response Object: ", responseObject);
callback(responseObject);
}
})
}).on('error', err => {
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

@@ -8,10 +8,10 @@ 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},`;
}
}
@@ -20,7 +20,7 @@ class Options {
* @param updatedId The updated ID assigned to the bot
*/
exports.updateId = (updatedId) => {
this.updateConfig('CLIENT_ID', updatedId);
this.updateConfig('id', updatedId);
}
/**
@@ -46,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

@@ -262,23 +262,3 @@ 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,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

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

View File

@@ -1,9 +1,9 @@
// Modules
const { SlashCommandBuilder } = require('discord.js');
const { customSlashCommandBuilder } = require('../utilities/customSlashCommandBuilder');
const { DebugBuilder } = require("../utilities/debugBuilder");
const { getMembersInRole, getAllClientIds, filterAutocompleteValues } = require("../utilities/utils");
const { getMembersInRole, getAllClientIds } = require("../utilities/utils");
const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
const { getOnlineNodes, updateNodeInfo, addNodeConnection, getConnectionByNodeId, getAllConnections } = require("../utilities/mysqlHandler");
const { getOnlineNodes, updateNodeInfo, addNodeConnection, getConnectionByNodeId } = require("../utilities/mysqlHandler");
// Global Vars
const log = new DebugBuilder("server", "join");
@@ -13,10 +13,10 @@ const log = new DebugBuilder("server", "join");
*
* @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 {*} 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, connections) {
async function joinServerWrapper(presetName, channelId, clientIdsUsed) {
// Get nodes online
var onlineNodes = await new Promise((recordResolve, recordReject) => {
getOnlineNodes((nodeRows) => {
@@ -25,7 +25,7 @@ async function joinServerWrapper(presetName, channelId, connections) {
});
// Check which nodes have the selected preset
onlineNodes = onlineNodes.filter(node => node.presets.includes(presetName));
onlineNodes = onlineNodes.filter(node => node.nearbySystems.includes(presetName));
log.DEBUG("Filtered Online Nodes: ", onlineNodes);
// Check if any nodes with this preset are available
@@ -45,16 +45,16 @@ async function joinServerWrapper(presetName, channelId, connections) {
log.DEBUG("All clients: ", Object.keys(availableClientIds));
var selectedClientId;
if (typeof connections === 'string') {
if (typeof clientIdsUsed === 'string') {
for (const availableClientId of availableClientIds) {
if (availableClientId.discordId != connections ) selectedClientId = availableClientId;
if (availableClientId.discordId != clientIdsUsed ) 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("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);
@@ -84,63 +84,33 @@ async function joinServerWrapper(presetName, channelId, connections) {
const nodeConnection = await addNodeConnection(selectedNode, selectedClientId);
log.DEBUG("Node Connection: ", nodeConnection);
});
return selectedClientId;
}
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);
});
});
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
options = [...new Set(presetsAvailable)];
log.DEBUG("DeDuped Presets available: ", options);
// 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,12 +1,13 @@
// Modules
const { SlashCommandBuilder } = require('discord.js');
const { customSlashCommandBuilder } = require('../utilities/customSlashCommandBuilder');
const { DebugBuilder } = require("../utilities/debugBuilder");
const { getAllClientIds, getKeyByArrayValue, filterAutocompleteValues } = require("../utilities/utils");
const { getAllClientIds, getKeyByArrayValue } = require("../utilities/utils");
const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
const { checkNodeConnectionByClientId, removeNodeConnectionByNodeId, getAllConnections } = require('../utilities/mysqlHandler');
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");
@@ -33,7 +34,7 @@ async function leaveServerWrapper(clientIdObject) {
exports.leaveServerWrapper = leaveServerWrapper;
module.exports = {
data: new SlashCommandBuilder()
data: new customSlashCommandBuilder()
.setName('leave')
.setDescription('Disconnect a bot from the server')
.addStringOption(option =>
@@ -47,12 +48,17 @@ module.exports = {
defaultTokenUsage: 0,
deferInitialReply: true,
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{
const guildId = interaction.guild.id;
const botName = interaction.options.getString('bot');
log.DEBUG("Bot Name: ", botName)
const clinetIds = await getAllClientIds();

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

@@ -25,7 +25,7 @@ exports.listAllNodes = async (req, res) => {
// 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
@@ -35,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) {
@@ -65,62 +65,30 @@ exports.getNodeInfo = async (req, res) => {
exports.nodeCheckIn = async (req, res) => {
if (!req.body.id) return res.status(400).json("No id specified");
getNodeInfoFromId(req.body.id, (nodeInfo) => {
let checkInObject = {};
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.body.id, checkInObject);
checkInObject._id = req.body.id;
checkInObject = new nodeObject(checkInObject);
if (!nodeInfo) {
log.WARN("No existing node found with this ID, adding node: ", checkInObject);
addNewNode(checkInObject, (newNode) => {
return res.status(201).json({"updatedKeys": newNode});
});
}
else {
// 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});
});
}
});
})
})
}
/**
@@ -132,7 +100,7 @@ exports.nodeCheckIn = async (req, res) => {
*/
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);
await joinServerWrapper(req.body.presetName, req.body.channelId, req.body.clientId)
}
/**
@@ -140,7 +108,6 @@ exports.requestNodeJoinServer = async (req, res) => {
*/
exports.nodeMonitorService = class nodeMonitorService {
constructor() {
this.log = new DebugBuilder("server", "nodeMonitorService");
}
async start(){
@@ -163,21 +130,21 @@ exports.nodeMonitorService = class nodeMonitorService {
async checkInWithOnlineNodes(){
getOnlineNodes((nodes) => {
this.log.DEBUG("Online Nodes: ", nodes);
log.DEBUG("Online Nodes: ", nodes);
for (const node of nodes) {
const reqOptions = new requestOptions("/client/requestCheckIn", "GET", node.ip, node.port)
sendHttpRequest(reqOptions, "", (responseObj) => {
const request = sendHttpRequest(reqOptions, "", (responseObj) => {
if (responseObj) {
this.log.DEBUG("Response from: ", node.name, responseObj);
log.DEBUG("Response from: ", node.name, responseObj);
}
else {
this.log.DEBUG("No response from node, assuming it's offline");
log.DEBUG("No response from node, assuming it's offline");
const offlineNode = new nodeObject({ _online: 0, _id: node.id });
this.log.DEBUG("Offline node update object: ", offlineNode);
log.DEBUG("Offline node update object: ", offlineNode);
updateNodeInfo(offlineNode, (sqlResponse) => {
if (!sqlResponse) this.log.ERROR("No response from SQL object");
if (!sqlResponse) log.ERROR("No response from SQL object");
this.log.DEBUG("Updated offline node: ", sqlResponse);
log.DEBUG("Updated node: ", sqlResponse);
})
}
})

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;
@@ -481,10 +481,10 @@ exports.PostStorage = class PostStorage extends Storage {
savePost(_postObject, callback){
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

@@ -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

@@ -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

@@ -110,16 +110,17 @@ class nodeObject {
* @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._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.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
@@ -118,19 +117,3 @@ exports.getClientObjectByClientID = (clientId) => {
}
return undefined
}
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 })),
);
}

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)
---
## 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.
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.
#### [Read more about the Client](https://git.vpn.cusano.net/logan/DRB-CnC/src/branch/master/Client)
# Discord Radio Bot: Command & Control
---
## Troubleshooting
Check the [wiki](https://git.vpn.cusano.net/logan/DRB-CnC/wiki)
Project overview here
## Requirements Overview
---
### Server Requirements
#### Server: Discord Bot Requirements
### Client Requirements
#### Client: Discord Bot Requirements
## Server
---
Explanation and overview here
## Client
---
Explanation and overview here
## Discord Bot
---
Explanation and overview here