Compare commits
195 Commits
1316c109e9
...
master
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
ca2815ab8f | ||
|
|
556697725a | ||
|
|
b448f04aec | ||
|
|
fae8417b2f | ||
|
|
e06cc4762d | ||
|
|
6deba2bad2 | ||
|
|
e0d1a4a2fe | ||
| 1078faa766 | |||
|
|
75580c0547 | ||
|
|
880f1ccb01 | ||
|
|
76c4d002a0 | ||
|
|
2260deee01 | ||
|
|
8a0baa5bc9 | ||
|
|
ec091c0017 | ||
|
|
a5996ccfc0 | ||
|
|
3b248e36ec | ||
|
|
abdb725964 | ||
|
|
167f87128e | ||
|
|
bc09840dda | ||
|
|
c680c8fb2c | ||
|
|
4ceb71bd84 | ||
|
|
4b86621626 | ||
|
|
d847aa4fc7 | ||
|
|
9ff87403b2 | ||
|
|
cf9deb4841 | ||
|
|
58b4b7ff40 | ||
|
|
6b4ffc88b3 | ||
|
|
0f114066a6 | ||
|
|
648782658c | ||
|
|
d7ea6bbbd4 | ||
|
|
6ffa12911a | ||
|
|
61d7b69c10 | ||
|
|
c14316933b | ||
|
|
f55361575e | ||
|
|
c5f7cc1da6 | ||
|
|
02854fb783 | ||
|
|
4a54be7e51 | ||
|
|
cfeea57744 | ||
|
|
0a8dc75a93 | ||
|
|
0426f5eb27 | ||
|
|
d4b974f81b | ||
|
|
d05c266f75 | ||
|
|
57fa6be110 | ||
|
|
f5d58d45da | ||
|
|
62c0504028 | ||
|
|
5dd27f0bed | ||
|
|
e0bae665ed | ||
|
|
598c802b28 | ||
|
|
ace762fc76 | ||
|
|
75156d059e | ||
|
|
abb833840a | ||
|
|
11c8a149bb | ||
|
|
9c111eda1a | ||
|
|
31de3a040d | ||
|
|
318ee7bf91 | ||
|
|
5428ac6144 | ||
|
|
e27dd9d9cb | ||
|
|
c0927601b9 | ||
|
|
ef45cf6539 | ||
|
|
23bea5f74e | ||
|
|
fc743cbb46 | ||
|
|
e522326576 | ||
|
|
2e22fa66a6 | ||
|
|
6b37a48061 | ||
|
|
5d6d86fa47 | ||
|
|
c35d3f3fa7 | ||
|
|
c38bca4144 | ||
|
|
e6332dffc9 | ||
|
|
60b6eb7cda | ||
|
|
eec80f7239 | ||
|
|
a58f314475 | ||
|
|
a603a53602 | ||
| 7a0664ad0c | |||
| 8403c2e391 | |||
|
|
c9890c7cc8 | ||
|
|
bec4c2d743 | ||
|
|
45258d5e4b | ||
|
|
ffe9c413ba | ||
|
|
959cfdf7af | ||
|
|
97acfc312c | ||
|
|
c6cdc0809c | ||
|
|
851a9c55fa | ||
|
|
fb9df3230e | ||
|
|
7163df21e9 | ||
|
|
47d18c494d | ||
|
|
ea2dbd9fb0 | ||
|
|
9ce77a5be0 | ||
|
|
57881a7e17 | ||
|
|
e350cd6030 | ||
|
|
fba0a2a2b2 | ||
|
|
83cef63109 | ||
|
|
2390bdc2c6 | ||
|
|
93be4ca9dc | ||
|
|
d96e6ad519 | ||
|
|
b180f90973 | ||
|
|
fd7435c7bc | ||
|
|
e062cf5794 | ||
|
|
597546b73d | ||
|
|
333e7420f4 | ||
|
|
37a03c5cc6 | ||
|
|
d2e9f286e2 | ||
|
|
255b1282ec | ||
|
|
878e64fa42 | ||
|
|
7a040a8e97 | ||
|
|
8dffeccf83 | ||
|
|
2108a3b92b | ||
|
|
960b801dd2 | ||
|
|
dd5b442377 | ||
|
|
c5a7131063 | ||
|
|
5d54f07af4 | ||
|
|
24faa5279d | ||
|
|
79d2ca1887 | ||
|
|
c2b4b4bfa1 | ||
|
|
d8a697e583 | ||
|
|
44caa11f7c | ||
|
|
dc92b07426 | ||
|
|
92f4caad0c | ||
|
|
b888a9233d | ||
|
|
b4e27162aa | ||
|
|
bfda15866e | ||
|
|
f4475dc9d7 | ||
|
|
c4650a9e99 | ||
|
|
f5e119d845 | ||
|
|
e8d68b2da7 | ||
|
|
041e0d485d | ||
|
|
fc11324714 | ||
|
|
c6c048c919 | ||
|
|
8ab611836b | ||
| 7d8ad68e27 | |||
| 200ca9c926 | |||
|
|
ff8e86cc3a | ||
|
|
6b12c3e3df | ||
|
|
fa2f28207e | ||
|
|
5c8414b4d8 | ||
|
|
edaeb261f7 | ||
|
|
c31ccff5ca | ||
|
|
d2186e9471 | ||
|
|
07743cf8a3 | ||
|
|
18afa7c058 | ||
|
|
a5cff9ec7e | ||
|
|
9450b78bd4 | ||
|
|
5757c51fa3 | ||
|
|
fa91cbc887 | ||
|
|
7fbaf31335 | ||
|
|
0280cb5384 | ||
|
|
a298be40d5 | ||
|
|
43d60a748b | ||
|
|
51f517cae5 | ||
|
|
06cb2cc352 | ||
|
|
5ce525f2b5 | ||
|
|
69fdc63983 | ||
|
|
a9d3c33af2 | ||
|
|
3719fce86a | ||
|
|
ba927bae8c | ||
| 79fe542143 | |||
|
|
7512c8e1df | ||
|
|
c882fb63d3 | ||
|
|
7fc61bbf2e | ||
|
|
e1c2ce6484 | ||
|
|
c4070cc420 | ||
|
|
0f003f907e | ||
|
|
e7b802839e | ||
|
|
48999e0d63 | ||
|
|
2c25be1de7 | ||
|
|
cf04e37f89 | ||
|
|
d04cc8d5b1 | ||
|
|
4662f37a72 | ||
|
|
be34c5381b | ||
|
|
ed79403a9b | ||
| e0b6e567c1 | |||
| 205f285e0a | |||
|
|
4e67e21651 | ||
|
|
9b2d0c4bbb | ||
|
|
f77eb5444a | ||
|
|
177d25e54e | ||
|
|
6880c5952a | ||
|
|
a14c56b645 | ||
|
|
35b81758e3 | ||
|
|
7871b07113 | ||
|
|
6682d97156 | ||
|
|
7b2215e9da | ||
|
|
b0e52920a7 | ||
|
|
f3a4f25f85 | ||
| 6e8af5dbcc | |||
|
|
4fbed169ab | ||
|
|
d1a8059cb9 | ||
|
|
7965a1161d | ||
|
|
0cefdba00f | ||
|
|
95c99971a2 | ||
|
|
b248e7f40e | ||
|
|
546d9e8829 | ||
|
|
6b484ddda4 | ||
|
|
6f98e59b26 | ||
|
|
999affce93 | ||
|
|
2a3893b0e7 |
5
.gitignore
vendored
5
.gitignore
vendored
@@ -5,4 +5,7 @@ node_modules/
|
|||||||
# Development files
|
# Development files
|
||||||
*.log
|
*.log
|
||||||
*.txt
|
*.txt
|
||||||
|
*.env
|
||||||
|
*.wav
|
||||||
|
!requirements.txt
|
||||||
|
*testOP25Dir/
|
||||||
@@ -1 +0,0 @@
|
|||||||
DEBUG="client:*";
|
|
||||||
23
Client/.env.example
Normal file
23
Client/.env.example
Normal file
@@ -0,0 +1,23 @@
|
|||||||
|
DEBUG="client:*"
|
||||||
|
# Audio Config
|
||||||
|
AUDIO_DEVICE_ID=""
|
||||||
|
AUDIO_DEVICE_NAME=""
|
||||||
|
|
||||||
|
# Client Config
|
||||||
|
CLIENT_ID=0
|
||||||
|
CLIENT_NAME=""
|
||||||
|
CLIENT_IP=""
|
||||||
|
CLIENT_PORT=3010
|
||||||
|
CLIENT_LOCATION=""
|
||||||
|
CLIENT_ONLINE=true
|
||||||
|
|
||||||
|
# Configuration for the connection to the server
|
||||||
|
SERVER_IP=""
|
||||||
|
SERVER_HOSTNAME=""
|
||||||
|
SERVER_PORT=3000
|
||||||
|
|
||||||
|
# Configuration of the local OP25 application
|
||||||
|
#OP25_BIN_PATH=""
|
||||||
|
|
||||||
|
# Logfile location config
|
||||||
|
#LOG_LOCATION=""
|
||||||
1
Client/.gitignore
vendored
Normal file
1
Client/.gitignore
vendored
Normal file
@@ -0,0 +1 @@
|
|||||||
|
*radioPresets.json
|
||||||
@@ -5,37 +5,19 @@ var cookieParser = require('cookie-parser');
|
|||||||
var logger = require('morgan');
|
var logger = require('morgan');
|
||||||
var http = require('http');
|
var http = require('http');
|
||||||
require('dotenv').config();
|
require('dotenv').config();
|
||||||
const fs = require('fs');
|
|
||||||
const { DebugBuilder } = require("./utilities/debugBuilder");
|
const { DebugBuilder } = require("./utilities/debugBuilder");
|
||||||
const deployCommands = require('./utilities/deployCommands');
|
const { checkIn } = require("./controllers/clientController");
|
||||||
|
|
||||||
var indexRouter = require('./routes/index');
|
var indexRouter = require('./routes/index');
|
||||||
var botRouter = require('./routes/bot');
|
var botRouter = require('./routes/bot');
|
||||||
var clientRouter = require('./routes/client');
|
var clientRouter = require('./routes/client');
|
||||||
var radioRouter = require('./routes/radio');
|
var radioRouter = require('./routes/radio');
|
||||||
|
var { attachRadioSessionToRequest } = require('./controllers/radioController');
|
||||||
|
|
||||||
const log = new DebugBuilder("client", "app");
|
const log = new DebugBuilder("client", "app");
|
||||||
const {
|
|
||||||
Client,
|
|
||||||
Events,
|
|
||||||
Collection,
|
|
||||||
GatewayIntentBits,
|
|
||||||
MessageActionRow,
|
|
||||||
MessageButton
|
|
||||||
} = require('discord.js');
|
|
||||||
|
|
||||||
var app = express();
|
var app = express();
|
||||||
var discordToken = process.env.TOKEN;
|
var port = process.env.CLIENT_PORT || '3010';
|
||||||
var port = process.env.HTTP_PORT || '3010';
|
|
||||||
|
|
||||||
const discordClient = new Client({
|
|
||||||
intents: [
|
|
||||||
GatewayIntentBits.Guilds,
|
|
||||||
GatewayIntentBits.GuildMessages,
|
|
||||||
GatewayIntentBits.MessageContent,
|
|
||||||
GatewayIntentBits.GuildVoiceStates
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
// view engine setup
|
// view engine setup
|
||||||
app.set('views', path.join(__dirname, 'views'));
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
@@ -52,16 +34,13 @@ app.use(express.static(path.join(__dirname, 'public')));
|
|||||||
app.use('/', indexRouter);
|
app.use('/', indexRouter);
|
||||||
|
|
||||||
// Discord bot control route
|
// Discord bot control route
|
||||||
app.use('/bot', (req, res, next) => {
|
app.use('/bot', attachRadioSessionToRequest, botRouter);
|
||||||
req.discordClient = discordClient; // Add the discord client to bot requests to be used downstream
|
|
||||||
next();
|
|
||||||
}, botRouter);
|
|
||||||
|
|
||||||
// Local client control route
|
// Local client control route
|
||||||
app.use("/client", clientRouter);
|
app.use("/client", clientRouter);
|
||||||
|
|
||||||
// Local radio controller route
|
// Local radio controller route
|
||||||
app.use("/radio", radioRouter);
|
app.use("/radio", attachRadioSessionToRequest, radioRouter);
|
||||||
|
|
||||||
// catch 404 and forward to error handler
|
// catch 404 and forward to error handler
|
||||||
app.use((req, res, next) => {
|
app.use((req, res, next) => {
|
||||||
@@ -115,51 +94,8 @@ async function runHTTPServer() {
|
|||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
log.DEBUG(`Starting HTTP Server`);
|
||||||
// Discord bot config
|
|
||||||
|
|
||||||
// Setup commands for the Discord bot
|
|
||||||
discordClient.commands = new Collection();
|
|
||||||
const commandsPath = path.join(__dirname, 'commands');
|
|
||||||
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
|
|
||||||
//const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));
|
|
||||||
for (const file of commandFiles) {
|
|
||||||
const filePath = path.join(commandsPath, file);
|
|
||||||
const command = require(filePath);
|
|
||||||
log.DEBUG("Importing command: ", command.data.name);
|
|
||||||
// Set a new item in the Collection
|
|
||||||
// With the key as the command name and the value as the exported module
|
|
||||||
discordClient.commands.set(command.data.name, command);
|
|
||||||
}
|
|
||||||
|
|
||||||
// Run when the bot is ready
|
|
||||||
discordClient.on('ready', () => {
|
|
||||||
log.DEBUG(`Discord server up and running with client: ${discordClient.user.tag}`);
|
|
||||||
log.INFO(`Logged in as ${discordClient.user.tag}!`);
|
|
||||||
|
|
||||||
// Deploy slash commands
|
|
||||||
log.DEBUG("Deploying slash commands");
|
|
||||||
deployCommands.deploy(discordClient.user.id, discordClient.guilds.cache.map(guild => guild.id));
|
|
||||||
|
|
||||||
log.DEBUG(`Starting HTTP Server`);
|
|
||||||
runHTTPServer();
|
runHTTPServer();
|
||||||
});
|
|
||||||
|
|
||||||
// Setup any additional event handlers
|
log.DEBUG("Checking in with the master server")
|
||||||
const eventsPath = path.join(__dirname, 'events');
|
checkIn(true);
|
||||||
if (fs.existsSync(eventsPath)) {
|
|
||||||
const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js'));
|
|
||||||
if (eventFiles.length > 0) {
|
|
||||||
for (const file of eventFiles) {
|
|
||||||
const filePath = path.join(eventsPath, file);
|
|
||||||
const event = require(filePath);
|
|
||||||
if (event.once) {
|
|
||||||
discordClient.once(event.name, (...args) => event.execute(...args));
|
|
||||||
} else {
|
|
||||||
discordClient.on(event.name, (...args) => event.execute(...args));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
discordClient.login(discordToken); //Load Client Discord Token
|
|
||||||
|
|||||||
@@ -1,76 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module dependencies.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const app = require('../app');
|
|
||||||
// Debug
|
|
||||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
|
||||||
const log = new DebugBuilder("client", "www");
|
|
||||||
const http = require('http');
|
|
||||||
const config = require('../config/clientConfig');
|
|
||||||
const clientController = require('../controllers/clientController');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get port from environment and store in Express.
|
|
||||||
*/
|
|
||||||
|
|
||||||
app.set('port', config.clientConfig.port);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create HTTP server.
|
|
||||||
*/
|
|
||||||
|
|
||||||
const server = http.createServer(app);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listen on provided port, on all network interfaces.
|
|
||||||
*/
|
|
||||||
|
|
||||||
server.listen(config.clientConfig.port);
|
|
||||||
server.on('error', onError);
|
|
||||||
server.on('listening', onListening);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event listener for HTTP server "error" event.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function onError(error) {
|
|
||||||
if (error.syscall !== 'listen') {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
const bind = typeof port === 'string'
|
|
||||||
? 'Pipe ' + port
|
|
||||||
: 'Port ' + port;
|
|
||||||
|
|
||||||
// handle specific listen errors with friendly messages
|
|
||||||
switch (error.code) {
|
|
||||||
case 'EACCES':
|
|
||||||
console.error(bind + ' requires elevated privileges');
|
|
||||||
process.exit(1);
|
|
||||||
break;
|
|
||||||
case 'EADDRINUSE':
|
|
||||||
console.error(bind + ' is already in use');
|
|
||||||
process.exit(1);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event listener for HTTP server "listening" event.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function onListening() {
|
|
||||||
const addr = server.address();
|
|
||||||
const bind = typeof addr === 'string'
|
|
||||||
? 'pipe ' + addr
|
|
||||||
: 'port ' + addr.port;
|
|
||||||
log.DEBUG('Listening on ' + bind);
|
|
||||||
|
|
||||||
// check in with the server to add this node or come back online
|
|
||||||
clientController.checkIn();
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
// Debug
|
|
||||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
|
||||||
const log = new DebugBuilder("client", "ping");
|
|
||||||
// Modules
|
|
||||||
const { SlashCommandBuilder } = require('discord.js');
|
|
||||||
const { join } = require("../controllers/commandController")
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
data: new SlashCommandBuilder()
|
|
||||||
.setName('join')
|
|
||||||
.setDescription('Join a voice channel'),
|
|
||||||
example: "join",
|
|
||||||
isPrivileged: false,
|
|
||||||
async execute(interaction) {
|
|
||||||
await join({ interaction: interaction });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
// Debug
|
|
||||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
|
||||||
const log = new DebugBuilder("client-bot", "leave");
|
|
||||||
// Modules
|
|
||||||
const { SlashCommandBuilder } = require('discord.js');
|
|
||||||
const { leave } = require("../controllers/commandController")
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
data: new SlashCommandBuilder()
|
|
||||||
.setName('leave')
|
|
||||||
.setDescription('Leave a voice channel'),
|
|
||||||
example: "leave",
|
|
||||||
isPrivileged: false,
|
|
||||||
async execute(interaction) {
|
|
||||||
await leave({ interaction: interaction })
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,19 +0,0 @@
|
|||||||
// Debug
|
|
||||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
|
||||||
const log = new DebugBuilder("client-bot", "status");
|
|
||||||
// Modules
|
|
||||||
const { status } = require('../controllers/commandController');
|
|
||||||
// Utilities
|
|
||||||
|
|
||||||
const { SlashCommandBuilder } = require('discord.js');
|
|
||||||
|
|
||||||
module.exports = {
|
|
||||||
data: new SlashCommandBuilder()
|
|
||||||
.setName('status')
|
|
||||||
.setDescription('Check the status of the bot'),
|
|
||||||
example: "status",
|
|
||||||
isPrivileged: false,
|
|
||||||
async execute(interaction) {
|
|
||||||
await status({ interaction: interaction });
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
{
|
|
||||||
"ApplicationID": "943742040255115304",
|
|
||||||
"GuildID": "367396189529833472",
|
|
||||||
"DeviceID": "5",
|
|
||||||
"DeviceName": "VoiceMeeter Aux Output (VB-Audi"
|
|
||||||
}
|
|
||||||
@@ -1,23 +0,0 @@
|
|||||||
// Core config settings for the node, these are the settings that are checked with the server
|
|
||||||
const path = require("path");
|
|
||||||
exports.clientConfig = {
|
|
||||||
"id": 13,
|
|
||||||
"name": "boilin balls in the hall",
|
|
||||||
"ip": "172.16.100.150",
|
|
||||||
"port": 3001,
|
|
||||||
"location": "the house",
|
|
||||||
"nearbySystems": ["Westchester Cty. Simulcast"],
|
|
||||||
"online": true
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configuration for the connection to the server
|
|
||||||
exports.serverConfig = {
|
|
||||||
"ip": "127.0.0.1",
|
|
||||||
"hostname": "localhost",
|
|
||||||
"port": 3000
|
|
||||||
}
|
|
||||||
|
|
||||||
// Configuration of the local OP25 application
|
|
||||||
exports.radioAppConfig = {
|
|
||||||
"bin": "H:/Logan/Projects/Discord-Radio-Bot-CnC/Client/.idea/testOP25Dir/multi_rx.py"
|
|
||||||
}
|
|
||||||
@@ -1,16 +0,0 @@
|
|||||||
// Core config settings for the node, these are the settings that are checked with the server
|
|
||||||
exports.nodeConfig = {
|
|
||||||
"id": 0,
|
|
||||||
"name": "",
|
|
||||||
"ip": "",
|
|
||||||
"port": 0,
|
|
||||||
"location": "",
|
|
||||||
"nearbySystems": {
|
|
||||||
"System Name": {
|
|
||||||
"frequencies": [],
|
|
||||||
"mode": "",
|
|
||||||
"trunkFile": ""
|
|
||||||
}
|
|
||||||
},
|
|
||||||
"online": false
|
|
||||||
}
|
|
||||||
@@ -1 +0,0 @@
|
|||||||
{"Westchester Cty. Simulcast":{"frequencies":[470575000,470375000,470525000,470575000,470550000],"mode":"p25","trunkFile":"trunk.tsv"},"coppies":{"frequencies":[154875000],"mode":"nbfm","trunkFile":"none"}}
|
|
||||||
18
Client/config/radioPresets.json.EXAMPLE
Normal file
18
Client/config/radioPresets.json.EXAMPLE
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
{
|
||||||
|
"Default P25 System Name": {
|
||||||
|
"frequencies": [
|
||||||
|
155344000,
|
||||||
|
155444000,
|
||||||
|
155555000
|
||||||
|
],
|
||||||
|
"mode": "p25",
|
||||||
|
"trunkFile": "trunk.tsv"
|
||||||
|
},
|
||||||
|
"Default NBFM System": {
|
||||||
|
"frequencies": [
|
||||||
|
154690000
|
||||||
|
],
|
||||||
|
"mode": "nbfm",
|
||||||
|
"trunkFile": "none"
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,61 +0,0 @@
|
|||||||
// Config
|
|
||||||
const { getDeviceID } = require('../utilities/configHandler.js');
|
|
||||||
// Modules
|
|
||||||
const alsaInstance = require('alsa-capture');
|
|
||||||
const { returnAlsaDeviceObject } = require("../utilities/executeConsoleCommands.js");
|
|
||||||
// Debug
|
|
||||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
|
||||||
// Global Vars
|
|
||||||
const log = new DebugBuilder("client-bot", "audioController");
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Checks to make sure the selected audio device is available and returns the device object (PortAudio Device Info)
|
|
||||||
* At least one option must be supplied, it will prefer ID to device name
|
|
||||||
*
|
|
||||||
* @param deviceName The name of the device being queried
|
|
||||||
* @param deviceId The ID of the device being queried
|
|
||||||
* @returns {unknown}
|
|
||||||
*/
|
|
||||||
async function confirmAudioDevice({deviceName = undefined, deviceId = undefined}){
|
|
||||||
const deviceList = await getAudioDevices();
|
|
||||||
if (!deviceName && !deviceId) throw new Error("No device given");
|
|
||||||
if (deviceId) return deviceList.find(device => device.id === deviceId);
|
|
||||||
if (deviceName) return deviceList.find(device => device.name === deviceName);
|
|
||||||
}
|
|
||||||
exports.confirmAudioDevice = confirmAudioDevice;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Return a list of the audio devices connected with input channels
|
|
||||||
*
|
|
||||||
* @returns {unknown[]}
|
|
||||||
*/
|
|
||||||
async function getAudioDevices(){
|
|
||||||
// Exec output contains both stderr and stdout outputs
|
|
||||||
const deviceList = await returnAlsaDeviceObject();
|
|
||||||
log.DEBUG("Device list: ", deviceList);
|
|
||||||
|
|
||||||
return deviceList;
|
|
||||||
}
|
|
||||||
exports.getAudioDevices = getAudioDevices;
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create and return the audio instance from the saved settings
|
|
||||||
* TODO Allow the client to save and load these settings dynamically
|
|
||||||
*
|
|
||||||
* @returns new portAudio.AudioIO
|
|
||||||
*/
|
|
||||||
async function createAudioInstance() {
|
|
||||||
const selectedDevice = await confirmAudioDevice({deviceId: getDeviceID()});//{deviceName: "VoiceMeeter VAIO3 Output (VB-Au"});
|
|
||||||
log.DEBUG("Device selected from config: ", selectedDevice);
|
|
||||||
// Create an instance of AudioIO with outOptions (defaults are as below), which will return a WritableStream
|
|
||||||
return new alsaInstance({
|
|
||||||
channels: 2,
|
|
||||||
format: "S16_BE",
|
|
||||||
rate: 48000,
|
|
||||||
device: selectedDevice.name ?? "default", // Use -1 or omit the deviceId to select the default device
|
|
||||||
periodSize: 100, //(48000 / 1000) * 20, //(48000 * 16 * 2) / 1000 * 20 // (48000 * (16 / 8) * 2) / 60 / 1000 * 20 //0.025 * 48000 / 2
|
|
||||||
periodTime: undefined,
|
|
||||||
// highwaterMark: 3840
|
|
||||||
});
|
|
||||||
}
|
|
||||||
exports.createAudioInstance = createAudioInstance;
|
|
||||||
@@ -1,98 +1,159 @@
|
|||||||
// Debug
|
// Debug
|
||||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||||
const log = new DebugBuilder("client", "clientController");
|
const log = new DebugBuilder("client", "botController");
|
||||||
|
const botLog = new DebugBuilder("client", "botController:bot");
|
||||||
// Modules
|
// Modules
|
||||||
const { status, join, leave } = require("./commandController")
|
const spawn = require('child_process').spawn;
|
||||||
|
const { resolve } = require("path");
|
||||||
|
require('dotenv').config();
|
||||||
|
const { closeProcessWrapper } = require("../utilities/utilities");
|
||||||
|
|
||||||
/**
|
// Global vars
|
||||||
* Get an object of client guilds
|
let pythonProcess;
|
||||||
* @param req The express request which includes the discord client
|
let recordingProcess;
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
function getGuilds(req) {
|
|
||||||
return req.discordClient.guilds.cache.map(guild => guild.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get an object of the channels in a guild
|
|
||||||
* @param {*} guildId The Guild ID to check the channels of
|
|
||||||
* @param {*} req The request object to use to check the discord client
|
|
||||||
*/
|
|
||||||
function getChannels(guildId, req) {
|
|
||||||
const guild = req.discordClient.guilds.find(guildId);
|
|
||||||
log.DEBUG("Found Guild channels with guild", guild.channels, guild);
|
|
||||||
return guild.channels;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check to see if a given guild has a given channel
|
|
||||||
* @param {*} guildId The guild ID to check if the channel exists
|
|
||||||
* @param {*} channelId The channel ID to check if exists in the guild
|
|
||||||
* @param {*} req The express request param to use the discord client
|
|
||||||
* @returns {true|false}
|
|
||||||
*/
|
|
||||||
function checkIfGuildHasChannel(guildId, channelId, req){
|
|
||||||
const guildChannels = getChannels(guildId, req)
|
|
||||||
const checkedChannel = guildChannels.find(c => c.id === channelId);
|
|
||||||
if (!checkedChannel) return false;
|
|
||||||
|
|
||||||
return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGuildFromChannel(channelId, req){
|
|
||||||
const channel = req.discordClient.channels.cache.get(channelId);
|
|
||||||
|
|
||||||
if (!channel) return new Error("Error getting channel from client");
|
|
||||||
|
|
||||||
if (channel.guild) return channel.guild;
|
|
||||||
|
|
||||||
return new Error("No Guild found with the given ID");
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get Status of the discord process
|
* Get Status of the discord process
|
||||||
*/
|
*/
|
||||||
exports.getStatus = (req, res) => {
|
exports.getStatus = (req, res) => {
|
||||||
log.INFO("Getting the status of the bot");
|
log.INFO("Getting the status of the bot");
|
||||||
guildIds = getGuilds(req);
|
if (pythonProcess) return res.sendStatus(200);
|
||||||
log.DEBUG("Guild IDs: ", guildIds);
|
return res.sendStatus(201);
|
||||||
var guildStatuses = []
|
|
||||||
for (const guildId of guildIds){
|
|
||||||
status({guildID: guildId, callback: (statusObj) => {
|
|
||||||
log.DEBUG("Status Object string: ", statusObj);
|
|
||||||
guildStatuses.push(statusObj);
|
|
||||||
}});
|
|
||||||
}
|
|
||||||
return res.status(200).json(guildStatuses);
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Start the bot and join the server and preset specified
|
* Start the bot and join the server and preset specified
|
||||||
*/
|
*/
|
||||||
exports.joinServer = (req, res) => {
|
exports.joinServer = async (req, res) => {
|
||||||
const channelId = req.body.channelID;
|
if (!req.body.clientId || !req.body.channelId) return res.status(500).json({"message": "You must include the client ID (discord token), channel ID (The discord ID of the channel to connect to)"});
|
||||||
|
const deviceId = process.env.AUDIO_DEVICE_ID;
|
||||||
|
const channelId = req.body.channelId;
|
||||||
|
const clientId = req.body.clientId;
|
||||||
const presetName = req.body.presetName;
|
const presetName = req.body.presetName;
|
||||||
const guildObj = getGuildFromChannel(channelId, req);
|
const NGThreshold = req.body.NGThreshold ?? 50
|
||||||
|
|
||||||
if (!channelId || !presetName || !guildObj) return res.status(400).json({'message': "Request does not have all components to proceed"});
|
// Joining the discord server
|
||||||
|
log.INFO("Join requested to: ", deviceId, channelId, clientId, presetName, NGThreshold);
|
||||||
// join the sever
|
if (process.platform === "win32") {
|
||||||
join({guildID: guildObj.id, guildObj: guildObj, channelID: channelId, callback: () => {
|
log.DEBUG("Starting Windows Python");
|
||||||
return res.sendStatus(202);
|
pythonProcess = await spawn('python.exe', [resolve(__dirname, "../pdab/main.py"), deviceId, channelId, clientId, '-n', NGThreshold, '-p', presetName ], { 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/") });
|
||||||
|
}
|
||||||
|
|
||||||
|
log.VERBOSE("Python Process: ", pythonProcess);
|
||||||
|
|
||||||
|
let fullOutput;
|
||||||
|
pythonProcess.stdout.setEncoding('utf8');
|
||||||
|
pythonProcess.stdout.on("data", (data) => {
|
||||||
|
botLog.VERBOSE("From Process: ", data);
|
||||||
|
fullOutput += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
pythonProcess.stderr.on('data', (data) => {
|
||||||
|
botLog.VERBOSE(`stderr: ${data}`);
|
||||||
|
fullOutput += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
pythonProcess.on('close', (code) => {
|
||||||
|
log.DEBUG(`child process exited with code ${code}`);
|
||||||
|
log.VERBOSE("Full output from bot: ", fullOutput);
|
||||||
|
});
|
||||||
|
|
||||||
|
pythonProcess.on("error", (code, signal) => {
|
||||||
|
log.ERROR("Error from the discord bot process: ", code, signal);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Starting the radio application
|
||||||
|
|
||||||
|
return res.sendStatus(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Leaves the server if it's in one
|
* Leaves the server if it's in one
|
||||||
*/
|
*/
|
||||||
exports.leaveServer = (req, res) => {
|
exports.leaveServer = async (req, res) => {
|
||||||
log.INFO("Leaving the server");
|
log.INFO("Leaving the server");
|
||||||
const guildIds = getGuilds(req);
|
if (!pythonProcess) return res.sendStatus(200)
|
||||||
log.DEBUG("Guild IDs: ", guildIds);
|
|
||||||
for (const guildId of guildIds){
|
pythonProcess = await closeProcessWrapper(pythonProcess);
|
||||||
leave({guildID: guildId, callback: (response) => {
|
|
||||||
log.DEBUG("Response from leaving server on guild ID", guildId, response);
|
|
||||||
}});
|
|
||||||
}
|
|
||||||
return res.sendStatus(202);
|
return res.sendStatus(202);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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);
|
||||||
|
});
|
||||||
}
|
}
|
||||||
@@ -1,13 +1,18 @@
|
|||||||
// Debug
|
// Debug
|
||||||
// const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||||
// const log = new DebugBuilder("client", "clientController");
|
const log = new DebugBuilder("client", "clientController");
|
||||||
// Configs
|
// Configs
|
||||||
const config = require("../config/clientConfig");
|
require('dotenv').config();
|
||||||
const modes = require("../config/modes");
|
const modes = require("../config/modes");
|
||||||
|
// Modules
|
||||||
|
const { executeAsyncConsoleCommand, BufferToJson, nodeObject } = require("../utilities/utilities");
|
||||||
// Utilities
|
// Utilities
|
||||||
const updateConfig = require("../utilities/updateConfig");
|
const { getFullConfig } = require("../utilities/configHandler");
|
||||||
const updatePreset = require("../utilities/updatePresets");
|
const { updateId, updateConfig, updateClientConfig } = require("../utilities/updateConfig");
|
||||||
const requests = require("../utilities/httpRequests");
|
const { updatePreset, addNewPreset, getPresets, removePreset } = require("../utilities/updatePresets");
|
||||||
|
const { onHttpError, requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
|
||||||
|
|
||||||
|
var runningClientConfig = getFullConfig()
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Check the body for the required fields to update or add a preset
|
* Check the body for the required fields to update or add a preset
|
||||||
@@ -17,11 +22,11 @@ const requests = require("../utilities/httpRequests");
|
|||||||
* @returns {*}
|
* @returns {*}
|
||||||
*/
|
*/
|
||||||
function checkBodyForPresetFields(req, res, callback) {
|
function checkBodyForPresetFields(req, res, callback) {
|
||||||
if (!req.body?.systemName) return res.status(403).json({"message": "No system 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?.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?.mode && typeof req.body.mode === "string") return res.status(403).json({ "message": "No mode in the request" });
|
||||||
if (!req.body?.trunkFile) {
|
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)
|
// 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";
|
req.body.trunkFile = req.body.trunkFile ?? "none";
|
||||||
}
|
}
|
||||||
@@ -29,36 +34,142 @@ function checkBodyForPresetFields(req, res, callback) {
|
|||||||
return callback();
|
return callback();
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Check in with the server
|
async function checkLocalIP() {
|
||||||
* If the bot has a saved ID, check in with the server to update any information or just check back in
|
if (process.platform === "win32") {
|
||||||
* If the bot does not have a saved ID, it will attempt to request a new ID from the server
|
// Windows
|
||||||
*/
|
var networkConfig = await executeAsyncConsoleCommand("ipconfig");
|
||||||
exports.checkIn = async () => {
|
log.DEBUG('Network Config: ', networkConfig);
|
||||||
let reqOptions;
|
var networkConfigLines = await networkConfig.split("\n").filter(line => {
|
||||||
// Check if there is an ID found, if not add the node to the server. If there was an ID, check in with the server to make sure it has the correct information
|
if (!line.includes(":")) return false;
|
||||||
if (config.clientConfig.id === 0) {
|
|
||||||
// ID was not found in the config, creating a new node
|
line = line.split(":");
|
||||||
reqOptions = new requests.requestOptions("/nodes/newNode", "POST");
|
|
||||||
delete config.clientConfig.id;
|
if (!line.length === 2) return false;
|
||||||
requests.sendHttpRequest(reqOptions, JSON.stringify(config.clientConfig), (responseObject) => {
|
|
||||||
// Update the client's ID if the server accepted it
|
return true;
|
||||||
if (responseObject.statusCode === 202) {
|
}).map(line => {
|
||||||
config.clientConfig.id = responseObject.body.nodeId;
|
line = String(line).split(':', 2);
|
||||||
updateConfig.updateId(responseObject.body.nodeId);
|
line[0] = String(line[0]).replace(/[.]|[\s]/g, "").trim();
|
||||||
}
|
line[1] = String(line[1]).replace(/(\\r|\\n)/g, "").trim();
|
||||||
|
return line;
|
||||||
});
|
});
|
||||||
|
networkConfig = Object.fromEntries(networkConfigLines);
|
||||||
|
log.DEBUG("Parsed IP Config Results: ", networkConfig);
|
||||||
|
log.DEBUG("Local IP address: ", networkConfig['IPv4Address']);
|
||||||
|
return networkConfig['IPv4Address'];
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// ID is in the config, checking in with the server
|
// Linux
|
||||||
reqOptions = new requests.requestOptions("/nodes/nodeCheckIn", "POST");
|
var networkConfig = await executeAsyncConsoleCommand("ip addr");
|
||||||
requests.sendHttpRequest(reqOptions, JSON.stringify(config.clientConfig), (responseObject) => {
|
}
|
||||||
if (responseObject.statusCode === 202) {
|
}
|
||||||
// Server accepted an update
|
|
||||||
}
|
/**
|
||||||
if (responseObject.statusCode === 200) {
|
* Checks the config file for all required fields or gets and updates the required fields
|
||||||
// Server accepted the response but there were no keys to be updated
|
*/
|
||||||
}
|
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);
|
||||||
|
runningClientConfig.ip = ipAddr;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!runningClientConfig.name) {
|
||||||
|
const lastOctet = await String(checkLocalIP()).spit('.')[-1];
|
||||||
|
const name = `Radio-Node-${lastOctet}`;
|
||||||
|
await updateConfig('CLIENT_NAME', name);
|
||||||
|
runningClientConfig.name = name;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!runningClientConfig.port) {
|
||||||
|
const port = 3010;
|
||||||
|
await updateConfig('CLIENT_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 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) => {
|
||||||
|
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) {
|
||||||
|
// 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
|
||||||
|
if (responseObject.statusCode === 202) {
|
||||||
|
runningClientConfig.id = responseObject.body.nodeId;
|
||||||
|
log.DEBUG("Response object from new node: ", responseObject, runningClientConfig);
|
||||||
|
await updateId(runningClientConfig.id);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (responseObject.statusCode >= 300) {
|
||||||
|
// Server threw an error
|
||||||
|
log.DEBUG("HTTP Error: ", responseObject, await BufferToJson(responseObject.body));
|
||||||
|
await 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");
|
||||||
|
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
|
||||||
|
onHttpError(responseObject.statusCode);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
log.ERROR("Error checking in: ", err);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -70,11 +181,30 @@ exports.requestCheckIn = async (req, res) => {
|
|||||||
return res.sendStatus(200);
|
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
|
/** Controller for the /client/presets endpoint
|
||||||
* This is the endpoint wrapper to get the presets object
|
* This is the endpoint wrapper to get the presets object
|
||||||
*/
|
*/
|
||||||
exports.getPresets = async (req, res) => {
|
exports.getPresets = async (req, res) => {
|
||||||
return res.status(200).json(updatePreset.getPresets());
|
runningClientConfig.nearbySystems = getPresets();
|
||||||
|
return res.status(200).json(runningClientConfig.nearbySystems);
|
||||||
}
|
}
|
||||||
|
|
||||||
/** Controller for the /client/updatePreset endpoint
|
/** Controller for the /client/updatePreset endpoint
|
||||||
@@ -82,9 +212,11 @@ exports.getPresets = async (req, res) => {
|
|||||||
*/
|
*/
|
||||||
exports.updatePreset = async (req, res) => {
|
exports.updatePreset = async (req, res) => {
|
||||||
checkBodyForPresetFields(req, res, () => {
|
checkBodyForPresetFields(req, res, () => {
|
||||||
updatePreset.updatePreset(req.body.systemName, () => {
|
updatePreset(req.body.systemName, () => {
|
||||||
|
runningClientConfig.nearbySystems = getPresets();
|
||||||
|
this.checkIn(true);
|
||||||
return res.sendStatus(200);
|
return res.sendStatus(200);
|
||||||
}, {frequencies: req.body.frequencies, mode: req.body.mode, trunkFile: req.body.trunkFile});
|
}, { frequencies: req.body.frequencies, mode: req.body.mode, trunkFile: req.body.trunkFile });
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -93,10 +225,32 @@ exports.updatePreset = async (req, res) => {
|
|||||||
*/
|
*/
|
||||||
exports.addNewPreset = async (req, res) => {
|
exports.addNewPreset = async (req, res) => {
|
||||||
checkBodyForPresetFields(req, res, () => {
|
checkBodyForPresetFields(req, res, () => {
|
||||||
updatePreset.addNewPreset(req.body.systemName, req.body.frequencies, req.body.mode, () => {
|
addNewPreset(req.body.systemName, req.body.frequencies, req.body.mode, () => {
|
||||||
|
runningClientConfig.nearbySystems = getPresets();
|
||||||
|
this.checkIn(true);
|
||||||
return res.sendStatus(200);
|
return res.sendStatus(200);
|
||||||
}, req.body.trunkFile);
|
}, req.body.trunkFile);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Removes a preset from the client
|
||||||
|
*/
|
||||||
|
exports.removePreset = async (req, res) => {
|
||||||
|
checkBodyForPresetFields(req, res, () => {
|
||||||
|
if (!req.body.systemName) return res.status("500").json({ "message": "You must specify a system name to delete, this must match exactly to how the system name is saved." })
|
||||||
|
removePreset(req.body.systemName, () => {
|
||||||
|
runningClientConfig.nearbySystems = getPresets();
|
||||||
|
this.checkIn(true);
|
||||||
|
return res.sendStatus(200);
|
||||||
|
}, req.body.trunkFile);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Runs the updater service
|
||||||
|
*/
|
||||||
|
exports.updateClient = async (req, res) => {
|
||||||
|
await executeAsyncConsoleCommand("systemctl start RadioNodeUpdater.service");
|
||||||
|
return res.sendStatus(200);
|
||||||
|
}
|
||||||
|
|||||||
@@ -1,114 +0,0 @@
|
|||||||
// Debug
|
|
||||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
|
||||||
const log = new DebugBuilder("client-bot", "commandController");
|
|
||||||
// Modules
|
|
||||||
const { joinVoiceChannel, VoiceConnectionStatus, getVoiceConnection } = require("@discordjs/voice");
|
|
||||||
const { OpusEncoder } = require("@discordjs/opus");
|
|
||||||
// Utilities
|
|
||||||
const {replyToInteraction} = require("../utilities/messageHandler.js");
|
|
||||||
const {createAudioInstance} = require("../controllers/audioController.js");
|
|
||||||
|
|
||||||
// Declare the encoder
|
|
||||||
const encoder = new OpusEncoder(48000, 2);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Join the specified voice channel
|
|
||||||
*
|
|
||||||
* @param interaction Message interaction from discord
|
|
||||||
* @param {string||any} guildID The specified Guild ID if this function is run from the client instead of from an interaction in Discord
|
|
||||||
* @param {string||any} channelID The channel ID to join
|
|
||||||
* @param guild The guild object to be used to create a voice adapter
|
|
||||||
* @param {function} callback The callback that will be needed if this function is run with a Guild ID instead of an interaction
|
|
||||||
*/
|
|
||||||
exports.join = async function join({interaction= undefined, guildID= undefined, channelID = undefined, guildObj = undefined, callback = undefined}){
|
|
||||||
if (interaction){
|
|
||||||
const voiceChannel = interaction.options.getChannel('voicechannel');
|
|
||||||
channelID = voiceChannel.id;
|
|
||||||
guildID = interaction.guildId;
|
|
||||||
guildObj = interaction.guild;
|
|
||||||
if (interaction) replyToInteraction(interaction, `Ok, Joining ${voiceChannel.name}`);
|
|
||||||
}
|
|
||||||
log.DEBUG("Channel ID: ", channelID)
|
|
||||||
log.DEBUG("Guild ID: ", guildID)
|
|
||||||
|
|
||||||
const voiceConnection = joinVoiceChannel({
|
|
||||||
channelId: channelID,
|
|
||||||
guildId: guildID,
|
|
||||||
adapterCreator: guildObj.voiceAdapterCreator,
|
|
||||||
selfMute: false,
|
|
||||||
selfDeaf: false,
|
|
||||||
});
|
|
||||||
|
|
||||||
const audioInstance = await createAudioInstance();
|
|
||||||
|
|
||||||
audioInstance.on('audio', (buffer) => {
|
|
||||||
buffer = Buffer.from(buffer);
|
|
||||||
log.DEBUG("Audio buffer: ", buffer);
|
|
||||||
const encoded = encoder.encode(buffer);
|
|
||||||
// TODO Add a function here to check the volume of either buffer and only play audio to discord when there is audio to be played
|
|
||||||
voiceConnection.playOpusPacket(encoded);
|
|
||||||
})
|
|
||||||
|
|
||||||
// Exit the audio handler when the bot disconnects
|
|
||||||
voiceConnection.on(VoiceConnectionStatus.Destroyed, () => {
|
|
||||||
audioInstance.close();
|
|
||||||
})
|
|
||||||
|
|
||||||
if (guildID && callback) callback();
|
|
||||||
else return;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* If in a voice channel for the specified guild, leave
|
|
||||||
*
|
|
||||||
* @param interaction Message interaction from discord
|
|
||||||
* @param guildID
|
|
||||||
* @param callback
|
|
||||||
*/
|
|
||||||
exports.leave = async function leave({interaction = undefined, guildID= undefined, callback = undefined}) {
|
|
||||||
if(interaction) {
|
|
||||||
guildID = interaction.guild.id;
|
|
||||||
}
|
|
||||||
const voiceConnection = getVoiceConnection(guildID);
|
|
||||||
|
|
||||||
let response;
|
|
||||||
if (!voiceConnection){
|
|
||||||
response = "Not in a voice channel."
|
|
||||||
if (interaction) return replyToInteraction(interaction, response);
|
|
||||||
else callback(response);
|
|
||||||
}
|
|
||||||
voiceConnection.destroy();
|
|
||||||
|
|
||||||
response = "Goodbye"
|
|
||||||
if (interaction) return replyToInteraction(interaction, response);
|
|
||||||
else callback(response);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the voice status of the bots
|
|
||||||
* @param {*} param0
|
|
||||||
* @returns
|
|
||||||
*/
|
|
||||||
exports.status = async function status({interaction= undefined, guildID= undefined, callback = undefined}) {
|
|
||||||
//if (!interaction && !guildID) // Need error of sorts
|
|
||||||
if (interaction){
|
|
||||||
guildID = interaction.guild.id;
|
|
||||||
}
|
|
||||||
const voiceConnection = getVoiceConnection(guildID);
|
|
||||||
|
|
||||||
const statusObj = {
|
|
||||||
"guildID": guildID,
|
|
||||||
"voiceConnection": typeof g !== 'undefined' ? true : false // True if there is a voice connection, false if undefined
|
|
||||||
}
|
|
||||||
|
|
||||||
log.DEBUG('Status Object: ', statusObj);
|
|
||||||
|
|
||||||
// get the status and return it accordingly (message reply / module)
|
|
||||||
|
|
||||||
if (interaction) {
|
|
||||||
return replyToInteraction(interaction, "Pong! I have Aids and now you do too!");
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
callback(statusObj);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -2,146 +2,72 @@
|
|||||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||||
const log = new DebugBuilder("client", "radioController");
|
const log = new DebugBuilder("client", "radioController");
|
||||||
// Modules
|
// Modules
|
||||||
const { resolve, dirname } = require('path');
|
require('dotenv').config();
|
||||||
const fs = require('fs');
|
const { closeProcessWrapper, changeCurrentConfigWrapper, openRadioSessionWrapper } = require("../utilities/utilities");
|
||||||
const radioConfig = require('../config/clientConfig').radioAppConfig;
|
|
||||||
const radioConfigHelper = require("../utilities/radioConfigHelper");
|
|
||||||
const presetWrappers = require("../utilities/updatePresets");
|
|
||||||
const spawn = require('child_process').spawn;
|
|
||||||
const converter = require("convert-units");
|
|
||||||
|
|
||||||
let radioChildProcess, tempRes, radioConfigPath;
|
let radioChildProcess;
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Closes the radio executable if it's in one
|
* Closes the radio executable if it's in one
|
||||||
*/
|
*/
|
||||||
exports.closeRadioSession = (req, res) => {
|
exports.closeRadioSession = async (req, res) => {
|
||||||
if (!radioChildProcess) return res.sendStatus(200)
|
if (!radioChildProcess || !req.body.radioSession) return res.sendStatus(204);
|
||||||
tempRes = res;
|
if (radioChildProcess) radioChildProcess = await closeProcessWrapper(radioChildProcess);
|
||||||
radioChildProcess.kill();
|
if (req.body.radioSession) req.body.radioSession = await closeProcessWrapper(req.body.radioSession);
|
||||||
radioChildProcess = undefined;
|
if (!radioChildProcess) return res.sendStatus(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Change the current 'cfg.json' file to the preset specified
|
* Change the current 'cfg.json' file to the preset specified
|
||||||
* @param {string} presetName
|
* @param {string} presetName
|
||||||
*/
|
*/
|
||||||
exports.changeCurrentConfig = (req, res) => {
|
exports.changeCurrentConfig = async (req, res) => {
|
||||||
// Check if the given config is saved
|
const presetName = req.body.presetName;
|
||||||
log.DEBUG("[/radio/changeCurrentConfig] - Checking if provided preset is in the config");
|
if (!presetName) return res.status(500).json("You must include the preset name")
|
||||||
if (!checkIfPresetExists(req.body.presetName)) return res.status(500).JSON("No preset with given name found in config"); // No preset with the given name is in the config
|
|
||||||
|
|
||||||
// Check if the current config is the same as the preset given
|
const updatedConfigObject = await changeCurrentConfigWrapper(presetName);
|
||||||
const currentConfig = readOP25Config();
|
|
||||||
if (currentConfig.channels && currentConfig.channels.name === req.body.presetName) {
|
// No change was made to the config
|
||||||
log.DEBUG("[/radio/changeCurrentConfig] - Current config is the same as the preset given");
|
if (!updatedConfigObject) return res.sendStatus(200);
|
||||||
return res.sendStatus(202);
|
|
||||||
|
// Error was encountered
|
||||||
|
if (typeof updatedConfigObject === "string") return res.status(500).json(updatedConfigObject);
|
||||||
|
|
||||||
|
// There was a change made to the config, reopening the radio session if it was open
|
||||||
|
if (radioChildProcess) {
|
||||||
|
log.DEBUG("Radio session open, restarting to accept the new config");
|
||||||
|
const radioSessionResult = await openRadioSessionWrapper(radioChildProcess, presetName);
|
||||||
|
|
||||||
|
// throw an error to the client if the wrapper ran into an error
|
||||||
|
if (typeof radioSessionResult === "string") return res.status(500).json(updatedConfigObject);
|
||||||
}
|
}
|
||||||
|
|
||||||
// Convert radioPreset to OP25 'cfg.json. file
|
return res.sendStatus(202);
|
||||||
log.DEBUG("[/radio/changeCurrentConfig] - Converting radioPreset to OP25 config");
|
|
||||||
const updatedConfigObject = convertRadioPresetsToOP25Config(req.body.presetName);
|
|
||||||
|
|
||||||
// Replace current JSON file with the updated file
|
|
||||||
writeOP25Config(updatedConfigObject, () => {
|
|
||||||
res.sendStatus(200);
|
|
||||||
})
|
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Open a new OP25 process tuned to the specified system
|
* Open a new OP25 process tuned to the specified system
|
||||||
*/
|
*/
|
||||||
exports.openRadioSession = () => {
|
exports.openRadioSession = async (req, res) => {
|
||||||
if (radioChildProcess) closeRadioSession();
|
const presetName = req.body.presetName;
|
||||||
radioChildProcess = spawn(getRadioBinPath());
|
if(!presetName) return res.status(500).json({"message": "You must include the preset name to start the radio session with"})
|
||||||
|
|
||||||
|
radioChildProcess = await openRadioSessionWrapper(radioChildProcess, presetName);
|
||||||
|
|
||||||
|
// throw an error to the client if the wrapper ran into an error
|
||||||
|
if (typeof radioSessionResult === "string") return res.status(500).json(updatedConfigObject);
|
||||||
|
|
||||||
|
return res.sendStatus(200);
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Get the location of the 'multi_rx.py' binary from the config
|
* Attach the radio session to the request to be used elsewhere
|
||||||
|
*
|
||||||
|
* @param {*} req
|
||||||
|
* @param {*} res
|
||||||
*/
|
*/
|
||||||
function getRadioBinPath(){
|
exports.attachRadioSessionToRequest = async (req, res, next) => {
|
||||||
return resolve(radioConfig.bin);
|
req.body.radioSession = radioChildProcess;
|
||||||
}
|
next();
|
||||||
|
|
||||||
/**
|
|
||||||
* Write the given config to the JSON file in OP25 the bin dir
|
|
||||||
* @param config The full config to be written to the file
|
|
||||||
* @param {function} callback The function to be called when this wrapper completes
|
|
||||||
*/
|
|
||||||
function writeOP25Config(config, callback = undefined) {
|
|
||||||
log.DEBUG("Updating OP25 config with: ", config);
|
|
||||||
fs.writeFile(getRadioConfigPath(), JSON.stringify(config), (err) => {
|
|
||||||
// Error checking
|
|
||||||
if (err) {
|
|
||||||
log.ERROR(err);
|
|
||||||
throw err;
|
|
||||||
}
|
|
||||||
log.DEBUG("Write Complete");
|
|
||||||
if (callback) callback()
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the current config file in use by OP25
|
|
||||||
* @returns {object|*} The parsed config object currently set in OP25
|
|
||||||
*/
|
|
||||||
function readOP25Config() {
|
|
||||||
const configPath = getRadioConfigPath();
|
|
||||||
log.DEBUG(`Reading from config path: '${configPath}'`);
|
|
||||||
return JSON.parse(fs.readFileSync(configPath));
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get the path of the config for the radio app (OP25) and set the global variable
|
|
||||||
*/
|
|
||||||
function getRadioConfigPath(){
|
|
||||||
let radioConfigDirPath = dirname(getRadioBinPath());
|
|
||||||
return resolve(`${radioConfigDirPath}/cfg.json`);
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Check to see if the preset name exists in the config
|
|
||||||
* @param {string} presetName The system name as saved in the preset
|
|
||||||
* @returns {true||false}
|
|
||||||
*/
|
|
||||||
function checkIfPresetExists(presetName) {
|
|
||||||
const savedPresets = presetWrappers.getPresets();
|
|
||||||
if (!Object.keys(savedPresets).includes(presetName)) return false;
|
|
||||||
else return true;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Convert a radioPreset to OP25's cfg.json file
|
|
||||||
*/
|
|
||||||
function convertRadioPresetsToOP25Config(presetName){
|
|
||||||
const savedPresets = presetWrappers.getPresets();
|
|
||||||
let frequencyString = "";
|
|
||||||
for (const frequency of savedPresets[presetName].frequencies){
|
|
||||||
frequencyString += `${converter(frequency).from("Hz").to("MHz")},`
|
|
||||||
}
|
|
||||||
frequencyString = frequencyString.slice(0, -1);
|
|
||||||
|
|
||||||
let updatedOP25Config;
|
|
||||||
switch (savedPresets[presetName].mode){
|
|
||||||
case "p25":
|
|
||||||
updatedOP25Config = new radioConfigHelper.P25({
|
|
||||||
"systemName": presetName,
|
|
||||||
"controlChannelsString": frequencyString,
|
|
||||||
"tagsFile": savedPresets[presetName].trunkFile
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
case "nbfm":
|
|
||||||
//code for nbfm here
|
|
||||||
updatedOP25Config = new radioConfigHelper.NBFM({
|
|
||||||
"frequency": frequencyString,
|
|
||||||
"systemName": presetName
|
|
||||||
});
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw new Error("Radio mode of selected preset not recognized");
|
|
||||||
}
|
|
||||||
|
|
||||||
log.DEBUG(updatedOP25Config);
|
|
||||||
return updatedOP25Config;
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -1,150 +0,0 @@
|
|||||||
//Config
|
|
||||||
import { getTOKEN, getGuildID, getApplicationID } from './utilities/configHandler.js';
|
|
||||||
// Commands
|
|
||||||
import ping from './commands/ping.js';
|
|
||||||
import join from './commands/join.js';
|
|
||||||
import leave from './commands/leave.js';
|
|
||||||
import status from './commands/status.js';
|
|
||||||
// Debug
|
|
||||||
import ModuleDebugBuilder from "./utilities/moduleDebugBuilder.js";
|
|
||||||
const log = new ModuleDebugBuilder("bot", "app");
|
|
||||||
// Modules
|
|
||||||
import { Client, GatewayIntentBits } from 'discord.js';
|
|
||||||
// Utilities
|
|
||||||
import registerCommands from './utilities/registerCommands.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Host Process Object Builder
|
|
||||||
*
|
|
||||||
* This constructor is used to easily construct responses to the host process
|
|
||||||
*/
|
|
||||||
class HPOB {
|
|
||||||
/**
|
|
||||||
* Build an object to be passed to the host process
|
|
||||||
* @param command The command to that was run ("Status", "Join", "Leave", "ChgPreSet")
|
|
||||||
* @param response The response from the command that was run
|
|
||||||
*/
|
|
||||||
constructor(command = "Status"||"Join"||"Leave"||"ChgPreSet", response) {
|
|
||||||
this.cmd = command;
|
|
||||||
this.msg = response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the Discord client
|
|
||||||
const client = new Client({
|
|
||||||
intents: [
|
|
||||||
GatewayIntentBits.Guilds,
|
|
||||||
GatewayIntentBits.GuildMessages,
|
|
||||||
GatewayIntentBits.MessageContent,
|
|
||||||
GatewayIntentBits.GuildVoiceStates
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When the parent process sends a message, this will interpret the message and act accordingly
|
|
||||||
*
|
|
||||||
* DRB IPC Message Structure:
|
|
||||||
* msg.cmd = The command keyword; Commands covered on the server side
|
|
||||||
* msg.params = An array containing the parameters for the command
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
process.on('message', (msg) => {
|
|
||||||
log.DEBUG('IPC Message: ', msg);
|
|
||||||
const guildID = getGuilds()[0];
|
|
||||||
|
|
||||||
log.DEBUG("Guild Name: ", getGuildNameFromID(guildID));
|
|
||||||
switch (msg.cmd) {
|
|
||||||
// Check the status of the bot
|
|
||||||
case "Status":
|
|
||||||
log.INFO("Status command run from IPC");
|
|
||||||
|
|
||||||
status({guildID: guildID, callback: (statusObj) => {
|
|
||||||
log.DEBUG("Status Object string: ", statusObj);
|
|
||||||
if (!statusObj.voiceConnection) return process.send(new HPOB("Status", "VDISCONN"));
|
|
||||||
}});
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Check the params for a server ID and if so join the server
|
|
||||||
case "Join":
|
|
||||||
log.INFO("Join command run from IPC");
|
|
||||||
|
|
||||||
join({guildID: guildID, guildObj: client.guilds.cache.get(guildID), channelID: msg.params.channelID, callback: () => {
|
|
||||||
process.send(new HPOB("Join", "AIDS"));
|
|
||||||
}})
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Check to see if the bot is in a server and if so leave
|
|
||||||
case "Leave":
|
|
||||||
log.INFO("Leave command run from IPC");
|
|
||||||
|
|
||||||
leave({guildID: guildID, callback: (response) => {
|
|
||||||
process.send(new HPOB("Leave", response));
|
|
||||||
}});
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Command doesn't exist
|
|
||||||
log.INFO("Unknown command run from IPC");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// When the client is connected and ready
|
|
||||||
client.on('ready', () =>{
|
|
||||||
log.INFO(`${client.user.tag} is ready`)
|
|
||||||
process.send({'msg': "INIT READY"});
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Saved For later
|
|
||||||
client.on('messageCreate', (message) => {
|
|
||||||
log.DEBUG(`Message Sent by: ${message.author.tag}\n\t'${message.content}'`);
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
// When a command is sent
|
|
||||||
client.on('interactionCreate', (interaction) => {
|
|
||||||
if (interaction.isChatInputCommand()){
|
|
||||||
switch (interaction.commandName) {
|
|
||||||
case "ping":
|
|
||||||
ping(interaction);
|
|
||||||
break;
|
|
||||||
case "join":
|
|
||||||
join({ interaction: interaction });
|
|
||||||
break;
|
|
||||||
case "leave":
|
|
||||||
leave({ interaction: interaction });
|
|
||||||
break;
|
|
||||||
case "status":
|
|
||||||
status({ interaction: interaction });
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
interaction.reply({ content: 'Command not found, try one that exists', fetchReply: true })
|
|
||||||
.then((message) => log.DEBUG(`Reply sent with content ${message.content}`))
|
|
||||||
.catch((err) => log.ERROR(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function loginBot(){
|
|
||||||
client.login(getTOKEN());
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGuilds() {
|
|
||||||
return client.guilds.cache.map(guild => guild.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGuildNameFromID(guildID) {
|
|
||||||
return client.guilds.cache.map((guild) => {
|
|
||||||
if (guild.id === guildID) return guild.name;
|
|
||||||
})[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
function main(){
|
|
||||||
registerCommands(() => {
|
|
||||||
loginBot();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
//module.exports = client;
|
|
||||||
2907
Client/discord-bot/package-lock.json
generated
2907
Client/discord-bot/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,25 +0,0 @@
|
|||||||
{
|
|
||||||
"name": "discord-bot",
|
|
||||||
"version": "1.0.0",
|
|
||||||
"description": "",
|
|
||||||
"main": "app.js",
|
|
||||||
"scripts": {
|
|
||||||
"test": "echo \"Error: no test specified\" && exit 1"
|
|
||||||
},
|
|
||||||
"keywords": [],
|
|
||||||
"author": "",
|
|
||||||
"license": "ISC",
|
|
||||||
"dependencies": {
|
|
||||||
"@discordjs/builders": "^1.4.0",
|
|
||||||
"@discordjs/opus": "^0.9.0",
|
|
||||||
"@discordjs/rest": "^1.4.0",
|
|
||||||
"@discordjs/voice": "^0.14.0",
|
|
||||||
"@mapbox/node-pre-gyp": "^1.0.10",
|
|
||||||
"debug": "^4.3.4",
|
|
||||||
"discord.js": "^14.7.1",
|
|
||||||
"node-gyp": "^9.3.0",
|
|
||||||
"libsodium-wrappers": "^0.7.10",
|
|
||||||
"alsa-capture": "0.3.0"
|
|
||||||
},
|
|
||||||
"type": "module"
|
|
||||||
}
|
|
||||||
@@ -1,46 +0,0 @@
|
|||||||
# Discord Radio Bot: Command & Control - Client: Discord Bot (Client)
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Explanation here
|
|
||||||
|
|
||||||
## Requirements
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Requirements here (not modules, that will be installed with npm)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Installation
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Notes here
|
|
||||||
|
|
||||||
### Installation here
|
|
||||||
```shell
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Configuration
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
Notes here
|
|
||||||
|
|
||||||
### Configuration here
|
|
||||||
```shell
|
|
||||||
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
## Usage
|
|
||||||
|
|
||||||
---
|
|
||||||
|
|
||||||
### Usage here
|
|
||||||
```javascript
|
|
||||||
|
|
||||||
```
|
|
||||||
@@ -1,17 +0,0 @@
|
|||||||
// Debug
|
|
||||||
import Debug from 'debug';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create the different logging methods for a function
|
|
||||||
* Namespace template = ("[app]:[fileName]:['INFO', 'WARNING', 'DEBUG', 'ERROR']")
|
|
||||||
* @param {string} appName The name of the app to be used in the 'app' portion of the namespace
|
|
||||||
* @param {string} fileName The name of the file calling the builder to be used in the 'fileName' portion of the namespace
|
|
||||||
*/
|
|
||||||
export default class ModuleDebugBuilder {
|
|
||||||
constructor(appName, fileName) {
|
|
||||||
this.INFO = Debug(`${appName}:${fileName}:INFO`);
|
|
||||||
this.DEBUG = Debug(`${appName}:${fileName}:DEBUG`);
|
|
||||||
this.WARN = Debug(`${appName}:${fileName}:WARNING`);
|
|
||||||
this.ERROR = Debug(`${appName}:${fileName}:ERROR`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -1,55 +0,0 @@
|
|||||||
import {SlashCommandBuilder} from "@discordjs/builders";
|
|
||||||
import {REST} from "@discordjs/rest";
|
|
||||||
import {getApplicationID, getGuildID, getTOKEN} from "./configHandler.js";
|
|
||||||
import { Routes, ChannelType } from "discord.js";
|
|
||||||
// Debug
|
|
||||||
import ModuleDebugBuilder from "./moduleDebugBuilder.js";
|
|
||||||
const log = new ModuleDebugBuilder("bot", "registerCommands");
|
|
||||||
|
|
||||||
const pingCommand = new SlashCommandBuilder()
|
|
||||||
.setName("ping")
|
|
||||||
.setDescription("Confirm the bot is online")
|
|
||||||
.toJSON();
|
|
||||||
|
|
||||||
const joinCommand = new SlashCommandBuilder()
|
|
||||||
.setName('join')
|
|
||||||
.setDescription('Joins a voice channel')
|
|
||||||
.addChannelOption((option) => option
|
|
||||||
.setName('voicechannel')
|
|
||||||
.setDescription('The Channel to voiceController')
|
|
||||||
.setRequired(false)
|
|
||||||
.addChannelTypes(ChannelType.GuildVoice))
|
|
||||||
.toJSON();
|
|
||||||
|
|
||||||
const leaveCommand = new SlashCommandBuilder()
|
|
||||||
.setName("leave")
|
|
||||||
.setDescription("Leave current voice channel")
|
|
||||||
.toJSON();
|
|
||||||
|
|
||||||
const statusCommand = new SlashCommandBuilder()
|
|
||||||
.setName("status")
|
|
||||||
.setDescription("Returns if the bot is connected to a channel or not")
|
|
||||||
.toJSON();
|
|
||||||
|
|
||||||
export default async function registerCommands(callback){
|
|
||||||
const commands = [
|
|
||||||
pingCommand,
|
|
||||||
joinCommand,
|
|
||||||
leaveCommand,
|
|
||||||
statusCommand
|
|
||||||
];
|
|
||||||
|
|
||||||
try {
|
|
||||||
const rest = new REST({ version: '10' }).setToken(getTOKEN());
|
|
||||||
const clientID = getApplicationID();
|
|
||||||
const guildID = getGuildID();
|
|
||||||
|
|
||||||
await rest.put(Routes.applicationGuildCommands(clientID, guildID), {
|
|
||||||
body: commands,
|
|
||||||
});
|
|
||||||
log.DEBUG("Successfully registered the following commands: ", commands)
|
|
||||||
callback();
|
|
||||||
} catch (err) {
|
|
||||||
log.ERROR(err);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
915
Client/package-lock.json
generated
915
Client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -3,6 +3,11 @@
|
|||||||
"version": "0.0.0",
|
"version": "0.0.0",
|
||||||
"private": true,
|
"private": true,
|
||||||
"main": "app.js",
|
"main": "app.js",
|
||||||
|
"scripts": {
|
||||||
|
"test": "echo \"Error: no test specified\" && exit 1",
|
||||||
|
"preinstall": "echo preinstall",
|
||||||
|
"postinstall": "echo postinstall"
|
||||||
|
},
|
||||||
"dependencies": {
|
"dependencies": {
|
||||||
"convert-units": "^2.3.4",
|
"convert-units": "^2.3.4",
|
||||||
"cookie-parser": "~1.4.4",
|
"cookie-parser": "~1.4.4",
|
||||||
@@ -12,15 +17,9 @@
|
|||||||
"express": "~4.16.1",
|
"express": "~4.16.1",
|
||||||
"http-errors": "~1.6.3",
|
"http-errors": "~1.6.3",
|
||||||
"morgan": "~1.9.1",
|
"morgan": "~1.9.1",
|
||||||
"replace-in-file": "~6.3.5",
|
"replace-in-file": "~7.0.1",
|
||||||
"@discordjs/builders": "^1.4.0",
|
"@discordjs/builders": "^1.4.0",
|
||||||
"@discordjs/opus": "^0.9.0",
|
|
||||||
"@discordjs/rest": "^1.4.0",
|
"@discordjs/rest": "^1.4.0",
|
||||||
"@discordjs/voice": "^0.14.0",
|
"discord.js": "^14.7.1"
|
||||||
"@mapbox/node-pre-gyp": "^1.0.10",
|
|
||||||
"discord.js": "^14.7.1",
|
|
||||||
"node-gyp": "^9.3.0",
|
|
||||||
"libsodium-wrappers": "^0.7.10",
|
|
||||||
"alsa-capture": "0.3.0"
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
5
Client/pdab/.gitignore
vendored
Normal file
5
Client/pdab/.gitignore
vendored
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
*venv/
|
||||||
|
*__pycache__/
|
||||||
|
*.html
|
||||||
|
*.exe
|
||||||
|
LICENSE
|
||||||
215
Client/pdab/NoiseGatev2.py
Normal file
215
Client/pdab/NoiseGatev2.py
Normal file
@@ -0,0 +1,215 @@
|
|||||||
|
import audioop
|
||||||
|
import logging
|
||||||
|
import math
|
||||||
|
import time
|
||||||
|
|
||||||
|
import pyaudio
|
||||||
|
import discord
|
||||||
|
import numpy
|
||||||
|
|
||||||
|
voice_connection = None
|
||||||
|
|
||||||
|
LOGGER = logging.getLogger("Discord_Radio_Bot.NoiseGateV2")
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
class AudioStream:
|
||||||
|
def __init__(self, _channels: int = 2, _sample_rate: int = 48000, _frames_per_buffer: int = 1024,
|
||||||
|
_input_device_index: int = None, _output_device_index: int = None, _input: bool = True,
|
||||||
|
_output: bool = True, _init_on_startup: bool = True):
|
||||||
|
self.paInstance_kwargs = {
|
||||||
|
'format': pyaudio.paInt16,
|
||||||
|
'channels': _channels,
|
||||||
|
'rate': _sample_rate,
|
||||||
|
'input': _input,
|
||||||
|
'output': _output,
|
||||||
|
'frames_per_buffer': _frames_per_buffer
|
||||||
|
}
|
||||||
|
|
||||||
|
if _input_device_index:
|
||||||
|
if _input:
|
||||||
|
self.paInstance_kwargs['input_device_index'] = _input_device_index
|
||||||
|
else:
|
||||||
|
LOGGER.warning(f"[AudioStream.__init__]:\tInput was not enabled."
|
||||||
|
f" Reinitialize with '_input=True'")
|
||||||
|
|
||||||
|
if _output_device_index:
|
||||||
|
if _output:
|
||||||
|
self.paInstance_kwargs['output_device_index'] = _output_device_index
|
||||||
|
else:
|
||||||
|
LOGGER.warning(f"[AudioStream.__init__]:\tOutput was not enabled."
|
||||||
|
f" Reinitialize with '_output=True'")
|
||||||
|
|
||||||
|
if _init_on_startup:
|
||||||
|
# Init PyAudio instance
|
||||||
|
LOGGER.info("Creating PyAudio instance")
|
||||||
|
self.paInstance = pyaudio.PyAudio()
|
||||||
|
|
||||||
|
# Define and initialize stream object if we have been passed a device ID (pyaudio.open)
|
||||||
|
self.stream = None
|
||||||
|
|
||||||
|
if _output_device_index or _input_device_index:
|
||||||
|
if _init_on_startup:
|
||||||
|
LOGGER.info("Init stream")
|
||||||
|
self.init_stream()
|
||||||
|
|
||||||
|
def init_stream(self, _new_output_device_index: int = None, _new_input_device_index: int = None):
|
||||||
|
# Check what device was asked to be changed (or set)
|
||||||
|
if _new_input_device_index:
|
||||||
|
if self.paInstance_kwargs['input']:
|
||||||
|
self.paInstance_kwargs['input_device_index'] = _new_input_device_index
|
||||||
|
else:
|
||||||
|
LOGGER.warning(f"[AudioStream.init_stream]:\tInput was not enabled when initialized."
|
||||||
|
f" Reinitialize with '_input=True'")
|
||||||
|
|
||||||
|
if _new_output_device_index:
|
||||||
|
if self.paInstance_kwargs['output']:
|
||||||
|
self.paInstance_kwargs['output_device_index'] = _new_output_device_index
|
||||||
|
else:
|
||||||
|
LOGGER.warning(f"[AudioStream.init_stream]:\tOutput was not enabled when initialized."
|
||||||
|
f" Reinitialize with '_output=True'")
|
||||||
|
|
||||||
|
self.close_if_open()
|
||||||
|
|
||||||
|
# Open the stream
|
||||||
|
self.stream = self.paInstance.open(**self.paInstance_kwargs)
|
||||||
|
|
||||||
|
def close_if_open(self):
|
||||||
|
# Stop the stream if it is started
|
||||||
|
if self.stream:
|
||||||
|
if self.stream.is_active():
|
||||||
|
self.stream.stop_stream()
|
||||||
|
self.stream.close()
|
||||||
|
LOGGER.debug(f"[ReopenStream.close_if_open]:\t Stream was open; It was closed.")
|
||||||
|
|
||||||
|
def list_devices(self, _display_input_devices: bool = True, _display_output_devices: bool = True):
|
||||||
|
LOGGER.info('Getting a list of the devices connected')
|
||||||
|
info = self.paInstance.get_host_api_info_by_index(0)
|
||||||
|
numdevices = info.get('deviceCount')
|
||||||
|
|
||||||
|
devices = {
|
||||||
|
'Input': {},
|
||||||
|
'Output': {}
|
||||||
|
}
|
||||||
|
for i in range(0, numdevices):
|
||||||
|
if (self.paInstance.get_device_info_by_host_api_device_index(0, i).get('maxInputChannels')) > 0:
|
||||||
|
input_device = self.paInstance.get_device_info_by_host_api_device_index(0, i).get('name')
|
||||||
|
devices['Input'][i] = input_device
|
||||||
|
if _display_input_devices:
|
||||||
|
LOGGER.debug(f"Input Device id {i} - {input_device}")
|
||||||
|
|
||||||
|
if (self.paInstance.get_device_info_by_host_api_device_index(0, i).get('maxOutputChannels')) > 0:
|
||||||
|
output_device = self.paInstance.get_device_info_by_host_api_device_index(0, i).get('name')
|
||||||
|
devices['Output'][i] = output_device
|
||||||
|
if _display_output_devices:
|
||||||
|
LOGGER.debug(f"Output Device id {i} - {output_device}")
|
||||||
|
|
||||||
|
return devices
|
||||||
|
|
||||||
|
async def stop(self):
|
||||||
|
await voice_connection.disconnect()
|
||||||
|
self.close_if_open()
|
||||||
|
self.stream.close()
|
||||||
|
self.paInstance.terminate()
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
class NoiseGate(AudioStream):
|
||||||
|
def __init__(self, _voice_connection, _noise_gate_threshold: int, **kwargs):
|
||||||
|
super(NoiseGate, self).__init__(_init_on_startup=True, **kwargs)
|
||||||
|
global voice_connection
|
||||||
|
voice_connection = _voice_connection
|
||||||
|
self.THRESHOLD = _noise_gate_threshold
|
||||||
|
self.NGStream = NoiseGateStream(self)
|
||||||
|
self.Voice_Connection_Thread = None
|
||||||
|
|
||||||
|
def run(self) -> None:
|
||||||
|
global voice_connection
|
||||||
|
# Start the audio stream
|
||||||
|
LOGGER.debug(f"Starting stream")
|
||||||
|
self.stream.start_stream()
|
||||||
|
# Start the stream to discord
|
||||||
|
self.core()
|
||||||
|
|
||||||
|
def core(self, error=None):
|
||||||
|
if error:
|
||||||
|
LOGGER.warning(error)
|
||||||
|
|
||||||
|
while not voice_connection.is_connected():
|
||||||
|
time.sleep(.2)
|
||||||
|
|
||||||
|
if not voice_connection.is_playing():
|
||||||
|
LOGGER.debug(f"Playing stream to discord")
|
||||||
|
voice_connection.play(self.NGStream, after=self.core)
|
||||||
|
|
||||||
|
async def close(self):
|
||||||
|
LOGGER.debug(f"Closing")
|
||||||
|
await voice_connection.disconnect()
|
||||||
|
if self.stream.is_active:
|
||||||
|
self.stream.stop_stream()
|
||||||
|
LOGGER.debug(f"Stopping stream")
|
||||||
|
|
||||||
|
|
||||||
|
# noinspection PyUnresolvedReferences
|
||||||
|
class NoiseGateStream(discord.AudioSource):
|
||||||
|
def __init__(self, _stream):
|
||||||
|
super(NoiseGateStream, self).__init__()
|
||||||
|
self.stream = _stream # The actual audio stream object
|
||||||
|
self.NG_fadeout = 240/20 # Fadeout value used to hold the noisegate after de-triggering
|
||||||
|
self.NG_fadeout_count = 0 # A count set when the noisegate is triggered and was de-triggered
|
||||||
|
self.process_set_count = 0 # Counts how many processes have been made
|
||||||
|
|
||||||
|
def read(self):
|
||||||
|
try:
|
||||||
|
while voice_connection.is_connected():
|
||||||
|
curr_buffer = bytearray(self.stream.stream.read(960))
|
||||||
|
buffer_rms = audioop.rms(curr_buffer, 2)
|
||||||
|
if buffer_rms > 0:
|
||||||
|
buffer_decibel = 20 * math.log10(buffer_rms)
|
||||||
|
|
||||||
|
if self.process_set_count % 10 == 0:
|
||||||
|
if buffer_decibel >= self.stream.THRESHOLD:
|
||||||
|
LOGGER.debug(f"[Noisegate Open] {buffer_decibel} db")
|
||||||
|
else:
|
||||||
|
LOGGER.debug(f"[Noisegate Closed] {buffer_decibel} db")
|
||||||
|
|
||||||
|
if buffer_decibel >= self.stream.THRESHOLD:
|
||||||
|
self.NG_fadeout_count = self.NG_fadeout
|
||||||
|
self.process_set_count += 1
|
||||||
|
if curr_buffer:
|
||||||
|
return bytes(curr_buffer)
|
||||||
|
|
||||||
|
else:
|
||||||
|
if self.NG_fadeout_count > 0:
|
||||||
|
self.NG_fadeout_count -= 1
|
||||||
|
LOGGER.debug(f"Frames in fadeout remaining: {self.NG_fadeout_count}")
|
||||||
|
self.process_set_count += 1
|
||||||
|
if curr_buffer:
|
||||||
|
return bytes(curr_buffer)
|
||||||
|
|
||||||
|
except OSError as e:
|
||||||
|
LOGGER.warning(e)
|
||||||
|
pass
|
||||||
|
|
||||||
|
def audio_datalist_set_volume(self, datalist, volume):
|
||||||
|
""" Change value of list of audio chunks """
|
||||||
|
sound_level = (volume / 100.)
|
||||||
|
|
||||||
|
for i in range(len(datalist)):
|
||||||
|
chunk = numpy.fromstring(datalist[i], numpy.int16)
|
||||||
|
|
||||||
|
chunk = chunk * sound_level
|
||||||
|
|
||||||
|
datalist[i] = chunk.astype(numpy.int16)
|
||||||
|
|
||||||
|
|
||||||
|
if __name__ == '__main__':
|
||||||
|
|
||||||
|
input_index = int(input("Input:\t"))
|
||||||
|
output_index = int(input("Output:\t"))
|
||||||
|
|
||||||
|
ng = NoiseGate(_input_device_index=input_index, _output_device_index=output_index)
|
||||||
|
|
||||||
|
ng.list_devices()
|
||||||
|
|
||||||
|
ng.start()
|
||||||
11
Client/pdab/getDevices.py
Normal file
11
Client/pdab/getDevices.py
Normal file
@@ -0,0 +1,11 @@
|
|||||||
|
from NoiseGatev2 import AudioStream
|
||||||
|
|
||||||
|
print('Getting a list of devices')
|
||||||
|
list_of_devices = AudioStream().list_devices()
|
||||||
|
print("----- INPUT DEVICES -----")
|
||||||
|
for inputDevice in list_of_devices['Input']:
|
||||||
|
print(f"{inputDevice}\t-\t{list_of_devices['Input'][inputDevice]}")
|
||||||
|
|
||||||
|
print("----- OUTPUT DEVICES -----")
|
||||||
|
for outputDevice in list_of_devices['Output']:
|
||||||
|
print(f"{outputDevice}\t-\t{list_of_devices['Output'][outputDevice]}")
|
||||||
81
Client/pdab/main.py
Normal file
81
Client/pdab/main.py
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
import argparse, platform, os
|
||||||
|
from discord import Intents, Client, Member, opus, Activity, ActivityType
|
||||||
|
from discord.ext import commands
|
||||||
|
from NoiseGatev2 import NoiseGate
|
||||||
|
|
||||||
|
# Load the proper OPUS library for the device being used
|
||||||
|
async def load_opus():
|
||||||
|
# Check the system type and load the correct library
|
||||||
|
# Linux ARM AARCH64 running 32bit OS
|
||||||
|
processor = platform.machine()
|
||||||
|
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'))
|
||||||
|
print(f"Loaded OPUS library for AMD64")
|
||||||
|
return "AMD64"
|
||||||
|
else:
|
||||||
|
if processor == "aarch64":
|
||||||
|
opus.load_opus(os.path.join(script_dir, './opus/libopus_aarcch64.so'))
|
||||||
|
print(f"Loaded OPUS library for aarch64")
|
||||||
|
return "aarch64"
|
||||||
|
elif processor == "armv7l":
|
||||||
|
opus.load_opus(os.path.join(script_dir, './opus/libopus_armv7l.so'))
|
||||||
|
print(f"Loaded OPUS library for armv7l")
|
||||||
|
return "armv7l"
|
||||||
|
|
||||||
|
|
||||||
|
def main(clientId='OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY', channelId=367396189529833476, NGThreshold=50, deviceId=1, presence="the radio"):
|
||||||
|
intents = Intents.default()
|
||||||
|
|
||||||
|
client = commands.Bot(command_prefix='!', intents=intents)
|
||||||
|
|
||||||
|
@client.event
|
||||||
|
async def on_ready():
|
||||||
|
print(f'We have logged in as {client.user}')
|
||||||
|
# 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)
|
||||||
|
|
||||||
|
print("Loading opus")
|
||||||
|
await load_opus()
|
||||||
|
|
||||||
|
if opus.is_loaded():
|
||||||
|
print("Joining voice")
|
||||||
|
channelConnection = await channelIdToJoin.connect(timeout=60.0, reconnect=True)
|
||||||
|
print("Voice Connected")
|
||||||
|
streamHandler = NoiseGate(
|
||||||
|
_input_device_index=deviceId,
|
||||||
|
_voice_connection=channelConnection,
|
||||||
|
_noise_gate_threshold=NGThreshold)
|
||||||
|
# Start the audio stream
|
||||||
|
streamHandler.run()
|
||||||
|
print("stream running")
|
||||||
|
|
||||||
|
|
||||||
|
client.run(clientId)
|
||||||
|
|
||||||
|
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()
|
||||||
|
|
||||||
|
if (not args.NGThreshold):
|
||||||
|
args.NGThreshold = 50
|
||||||
|
|
||||||
|
print("Arguments:", args)
|
||||||
|
|
||||||
|
main(
|
||||||
|
clientId=args.clientId,
|
||||||
|
channelId=args.channelId,
|
||||||
|
NGThreshold=args.NGThreshold,
|
||||||
|
deviceId=args.deviceId,
|
||||||
|
presence=args.presence
|
||||||
|
)
|
||||||
BIN
Client/pdab/opus/libopus_aarcch64.so
Normal file
BIN
Client/pdab/opus/libopus_aarcch64.so
Normal file
Binary file not shown.
BIN
Client/pdab/opus/libopus_amd64.dll
Normal file
BIN
Client/pdab/opus/libopus_amd64.dll
Normal file
Binary file not shown.
BIN
Client/pdab/opus/libopus_armv7l.so
Normal file
BIN
Client/pdab/opus/libopus_armv7l.so
Normal file
Binary file not shown.
180
Client/pdab/recorder.py
Normal file
180
Client/pdab/recorder.py
Normal file
@@ -0,0 +1,180 @@
|
|||||||
|
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)
|
||||||
5
Client/pdab/requirements.txt
Normal file
5
Client/pdab/requirements.txt
Normal file
@@ -0,0 +1,5 @@
|
|||||||
|
discord>=2.2.3
|
||||||
|
PyNaCl>=1.5.0
|
||||||
|
pyaudio>=0.2.13
|
||||||
|
numpy==1.24.3
|
||||||
|
argparse
|
||||||
183
Client/public/res/css/main.css
Normal file
183
Client/public/res/css/main.css
Normal file
@@ -0,0 +1,183 @@
|
|||||||
|
.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;
|
||||||
|
}
|
||||||
402
Client/public/res/js/node.js
Normal file
402
Client/public/res/js/node.js
Normal file
@@ -0,0 +1,402 @@
|
|||||||
|
$(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();
|
||||||
|
}
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
body {
|
|
||||||
padding: 50px;
|
|
||||||
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
|
||||||
}
|
|
||||||
|
|
||||||
a {
|
|
||||||
color: #00B7FF;
|
|
||||||
}
|
|
||||||
@@ -2,13 +2,26 @@
|
|||||||
|
|
||||||
---
|
---
|
||||||
|
|
||||||
Explanation here
|
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.
|
||||||
|
|
||||||
## Requirements
|
## 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)
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
7
Client/restartSdrScanner.sh
Normal file
7
Client/restartSdrScanner.sh
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# This script should be another service on the machine to watch the main script for failures and restart it if there are any
|
||||||
|
|
||||||
|
( tail -f -n0 /opt/sdr-scanner/scanner_log & ) | grep -q ": cb transfer status: 1, canceling..."
|
||||||
|
systemctl restart radioNode.service
|
||||||
|
echo "Restarted SDR Scanner service"
|
||||||
@@ -5,7 +5,7 @@ const botController = require("../controllers/botController");
|
|||||||
/** GET bot status
|
/** GET bot status
|
||||||
* Check to see if the bot is online and if so, if it is currently connected to anything
|
* Check to see if the bot is online and if so, if it is currently connected to anything
|
||||||
*
|
*
|
||||||
* The status of the bot: 200 = client is online but not connected to discord, 201 = online on discord, 202 = connected to a channel, 500 + JSON = encountered error
|
* The status of the bot: 200 = connected to discord, 201 = not connected to discord, 500 + JSON = encountered error
|
||||||
* @returns status
|
* @returns status
|
||||||
*/
|
*/
|
||||||
router.get('/status', botController.getStatus);
|
router.get('/status', botController.getStatus);
|
||||||
@@ -15,7 +15,9 @@ router.get('/status', botController.getStatus);
|
|||||||
*
|
*
|
||||||
* @param req The request sent from the master
|
* @param req The request sent from the master
|
||||||
* @param req.body.channelId The channel ID to join
|
* @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.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);
|
router.post('/join', botController.joinServer);
|
||||||
|
|
||||||
@@ -27,4 +29,20 @@ router.post('/join', botController.joinServer);
|
|||||||
*/
|
*/
|
||||||
router.post('/leave', botController.leaveServer);
|
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;
|
module.exports = router;
|
||||||
|
|||||||
@@ -2,22 +2,31 @@
|
|||||||
const express = require('express');
|
const express = require('express');
|
||||||
const router = express.Router();
|
const router = express.Router();
|
||||||
// Controllers
|
// Controllers
|
||||||
const clientController = require("../controllers/clientController");
|
const { requestCheckIn, getPresets, updatePreset, addNewPreset, removePreset, updateClient, updateClientConfigWrapper } = require("../controllers/clientController");
|
||||||
|
|
||||||
/** GET Request a check in from the client
|
/** GET Request a check in from the client
|
||||||
* Queue the client to check in with the server
|
* Queue the client to check in with the server
|
||||||
*
|
*
|
||||||
* The status of the checkin request: 200 = Queued
|
* The status of the checkin request: 200 = Queued
|
||||||
*/
|
*/
|
||||||
router.get('/requestCheckIn', clientController.requestCheckIn);
|
router.get('/requestCheckIn', requestCheckIn);
|
||||||
|
|
||||||
/** GET Object of all known presets
|
/** GET Object of all known presets
|
||||||
* Query the client to get all the known presets
|
* Query the client to get all the known presets
|
||||||
*/
|
*/
|
||||||
router.get('/presets', clientController.getPresets);
|
router.get('/presets', getPresets);
|
||||||
|
|
||||||
|
/**
|
||||||
|
* 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
|
/** POST Update to preset
|
||||||
* Join the channel specified listening to the specified freq/mode
|
|
||||||
*
|
*
|
||||||
* @param req The request sent from the master
|
* @param req The request sent from the master
|
||||||
* @param {string} req.body.systemName The name of the system to be updated
|
* @param {string} req.body.systemName The name of the system to be updated
|
||||||
@@ -25,7 +34,7 @@ router.get('/presets', clientController.getPresets);
|
|||||||
* @param {string} req.body.mode The listening mode for the SDR
|
* @param {string} req.body.mode The listening mode for the SDR
|
||||||
* @param {string} req.body.trunkFile If the listening mode is digital this can be set to identify the communications
|
* @param {string} req.body.trunkFile If the listening mode is digital this can be set to identify the communications
|
||||||
*/
|
*/
|
||||||
router.post('/updatePreset', clientController.updatePreset);
|
router.post('/updatePreset', updatePreset);
|
||||||
|
|
||||||
/** POST Add new preset
|
/** POST Add new preset
|
||||||
* Join the channel specified listening to the specified freq/mode
|
* Join the channel specified listening to the specified freq/mode
|
||||||
@@ -36,6 +45,20 @@ router.post('/updatePreset', clientController.updatePreset);
|
|||||||
* @param {string} req.body.mode The listening mode for the SDR
|
* @param {string} req.body.mode The listening mode for the SDR
|
||||||
* @param {string} req.body.trunkFile If the listening mode is digital this can be set to identify the communications
|
* @param {string} req.body.trunkFile If the listening mode is digital this can be set to identify the communications
|
||||||
*/
|
*/
|
||||||
router.post('/addPreset', clientController.addNewPreset);
|
router.post('/addPreset', addNewPreset);
|
||||||
|
|
||||||
|
/** POST Remove a preset
|
||||||
|
*
|
||||||
|
* @param req The request sent from the master
|
||||||
|
* @param {string} req.body.systemName The name of the system to be updated
|
||||||
|
*/
|
||||||
|
router.post('/removePreset', removePreset);
|
||||||
|
|
||||||
|
|
||||||
|
/** POST Update the bot
|
||||||
|
*
|
||||||
|
* @param req The request sent from the master
|
||||||
|
*/
|
||||||
|
router.post('/updateClient', updateClient);
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -1,9 +1,11 @@
|
|||||||
var express = require('express');
|
var express = require('express');
|
||||||
var router = express.Router();
|
var router = express.Router();
|
||||||
|
const { getFullConfig } = require('../utilities/configHandler');
|
||||||
|
|
||||||
/* GET home page. */
|
/* GET home page. */
|
||||||
router.get('/', function(req, res, next) {
|
router.get('/', async function(req, res, next) {
|
||||||
res.render('index', { title: 'Express' });
|
const clientConfig = await getFullConfig();
|
||||||
|
res.render('index', { 'node': clientConfig });
|
||||||
});
|
});
|
||||||
|
|
||||||
module.exports = router;
|
module.exports = router;
|
||||||
|
|||||||
@@ -15,6 +15,7 @@ router.post('/start', radioController.openRadioSession);
|
|||||||
|
|
||||||
/**
|
/**
|
||||||
* POST Close the current radio session
|
* POST Close the current radio session
|
||||||
|
* Response from the radio: 200: closed; 204: not connected
|
||||||
*/
|
*/
|
||||||
router.post('/stop', radioController.closeRadioSession);
|
router.post('/stop', radioController.closeRadioSession);
|
||||||
|
|
||||||
|
|||||||
208
Client/setup.sh
Normal file
208
Client/setup.sh
Normal file
@@ -0,0 +1,208 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Check if the user is root
|
||||||
|
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
|
||||||
|
|
||||||
|
# Copy the radio config example file
|
||||||
|
cp config/radioPresets.json.EXAMPLE config/radioPresets.json
|
||||||
|
|
||||||
|
echo "----- Collecting Setup Information -----"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
|
||||||
|
# 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 -----"
|
||||||
|
|
||||||
|
# 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
|
||||||
|
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
|
||||||
|
|
||||||
|
# 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 -----"
|
||||||
|
|
||||||
|
# Setup bot service
|
||||||
|
echo "[Unit]
|
||||||
|
Description=Radio Node Service
|
||||||
|
After=network.target
|
||||||
|
|
||||||
|
[Service]
|
||||||
|
WorkingDirectory=$SCRIPT_DIR/
|
||||||
|
ExecStart=/usr/bin/node .
|
||||||
|
Restart=always
|
||||||
|
RestartDelay=10
|
||||||
|
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/
|
||||||
|
ExecStart=/usr/bin/bash update.sh
|
||||||
|
Restart=on-failure
|
||||||
|
|
||||||
|
[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
|
||||||
|
|
||||||
|
# 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
|
||||||
34
Client/update.sh
Normal file
34
Client/update.sh
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
#!/bin/bash
|
||||||
|
|
||||||
|
# Check if the user is root
|
||||||
|
if [ "$EUID" -ne 0 ]
|
||||||
|
then echo "Please run as root"
|
||||||
|
exit
|
||||||
|
fi
|
||||||
|
|
||||||
|
# Sleeping to give the client time to respond to the requester
|
||||||
|
sleep 5
|
||||||
|
|
||||||
|
# Stating Message
|
||||||
|
echo "<!-- UPDATING ---!>"
|
||||||
|
|
||||||
|
# TODO - Add an updater for Stable Diffusion API
|
||||||
|
|
||||||
|
# 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'
|
||||||
|
|
||||||
|
# Install any new libraries
|
||||||
|
npm i
|
||||||
|
|
||||||
|
# Start the service
|
||||||
|
systemctl start RadioNode
|
||||||
|
|
||||||
|
# Update complete message
|
||||||
|
echo "<!--- UPDATE COMPLETE! ---!>"
|
||||||
@@ -1,48 +1,42 @@
|
|||||||
// Debug
|
// Debug
|
||||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||||
const log = new DebugBuilder("client-bot", "configController");
|
const log = new DebugBuilder("client", "configController");
|
||||||
// Modules
|
// Modules
|
||||||
|
const { nodeObject } = require("./utilities.js");
|
||||||
|
const { getPresets } = require("../utilities/updatePresets");
|
||||||
const { readFileSync } = require('fs');
|
const { readFileSync } = require('fs');
|
||||||
const path = require("path");
|
const path = require("path");
|
||||||
|
require('dotenv').config();
|
||||||
|
|
||||||
function getConfig() {
|
const GuildID = process.env.GUILD_ID;
|
||||||
const botConfigObj = JSON.parse(readFileSync(path.resolve("./config/botConfig.json")))
|
const ApplicationID = process.env.APPLICATION_ID;
|
||||||
return botConfigObj;
|
const DeviceID = parseInt(process.env.AUDIO_DEVICE_ID);
|
||||||
}
|
const DeviceName = process.env.AUDIO_DEVICE_NAME;
|
||||||
exports.getConfig = getConfig;
|
|
||||||
|
|
||||||
function getGuildID() {
|
function getGuildID() {
|
||||||
const parsedJSON = getConfig();
|
log.DEBUG("Guild ID: ", GuildID);
|
||||||
const guildID = parsedJSON.GuildID;
|
return GuildID;
|
||||||
|
|
||||||
log.DEBUG("Guild ID: ", guildID);
|
|
||||||
return guildID;
|
|
||||||
}
|
}
|
||||||
exports.getGuildID = getGuildID;
|
exports.getGuildID = getGuildID;
|
||||||
|
|
||||||
function getApplicationID() {
|
function getApplicationID() {
|
||||||
const parsedJSON = getConfig();
|
log.DEBUG("Application ID: ", ApplicationID);
|
||||||
const appID = parsedJSON.ApplicationID;
|
return ApplicationID;
|
||||||
|
|
||||||
log.DEBUG("Application ID: ", appID);
|
|
||||||
return appID;
|
|
||||||
}
|
}
|
||||||
exports.getApplicationID = getApplicationID;
|
exports.getApplicationID = getApplicationID;
|
||||||
|
|
||||||
function getDeviceID(){
|
function getDeviceID(){
|
||||||
const parsedJSON = getConfig();
|
log.DEBUG("Device ID: ", DeviceID);
|
||||||
const deviceID = parseInt(parsedJSON.DeviceID);
|
return DeviceID;
|
||||||
|
|
||||||
log.DEBUG("Device ID: ", deviceID);
|
|
||||||
return deviceID;
|
|
||||||
}
|
}
|
||||||
exports.getDeviceID = getDeviceID;
|
exports.getDeviceID = getDeviceID;
|
||||||
|
|
||||||
function getDeviceName(){
|
function getDeviceName(){
|
||||||
const parsedJSON = getConfig();
|
log.DEBUG("Device Name: ", DeviceName);
|
||||||
const deviceName = parsedJSON.DeviceName;
|
return DeviceName;
|
||||||
|
|
||||||
log.DEBUG("Device Name: ", deviceName);
|
|
||||||
return deviceName;
|
|
||||||
}
|
}
|
||||||
exports.getDeviceName = getDeviceID;
|
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()});
|
||||||
|
}
|
||||||
@@ -1,5 +1,28 @@
|
|||||||
// Debug
|
// Debug
|
||||||
const debug = require('debug');
|
const debug = require('debug');
|
||||||
|
// Read .env file to process.env
|
||||||
|
require('dotenv').config();
|
||||||
|
// Modules
|
||||||
|
const { writeFile } = require('fs');
|
||||||
|
const { inspect } = require('util');
|
||||||
|
|
||||||
|
const logLocation = process.env.LOG_LOCATION;
|
||||||
|
|
||||||
|
async function writeToLog(logMessage, appName) {
|
||||||
|
logMessage = String(logMessage + "\n");
|
||||||
|
|
||||||
|
writeFile(
|
||||||
|
logLocation ?? `./${appName}.log`,
|
||||||
|
logMessage,
|
||||||
|
{ encoding: "utf-8", flag: 'a+' },
|
||||||
|
(err) => {
|
||||||
|
if (err) console.error(err);
|
||||||
|
|
||||||
|
// file written successfully
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Create the different logging methods for a function
|
* Create the different logging methods for a function
|
||||||
@@ -9,9 +32,38 @@ const debug = require('debug');
|
|||||||
*/
|
*/
|
||||||
exports.DebugBuilder = class DebugBuilder {
|
exports.DebugBuilder = class DebugBuilder {
|
||||||
constructor(appName, fileName) {
|
constructor(appName, fileName) {
|
||||||
this.INFO = debug(`${appName}:${fileName}:INFO`);
|
this.INFO = (...messageParts) => {
|
||||||
this.DEBUG = debug(`${appName}:${fileName}:DEBUG`);
|
const _info = debug(`${appName}:${fileName}:INFO`);
|
||||||
this.WARN = debug(`${appName}:${fileName}:WARNING`);
|
_info(messageParts);
|
||||||
this.ERROR = debug(`${appName}:${fileName}:ERROR`);
|
writeToLog(`${Date.now().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:INFO\t-\t${messageParts.map((messagePart, index, array) => {return inspect(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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
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);
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,52 +0,0 @@
|
|||||||
// Modules
|
|
||||||
const { promisify } = require('util');
|
|
||||||
const { exec } = require("child_process");
|
|
||||||
// Debug
|
|
||||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
|
||||||
// Global Vars
|
|
||||||
const log = new DebugBuilder("client-bot", "executeConsoleCommands");
|
|
||||||
const execCommand = promisify(exec);
|
|
||||||
|
|
||||||
|
|
||||||
async function executeAsyncConsoleCommand(consoleCommand) {
|
|
||||||
// Check to see if the command is a real command
|
|
||||||
// TODO needs to be improved
|
|
||||||
const acceptableCommands = [ "arecord -L" ];
|
|
||||||
if (!acceptableCommands.includes(consoleCommand)) {
|
|
||||||
log.WARN("Console command is not acceptable: ", consoleCommand);
|
|
||||||
return undefined;
|
|
||||||
}
|
|
||||||
log.DEBUG("Running console command: ", consoleCommand);
|
|
||||||
|
|
||||||
const tempOutput = await execCommand(consoleCommand);
|
|
||||||
const output = tempOutput.stdout.trim();
|
|
||||||
|
|
||||||
log.DEBUG("Executed Console Command Response: ", output)
|
|
||||||
|
|
||||||
// TODO add some error checking
|
|
||||||
return output;
|
|
||||||
}
|
|
||||||
exports.executeAsyncConsoleCommand = executeAsyncConsoleCommand;
|
|
||||||
|
|
||||||
async function returnAlsaDeviceObject() {
|
|
||||||
const listAlsaDevicesCommand = "arecord -L";
|
|
||||||
const commandResponse = await executeAsyncConsoleCommand(listAlsaDevicesCommand);
|
|
||||||
const brokenCommand = String(commandResponse).split('\n');
|
|
||||||
var devices = [];
|
|
||||||
var i = 0;
|
|
||||||
|
|
||||||
for (const responseLine of brokenCommand) {
|
|
||||||
if (String(responseLine) && !String(responseLine).match(/^\s/g)) {
|
|
||||||
const tempDevice = {
|
|
||||||
id: i,
|
|
||||||
name: responseLine
|
|
||||||
}
|
|
||||||
devices.push(tempDevice);
|
|
||||||
i += 1;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
return devices;
|
|
||||||
}
|
|
||||||
|
|
||||||
exports.returnAlsaDeviceObject = returnAlsaDeviceObject;
|
|
||||||
@@ -2,20 +2,25 @@
|
|||||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||||
const log = new DebugBuilder("client", "httpRequests");
|
const log = new DebugBuilder("client", "httpRequests");
|
||||||
// Config
|
// Config
|
||||||
const config = require("../config/clientConfig");
|
require('dotenv').config();
|
||||||
// Modules
|
// Modules
|
||||||
const http = require("http");
|
const http = require("http");
|
||||||
|
const { isJsonString } = require("./utilities.js");
|
||||||
|
|
||||||
exports.requestOptions = class requestOptions {
|
exports.requestOptions = class requestOptions {
|
||||||
constructor(path, method, hostname = undefined, headers = undefined, port = undefined) {
|
constructor(path, method, hostname = undefined, headers = undefined, port = undefined) {
|
||||||
if (method === "POST"){
|
if (["POST", "PUT"].includes(method)){
|
||||||
this.hostname = hostname ?? config.serverConfig.hostname
|
log.VERBOSE("Hostname Vars: ", hostname, process.env.SERVER_HOSTNAME, process.env.SERVER_IP);
|
||||||
this.path = path
|
if (hostname) this.hostname = hostname;
|
||||||
this.port = port ?? config.serverConfig.port
|
if (!this.hostname && process.env.SERVER_HOSTNAME) this.hostname = process.env.SERVER_HOSTNAME;
|
||||||
this.method = method
|
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");
|
||||||
|
this.path = path;
|
||||||
|
this.port = port ?? process.env.SERVER_PORT;
|
||||||
|
this.method = method;
|
||||||
this.headers = headers ?? {
|
this.headers = headers ?? {
|
||||||
'Content-Type': 'application/json',
|
'Content-Type': 'application/json',
|
||||||
}
|
};
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -33,17 +38,36 @@ exports.sendHttpRequest = function sendHttpRequest(requestOptions, data, callbac
|
|||||||
res.on('data', (data) => {
|
res.on('data', (data) => {
|
||||||
const responseObject = {
|
const responseObject = {
|
||||||
"statusCode": res.statusCode,
|
"statusCode": res.statusCode,
|
||||||
"body": JSON.parse(data)
|
"body": (isJsonString(data.toString())) ? JSON.parse(data.toString()) : data.toString()
|
||||||
};
|
};
|
||||||
log.DEBUG("Response Object: ", responseObject);
|
log.VERBOSE("Response Object: ", responseObject);
|
||||||
callback(responseObject);
|
callback(responseObject);
|
||||||
})
|
})
|
||||||
}).on('error', err => {
|
}).on('error', err => {
|
||||||
log.ERROR('Error: ', err.message)
|
if (err.code === "ECONNREFUSED"){
|
||||||
|
// Bot refused connection, assumed offline
|
||||||
|
log.WARN("Connection Refused");
|
||||||
|
}
|
||||||
|
else log.ERROR('Error: ', err.message, err);
|
||||||
|
callback(undefined);
|
||||||
// TODO need to handle if the server is down
|
// TODO need to handle if the server is down
|
||||||
})
|
})
|
||||||
|
|
||||||
// Write the data to the request and send it
|
// Write the data to the request and send it
|
||||||
req.write(data)
|
req.write(data);
|
||||||
req.end()
|
req.end();
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.onHttpError = function onHttpError(httpStatusCode) {
|
||||||
|
switch(httpStatusCode){
|
||||||
|
case 404:
|
||||||
|
// Endpoint not found
|
||||||
|
log.WARN("404 received");
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
// Unhandled HTTP error code
|
||||||
|
log.ERROR("HTTP request returned with status: ", httpStatusCode)
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
}
|
}
|
||||||
@@ -1,9 +0,0 @@
|
|||||||
// Debug
|
|
||||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
|
||||||
const log = new DebugBuilder("client-bot", "messageHandler");
|
|
||||||
|
|
||||||
exports.replyToInteraction = async function replyToInteraction(interaction, message){
|
|
||||||
interaction.reply({ content: message, fetchReply: true })
|
|
||||||
.then((message) => log.DEBUG(`Reply sent with content ${message.content}`))
|
|
||||||
.catch((err) => log.ERROR(err));
|
|
||||||
}
|
|
||||||
@@ -134,7 +134,7 @@ class audioConfig {
|
|||||||
"instance_name": "audio0",
|
"instance_name": "audio0",
|
||||||
"device_name": deviceName,
|
"device_name": deviceName,
|
||||||
"udp_port": port,
|
"udp_port": port,
|
||||||
"audio_gain": 1.0,
|
"audio_gain": 2.0,
|
||||||
"number_channels": 1
|
"number_channels": 1
|
||||||
}];
|
}];
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -3,15 +3,16 @@ const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
|||||||
const log = new DebugBuilder("client", "updateConfig");
|
const log = new DebugBuilder("client", "updateConfig");
|
||||||
// Modules
|
// Modules
|
||||||
const replace = require('replace-in-file');
|
const replace = require('replace-in-file');
|
||||||
|
const { getFullConfig } = require("./configHandler.js");
|
||||||
|
|
||||||
class Options {
|
class Options {
|
||||||
constructor(key, updatedValue) {
|
constructor(key, updatedValue) {
|
||||||
this.files = "./config/clientConfig.js";
|
this.files = "./.env";
|
||||||
// A regex of the line containing the key in the config file
|
// A regex of the line containing the key in the config file
|
||||||
this.from = new RegExp(`"${key}": (.+),`, "g");
|
this.from = new RegExp(`${key}="?(.+)"?`, "g");
|
||||||
// Check to see if the value is a string and needs to be wrapped in double quotes
|
// Check to see if the value is a string and needs to be wrapped in double quotes
|
||||||
if (typeof updatedValue === "string") this.to = `"${key}": "${updatedValue}",`;
|
if (Array(["string", "number"]).includes(typeof updatedValue)) this.to = `${key}="${updatedValue}",`;
|
||||||
else this.to = `"${key}": ${updatedValue},`;
|
else this.to = `${key}=${updatedValue}`;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -20,14 +21,83 @@ class Options {
|
|||||||
* @param updatedId The updated ID assigned to the bot
|
* @param updatedId The updated ID assigned to the bot
|
||||||
*/
|
*/
|
||||||
exports.updateId = (updatedId) => {
|
exports.updateId = (updatedId) => {
|
||||||
const options = new Options("id", 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;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {string} key The config file key to update with the value
|
||||||
|
* @param {string} value The value to update the key with
|
||||||
|
*/
|
||||||
|
exports.updateConfig = function updateConfig(key, value) {
|
||||||
|
const options = new Options(key, value);
|
||||||
|
|
||||||
updateConfigFile(options, (updatedFiles) => {
|
updateConfigFile(options, (updatedFiles) => {
|
||||||
// Do Something
|
// Do Something
|
||||||
|
log.DEBUG("Updated config file: ", updatedFiles);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|
||||||
/**
|
/**
|
||||||
* Wrapper to write changes to the file
|
* Wrapper to write changes to the file
|
||||||
* @param options An instance of the Objects class specified to the key being updated
|
* @param options An instance of the Objects class specified to the key being updated
|
||||||
@@ -36,7 +106,7 @@ exports.updateId = (updatedId) => {
|
|||||||
function updateConfigFile(options, callback){
|
function updateConfigFile(options, callback){
|
||||||
replace(options, (error, changedFiles) => {
|
replace(options, (error, changedFiles) => {
|
||||||
if (error) return console.error('Error occurred:', error);
|
if (error) return console.error('Error occurred:', error);
|
||||||
log.DEBUG('Modified files:', changedFiles);
|
log.VERBOSE('Modified files:', changedFiles);
|
||||||
callback(changedFiles);
|
callback(changedFiles);
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
@@ -17,7 +17,7 @@ function writePresets(presets, callback = undefined) {
|
|||||||
// Error checking
|
// Error checking
|
||||||
if (err) throw err;
|
if (err) throw err;
|
||||||
log.DEBUG("Write Complete");
|
log.DEBUG("Write Complete");
|
||||||
if (callback) callback()
|
if (callback) callback(); else return
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -48,12 +48,14 @@ function convertFrequencyToHertz(frequency){
|
|||||||
if (Number.isInteger(frequency)) {
|
if (Number.isInteger(frequency)) {
|
||||||
log.DEBUG(`${frequency} is an integer.`);
|
log.DEBUG(`${frequency} is an integer.`);
|
||||||
// Check to see if the frequency has the correct length
|
// Check to see if the frequency has the correct length
|
||||||
if (frequency.toString().length >= 7 && frequency.toString().length <= 9) return frequency
|
if (frequency >= 1000000) return frequency
|
||||||
|
if (frequency >= 100 && frequency <= 999) return frequency * 1000000
|
||||||
|
log.WARN("Frequency hasn't matched filters: ", frequency);
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
log.DEBUG(`${frequency} is a float value.`);
|
log.DEBUG(`${frequency} is a float value.`);
|
||||||
// Convert to a string to remove the decimal in place and then correct the length
|
// Convert to a string to remove the decimal in place and then correct the length
|
||||||
return converter(frequency).from("MHz").to("Hz");
|
return parseInt(converter(frequency).from("MHz").to("Hz"));
|
||||||
}
|
}
|
||||||
} else {
|
} else {
|
||||||
log.DEBUG(`${frequency} is not a number`);
|
log.DEBUG(`${frequency} is not a number`);
|
||||||
@@ -69,8 +71,9 @@ function convertFrequencyToHertz(frequency){
|
|||||||
*/
|
*/
|
||||||
exports.getPresets = function getPresets() {
|
exports.getPresets = function getPresets() {
|
||||||
const presetDir = path.resolve("./config/radioPresets.json");
|
const presetDir = path.resolve("./config/radioPresets.json");
|
||||||
log.DEBUG(`Getting presets from directory: '${presetDir}'`);
|
log.DEBUG(`Getting presets from directory: '${presetDir}'`);
|
||||||
return JSON.parse(fs.readFileSync(presetDir));
|
if (fs.existsSync(presetDir)) return JSON.parse(fs.readFileSync(presetDir));
|
||||||
|
else return {};
|
||||||
}
|
}
|
||||||
|
|
||||||
/**
|
/**
|
||||||
@@ -116,5 +119,20 @@ exports.updatePreset = (systemName, callback, { frequencies = undefined, mode =
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes the specified system
|
||||||
|
*
|
||||||
|
* @param {string} systemName The name of the system being modified
|
||||||
|
* @param {function} callback The callback function to be called when the function completes
|
||||||
|
*/
|
||||||
|
exports.removePreset = (systemName, callback) => {
|
||||||
|
const presets = this.getPresets();
|
||||||
|
// Check if a system name was passed
|
||||||
|
if (systemName in presets) {
|
||||||
|
delete presets[systemName];
|
||||||
|
writePresets(presets, callback);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
|||||||
282
Client/utilities/utilities.js
Normal file
282
Client/utilities/utilities.js
Normal file
@@ -0,0 +1,282 @@
|
|||||||
|
// Modules
|
||||||
|
const { promisify } = require('util');
|
||||||
|
const { exec, spawn } = require("child_process");
|
||||||
|
const { resolve, dirname } = require('path');
|
||||||
|
const radioConfigHelper = require("../utilities/radioConfigHelper");
|
||||||
|
const presetWrappers = require("../utilities/updatePresets");
|
||||||
|
const converter = require("convert-units");
|
||||||
|
const fs = require('fs');
|
||||||
|
require('dotenv').config();
|
||||||
|
// Debug
|
||||||
|
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||||
|
// Global Vars
|
||||||
|
const log = new DebugBuilder("client", "executeConsoleCommands");
|
||||||
|
const execCommand = promisify(exec);
|
||||||
|
const radioBinPath = process.env.OP25_BIN_PATH;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* An object containing the variables needed to run the local node
|
||||||
|
*/
|
||||||
|
exports.nodeObject = class nodeObject {
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} param0._id The ID of the node
|
||||||
|
* @param {*} param0._name The name of the node
|
||||||
|
* @param {*} param0._ip The IP that the master can contact the node at
|
||||||
|
* @param {*} param0._port The port that the client is listening on
|
||||||
|
* @param {*} param0._location The physical location of the node
|
||||||
|
* @param {*} param0._nearbySystems An object array of nearby systems
|
||||||
|
*/
|
||||||
|
constructor({ _id = null, _name = null, _ip = null, _port = null, _location = null, _nearbySystems = null }) {
|
||||||
|
this.id = _id;
|
||||||
|
this.name = _name;
|
||||||
|
this.ip = _ip;
|
||||||
|
this.port = _port;
|
||||||
|
this.location = _location;
|
||||||
|
this.nearbySystems = _nearbySystems;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} consoleCommand
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
exports.executeAsyncConsoleCommand = async function executeAsyncConsoleCommand(consoleCommand) {
|
||||||
|
// Check to see if the command is a real command
|
||||||
|
// TODO needs to be improved
|
||||||
|
const acceptableCommands = [ "arecord -L", 'ipconfig', 'ip addr' ];
|
||||||
|
if (!acceptableCommands.includes(consoleCommand)) {
|
||||||
|
log.WARN("Console command is not acceptable: ", consoleCommand);
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
log.DEBUG("Running console command: ", consoleCommand);
|
||||||
|
|
||||||
|
const tempOutput = await execCommand(consoleCommand);
|
||||||
|
const output = tempOutput.stdout.trim();
|
||||||
|
|
||||||
|
log.DEBUG("Executed Console Command Response: ", output)
|
||||||
|
|
||||||
|
// TODO add some error checking
|
||||||
|
return output;
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
*
|
||||||
|
* @param {*} process The process to close
|
||||||
|
* @returns {undefined} Undefined to replace the existing process in the parent
|
||||||
|
*/
|
||||||
|
exports.closeProcessWrapper = async (process) => {
|
||||||
|
log.INFO("Leaving the server");
|
||||||
|
if (!process) return undefined;
|
||||||
|
|
||||||
|
// Try to close the process gracefully
|
||||||
|
await process.kill(2);
|
||||||
|
|
||||||
|
// Wait 25 seconds and see if the process is still open, if it is force it close
|
||||||
|
await setTimeout(async () => {
|
||||||
|
if (process) await process.kill(9);
|
||||||
|
}, 25000)
|
||||||
|
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* This wrapper closes any open radio sessions and the opens a new one
|
||||||
|
*
|
||||||
|
* @returns {radioChildProcess} The process of the radio session for use
|
||||||
|
*/
|
||||||
|
exports.openRadioSessionWrapper = async (radioChildProcess, presetName) => {
|
||||||
|
if (radioChildProcess) radioChildProcess = await this.closeProcessWrapper(radioChildProcess);
|
||||||
|
|
||||||
|
const configChangeResult = await this.changeCurrentConfigWrapper(presetName);
|
||||||
|
|
||||||
|
// Throw an error to the client if the config change ran into an error
|
||||||
|
if (typeof configChangeResult === "string") return configChangeResult;
|
||||||
|
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
log.DEBUG("Starting Windows OP25");
|
||||||
|
radioChildProcess = await spawn("C:\\Python310\\python.exe", [getRadioBinPath(), "-c", getRadioConfigPath()], { cwd: dirname(getRadioBinPath()) });
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.DEBUG("Starting Linux OP25");
|
||||||
|
radioChildProcess = await spawn(getRadioBinPath(), ["-c", getRadioConfigPath()], { cwd: dirname(getRadioBinPath()) });
|
||||||
|
}
|
||||||
|
|
||||||
|
log.VERBOSE("Radio Process: ", radioChildProcess);
|
||||||
|
|
||||||
|
let fullOutput;
|
||||||
|
radioChildProcess.stdout.setEncoding('utf8');
|
||||||
|
radioChildProcess.stdout.on("data", (data) => {
|
||||||
|
log.VERBOSE("From Process: ", data);
|
||||||
|
fullOutput += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
radioChildProcess.stderr.on('data', (data) => {
|
||||||
|
log.VERBOSE(`stderr: ${data}`);
|
||||||
|
fullOutput += data.toString();
|
||||||
|
});
|
||||||
|
|
||||||
|
radioChildProcess.on('close', (code) => {
|
||||||
|
log.DEBUG(`child process exited with code ${code}`);
|
||||||
|
log.VERBOSE("Full output from radio: ", fullOutput);
|
||||||
|
});
|
||||||
|
|
||||||
|
radioChildProcess.on("error", (code, signal) => {
|
||||||
|
log.ERROR("Error from the radio process: ", code, signal);
|
||||||
|
});
|
||||||
|
|
||||||
|
// Starting the radio application
|
||||||
|
|
||||||
|
return radioChildProcess
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update the OP25 config with a preset
|
||||||
|
*
|
||||||
|
* @param {*} presetName The preset name to update the OP25 config file with
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
exports.changeCurrentConfigWrapper = async (presetName) => {
|
||||||
|
// Check if the given config is saved
|
||||||
|
log.DEBUG("Checking if provided preset is in the config");
|
||||||
|
const presetIsPresent = await checkIfPresetExists(presetName);
|
||||||
|
if (!presetIsPresent) return "No preset with given name found in config"; // No preset with the given name is in the config
|
||||||
|
|
||||||
|
// Check if the current config is the same as the preset given
|
||||||
|
try {
|
||||||
|
const currentConfig = readOP25Config();
|
||||||
|
if (currentConfig.channels && currentConfig.channels.name === presetName) {
|
||||||
|
log.DEBUG("Current config is the same as the preset given");
|
||||||
|
return undefined;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
log.WARN("Problem reading the config file, overwriting with the new config", err);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Convert radioPreset to OP25 'cfg.json. file
|
||||||
|
log.DEBUG("Converting radioPreset to OP25 config");
|
||||||
|
const updatedConfigObject = convertRadioPresetsToOP25Config(presetName);
|
||||||
|
|
||||||
|
// Replace current JSON file with the updated file
|
||||||
|
writeOP25Config(updatedConfigObject, () => {
|
||||||
|
return updatedConfigObject;
|
||||||
|
})
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the location of the 'multi_rx.py' binary from the config
|
||||||
|
*/
|
||||||
|
function getRadioBinPath(){
|
||||||
|
return resolve(radioBinPath);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the path of the config for the radio app (OP25) and set the global variable
|
||||||
|
*/
|
||||||
|
function getRadioConfigPath(){
|
||||||
|
let radioConfigDirPath = dirname(getRadioBinPath());
|
||||||
|
return resolve(`${radioConfigDirPath}/cfg.json`);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Write the given config to the JSON file in OP25 the bin dir
|
||||||
|
* @param config The full config to be written to the file
|
||||||
|
* @param {function} callback The function to be called when this wrapper completes
|
||||||
|
*/
|
||||||
|
function writeOP25Config(config, callback = undefined) {
|
||||||
|
log.DEBUG("Updating OP25 config with: ", config);
|
||||||
|
fs.writeFile(getRadioConfigPath(), JSON.stringify(config), (err) => {
|
||||||
|
// Error checking
|
||||||
|
if (err) {
|
||||||
|
log.ERROR(err);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
log.DEBUG("Write Complete");
|
||||||
|
if (callback) callback()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the current config file in use by OP25
|
||||||
|
* @returns {object|*} The parsed config object currently set in OP25
|
||||||
|
*/
|
||||||
|
function readOP25Config() {
|
||||||
|
const configPath = getRadioConfigPath();
|
||||||
|
log.DEBUG(`Reading from config path: '${configPath}'`);
|
||||||
|
const readFile = fs.readFileSync(configPath);
|
||||||
|
log.VERBOSE("File Contents: ", readFile.toString());
|
||||||
|
return JSON.parse(readFile);
|
||||||
|
}
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check to see if the preset name exists in the config
|
||||||
|
* @param {string} presetName The system name as saved in the preset
|
||||||
|
* @returns {true||false}
|
||||||
|
*/
|
||||||
|
function checkIfPresetExists(presetName) {
|
||||||
|
const savedPresets = presetWrappers.getPresets();
|
||||||
|
log.DEBUG("Found presets: ", savedPresets, presetName, Object.keys(savedPresets).includes(presetName));
|
||||||
|
if (!Object.keys(savedPresets).includes(presetName)) return false;
|
||||||
|
else return true;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Convert a radioPreset to OP25's cfg.json file
|
||||||
|
*/
|
||||||
|
function convertRadioPresetsToOP25Config(presetName){
|
||||||
|
const savedPresets = presetWrappers.getPresets();
|
||||||
|
let frequencyString = "";
|
||||||
|
for (const frequency of savedPresets[presetName].frequencies){
|
||||||
|
frequencyString += `${converter(frequency).from("Hz").to("MHz")},`
|
||||||
|
}
|
||||||
|
frequencyString = frequencyString.slice(0, -1);
|
||||||
|
|
||||||
|
let updatedOP25Config;
|
||||||
|
switch (savedPresets[presetName].mode){
|
||||||
|
case "p25":
|
||||||
|
updatedOP25Config = new radioConfigHelper.P25({
|
||||||
|
"systemName": presetName,
|
||||||
|
"controlChannelsString": frequencyString,
|
||||||
|
"tagsFile": savedPresets[presetName].trunkFile
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
case "nbfm":
|
||||||
|
//code for nbfm here
|
||||||
|
updatedOP25Config = new radioConfigHelper.NBFM({
|
||||||
|
"frequency": frequencyString,
|
||||||
|
"systemName": presetName
|
||||||
|
});
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
throw new Error("Radio mode of selected preset not recognized");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.DEBUG(updatedOP25Config);
|
||||||
|
return updatedOP25Config;
|
||||||
|
}
|
||||||
|
|
||||||
|
// 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;
|
||||||
|
}
|
||||||
@@ -1,11 +1,169 @@
|
|||||||
<!DOCTYPE html>
|
<!DOCTYPE html>
|
||||||
<html>
|
<html lang="en" data-bs-theme="auto">
|
||||||
<head>
|
|
||||||
<title><%= title %></title>
|
<head>
|
||||||
<link rel='stylesheet' href='/stylesheets/style.css' />
|
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||||
</head>
|
<title>'<%=node.name%>' - Configuration</title>
|
||||||
<body>
|
<link href="https://cdn.jsdelivr.net/npm/bootstrap@5.3.0/dist/css/bootstrap.min.css" rel="stylesheet"
|
||||||
<h1><%= title %></h1>
|
integrity="sha384-9ndCyUaIbzAi2FUVXJi0CjmCapSmO7SnpJef0486qhLnuZ2cdeRhO02iuK6FUUVM" crossorigin="anonymous">
|
||||||
<p>Welcome to <%= title %></p>
|
<link rel="stylesheet" href="https://cdn.jsdelivr.net/npm/bootstrap-icons@1.3.0/font/bootstrap-icons.css">
|
||||||
</body>
|
<link rel="stylesheet" href="/res/css/main.css">
|
||||||
</html>
|
</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> </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>
|
||||||
44
Client/views/partials/joinModal.ejs
Normal file
44
Client/views/partials/joinModal.ejs
Normal file
@@ -0,0 +1,44 @@
|
|||||||
|
<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>
|
||||||
61
Client/views/partials/modifySystemModal.ejs
Normal file
61
Client/views/partials/modifySystemModal.ejs
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
<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>
|
||||||
42
Client/views/partials/navbar.ejs
Normal file
42
Client/views/partials/navbar.ejs
Normal file
@@ -0,0 +1,42 @@
|
|||||||
|
<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>
|
||||||
@@ -1 +0,0 @@
|
|||||||
DEBUG="server:*";
|
|
||||||
58
Server/.env.example
Normal file
58
Server/.env.example
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
# Discord Bot Configs
|
||||||
|
# Bot Token
|
||||||
|
TOKEN=""
|
||||||
|
# Client ID
|
||||||
|
clientId=""
|
||||||
|
# Prefix (deprecated)
|
||||||
|
PREFIX="^"
|
||||||
|
# ID of the Group than Can Admin The Bot
|
||||||
|
BOT_ADMINS=""
|
||||||
|
# Default Voice Channel to Join if None are Specified
|
||||||
|
DEFAULT_VOICE_CHANNEL_ID=""
|
||||||
|
|
||||||
|
# HTTP Server Config (DRB_CNC)
|
||||||
|
# HTTP Port to listen on
|
||||||
|
HTTP_PORT=3000
|
||||||
|
|
||||||
|
# MySQL Config for Emmelia
|
||||||
|
# Core DB Info and Login
|
||||||
|
EM_DB_HOST=""
|
||||||
|
EM_DB_USER=""
|
||||||
|
EM_DB_PASS=""
|
||||||
|
EM_DB_NAME=""
|
||||||
|
# Names of DB Tables
|
||||||
|
DB_RSS_FEEDS_TABLE="RSSFeeds"
|
||||||
|
DB_RSS_POSTS_TABLE="RSSPosts"
|
||||||
|
DB_ACCOUNTS_TABLE="accounts"
|
||||||
|
DB_TRANSACTIONS_TABLE="transactions"
|
||||||
|
DB_PRICING_TABLE="pricing"
|
||||||
|
|
||||||
|
# MySQL Config for Node Control
|
||||||
|
NODE_DB_HOST=''
|
||||||
|
NODE_DB_USER=''
|
||||||
|
NODE_DB_PASS=''
|
||||||
|
NODE_DB_NAME=''
|
||||||
|
|
||||||
|
# Node Config
|
||||||
|
# Time betwen check ins with the nodes
|
||||||
|
NODE_MONITOR_REFRESH_INTERVAL=100000
|
||||||
|
|
||||||
|
# RSS Config
|
||||||
|
# Interval between refreshes
|
||||||
|
RSS_REFRESH_INTERVAL=3000000
|
||||||
|
|
||||||
|
# OpenAI Config
|
||||||
|
# OpenAI Organization ID
|
||||||
|
OPENAI_ORG=""
|
||||||
|
# OpenAI API Key
|
||||||
|
OPENAI_KEY=""
|
||||||
|
|
||||||
|
# Stable Diffusion (Stability AI) Config
|
||||||
|
# API KEY
|
||||||
|
STABILITY_API_KEY=""
|
||||||
|
|
||||||
|
# General Config
|
||||||
|
# Exit when the program encounters and error (this may be ignored in some instances, and the error will exit the program either way)
|
||||||
|
EXIT_ON_ERROR=false
|
||||||
|
# Delay the exit of the program for X miliseconds, this can be used if you want to see what happens just after the error occurs or see if something else errors
|
||||||
|
EXIT_ON_ERROR_DELAY=0
|
||||||
7
Server/.gitignore
vendored
Normal file
7
Server/.gitignore
vendored
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
node_modules/
|
||||||
|
.env
|
||||||
|
package-lock.json
|
||||||
|
*.bak
|
||||||
|
*.log
|
||||||
|
*._.*
|
||||||
|
clientIds.json
|
||||||
15
Server/LICENSE.md
Normal file
15
Server/LICENSE.md
Normal file
@@ -0,0 +1,15 @@
|
|||||||
|
ISC License
|
||||||
|
|
||||||
|
Copyright (c) 2021
|
||||||
|
|
||||||
|
Permission to use, copy, modify, and/or distribute this software for any
|
||||||
|
purpose with or without fee is hereby granted, provided that the above
|
||||||
|
copyright notice and this permission notice appear in all copies.
|
||||||
|
|
||||||
|
THE SOFTWARE IS PROVIDED "AS IS" AND THE AUTHOR DISCLAIMS ALL WARRANTIES
|
||||||
|
WITH REGARD TO THIS SOFTWARE INCLUDING ALL IMPLIED WARRANTIES OF
|
||||||
|
MERCHANTABILITY AND FITNESS. IN NO EVENT SHALL THE AUTHOR BE LIABLE FOR
|
||||||
|
ANY SPECIAL, DIRECT, INDIRECT, OR CONSEQUENTIAL DAMAGES OR ANY DAMAGES
|
||||||
|
WHATSOEVER RESULTING FROM LOSS OF USE, DATA OR PROFITS, WHETHER IN AN
|
||||||
|
ACTION OF CONTRACT, NEGLIGENCE OR OTHER TORTIOUS ACTION, ARISING OUT OF
|
||||||
|
OR IN CONNECTION WITH THE USE OR PERFORMANCE OF THIS SOFTWARE.
|
||||||
@@ -1,48 +0,0 @@
|
|||||||
var createError = require('http-errors');
|
|
||||||
var express = require('express');
|
|
||||||
var path = require('path');
|
|
||||||
var cookieParser = require('cookie-parser');
|
|
||||||
var logger = require('morgan');
|
|
||||||
|
|
||||||
var indexRouter = require('./routes/index');
|
|
||||||
var nodesRouter = require('./routes/nodes');
|
|
||||||
var adminRouter = require('./routes/admin');
|
|
||||||
|
|
||||||
var app = express();
|
|
||||||
|
|
||||||
// view engine setup
|
|
||||||
app.set('views', path.join(__dirname, 'views'));
|
|
||||||
app.set('view engine', 'ejs');
|
|
||||||
|
|
||||||
app.use(logger('dev'));
|
|
||||||
app.use(express.json());
|
|
||||||
app.use(express.urlencoded({ extended: false }));
|
|
||||||
app.use(cookieParser());
|
|
||||||
app.use(express.static(path.join(__dirname, 'public')));
|
|
||||||
|
|
||||||
// Web Interface
|
|
||||||
app.use('/', indexRouter);
|
|
||||||
|
|
||||||
// Nodes API
|
|
||||||
app.use('/nodes', nodesRouter);
|
|
||||||
|
|
||||||
// Admin API
|
|
||||||
app.use('/admin', adminRouter);
|
|
||||||
|
|
||||||
// catch 404 and forward to error handler
|
|
||||||
app.use((req, res, next) => {
|
|
||||||
next(createError(404));
|
|
||||||
});
|
|
||||||
|
|
||||||
// error handler
|
|
||||||
app.use((err, req, res, next) => {
|
|
||||||
// set locals, only providing error in development
|
|
||||||
res.locals.message = err.message;
|
|
||||||
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
|
||||||
|
|
||||||
// render the error page
|
|
||||||
res.status(err.status || 500);
|
|
||||||
res.render('error');
|
|
||||||
});
|
|
||||||
|
|
||||||
module.exports = app;
|
|
||||||
@@ -1,94 +0,0 @@
|
|||||||
#!/usr/bin/env node
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Module dependencies.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var app = require('../app');
|
|
||||||
// Debug
|
|
||||||
const debug = require('debug')('server');
|
|
||||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
|
||||||
const log = new DebugBuilder("server", "www");
|
|
||||||
var http = require('http');
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Get port from environment and store in Express.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var port = normalizePort(process.env.PORT || '3000');
|
|
||||||
app.set('port', port);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Create HTTP server.
|
|
||||||
*/
|
|
||||||
|
|
||||||
var server = http.createServer(app);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Listen on provided port, on all network interfaces.
|
|
||||||
*/
|
|
||||||
|
|
||||||
server.listen(port);
|
|
||||||
server.on('error', onError);
|
|
||||||
server.on('listening', onListening);
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Normalize a port into a number, string, or false.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function normalizePort(val) {
|
|
||||||
var port = parseInt(val, 10);
|
|
||||||
|
|
||||||
if (isNaN(port)) {
|
|
||||||
// named pipe
|
|
||||||
return val;
|
|
||||||
}
|
|
||||||
|
|
||||||
if (port >= 0) {
|
|
||||||
// port number
|
|
||||||
return port;
|
|
||||||
}
|
|
||||||
|
|
||||||
return false;
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event listener for HTTP server "error" event.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function onError(error) {
|
|
||||||
if (error.syscall !== 'listen') {
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
|
|
||||||
var bind = typeof port === 'string'
|
|
||||||
? 'Pipe ' + port
|
|
||||||
: 'Port ' + port;
|
|
||||||
|
|
||||||
// handle specific listen errors with friendly messages
|
|
||||||
switch (error.code) {
|
|
||||||
case 'EACCES':
|
|
||||||
console.error(bind + ' requires elevated privileges');
|
|
||||||
process.exit(1);
|
|
||||||
break;
|
|
||||||
case 'EADDRINUSE':
|
|
||||||
console.error(bind + ' is already in use');
|
|
||||||
process.exit(1);
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
throw error;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Event listener for HTTP server "listening" event.
|
|
||||||
*/
|
|
||||||
|
|
||||||
function onListening() {
|
|
||||||
var addr = server.address();
|
|
||||||
var bind = typeof addr === 'string'
|
|
||||||
? 'pipe ' + addr
|
|
||||||
: 'port ' + addr.port;
|
|
||||||
log.DEBUG('Listening on ' + bind);
|
|
||||||
debug("testing");
|
|
||||||
}
|
|
||||||
6
Server/clientIds.json.EXAMPLE
Normal file
6
Server/clientIds.json.EXAMPLE
Normal file
@@ -0,0 +1,6 @@
|
|||||||
|
{
|
||||||
|
"[ID from Discord]": {
|
||||||
|
"name": "[Nickname of the Bot]",
|
||||||
|
"id": "[Client ID from Discord Dev Portal]"
|
||||||
|
}
|
||||||
|
}
|
||||||
47
Server/commands/add.js
Normal file
47
Server/commands/add.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
const libCore = require("../libCore.js");
|
||||||
|
const { SlashCommandBuilder } = require('discord.js');
|
||||||
|
|
||||||
|
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||||
|
const log = new DebugBuilder("server", "add");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('add')
|
||||||
|
.setDescription('Add RSS Source')
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('title')
|
||||||
|
.setDescription('The title of the RSS feed')
|
||||||
|
.setRequired(true))
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('link')
|
||||||
|
.setDescription('The link to the RSS feed')
|
||||||
|
.setRequired(true))
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('category')
|
||||||
|
.setDescription('The category for the RSS feed *("ALL" by default")*')
|
||||||
|
.setRequired(false)),
|
||||||
|
example: "add [title] [https://domain.com/feed.xml] [category]",
|
||||||
|
isPrivileged: false,
|
||||||
|
async execute(interaction, args) {
|
||||||
|
try {
|
||||||
|
var title = interaction.options.getString('title');
|
||||||
|
var link = interaction.options.getString('link');
|
||||||
|
var category = interaction.options.getString('category');
|
||||||
|
|
||||||
|
if (!category) category = "ALL";
|
||||||
|
|
||||||
|
await libCore.addSource(title, link, category, interaction.guildId, interaction.channelId, (err, result) => {
|
||||||
|
log.DEBUG("Result from adding entry", result);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
interaction.reply(`Adding ${title} to the list of RSS sources`);
|
||||||
|
} else {
|
||||||
|
interaction.reply(`${title} already exists in the list of RSS sources`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}catch(err){
|
||||||
|
log.ERROR(err)
|
||||||
|
await interaction.reply(err.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
28
Server/commands/balance.js
Normal file
28
Server/commands/balance.js
Normal file
@@ -0,0 +1,28 @@
|
|||||||
|
const { SlashCommandBuilder } = require('discord.js');
|
||||||
|
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||||
|
const log = new DebugBuilder("server", "balance");
|
||||||
|
|
||||||
|
const { checkBalance } = require("../controllers/accountController");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('balance')
|
||||||
|
.setDescription('Check your balance of AI tokens'),
|
||||||
|
example: "balance",
|
||||||
|
isPrivileged: false,
|
||||||
|
requiresTokens: false,
|
||||||
|
defaultTokenUsage: 0,
|
||||||
|
deferInitialReply: false,
|
||||||
|
async execute(interaction) {
|
||||||
|
try{
|
||||||
|
checkBalance(interaction.member.id, async (err, balance) => {
|
||||||
|
if (err) throw err;
|
||||||
|
|
||||||
|
await interaction.reply({ content: `${interaction.member.user}, you have ${balance} tokens remaining`, ephemeral: true })
|
||||||
|
})
|
||||||
|
}catch(err){
|
||||||
|
log.ERROR(err)
|
||||||
|
await interaction.reply(`Sorry ${interaction.member.user}, something went wrong`);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
27
Server/commands/category.js
Normal file
27
Server/commands/category.js
Normal file
@@ -0,0 +1,27 @@
|
|||||||
|
var libCore = require("../libCore.js");
|
||||||
|
const { SlashCommandBuilder } = require('discord.js');
|
||||||
|
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||||
|
const log = new DebugBuilder("server", "categories");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('categories')
|
||||||
|
.setDescription('Return all categories'),
|
||||||
|
example: "categories",
|
||||||
|
isPrivileged: false,
|
||||||
|
async execute(interaction) {
|
||||||
|
await libCore.getCategories(async (err, categoryResults) => {
|
||||||
|
if (err) throw err;
|
||||||
|
|
||||||
|
log.DEBUG("Returned Categories: ", categoryResults);
|
||||||
|
var categories = [];
|
||||||
|
for (const record of categoryResults) {
|
||||||
|
categories.push(record.category);
|
||||||
|
}
|
||||||
|
|
||||||
|
await interaction.reply(
|
||||||
|
`Categories: [${categories}]`
|
||||||
|
);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
};
|
||||||
61
Server/commands/chat.js
Normal file
61
Server/commands/chat.js
Normal file
@@ -0,0 +1,61 @@
|
|||||||
|
const { submitTextPromptTransaction } = require("../controllers/openAiController");
|
||||||
|
const { SlashCommandBuilder } = require('discord.js');
|
||||||
|
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||||
|
const log = new DebugBuilder("server", "chat");
|
||||||
|
const { EmmeliaEmbedBuilder } = require('../libUtils');
|
||||||
|
|
||||||
|
const COST_OF_COMMAND = 100
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('chat')
|
||||||
|
.setDescription(`Send a text prompt to ChatGPT`)
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('prompt')
|
||||||
|
.setDescription('The prompt to be sent to ChatGPT')
|
||||||
|
.setRequired(true))
|
||||||
|
.addBooleanOption(option =>
|
||||||
|
option.setName('public')
|
||||||
|
.setDescription("Set this to false if you would like the message to only be visible to you. *defaults to public*")
|
||||||
|
.setRequired(false))
|
||||||
|
.addNumberOption(option =>
|
||||||
|
option.setName('temperature')
|
||||||
|
.setDescription('Set the temperature, 0 = repetitive, 1 = random; Defaults to 0')
|
||||||
|
.setRequired(false))
|
||||||
|
.addNumberOption(option =>
|
||||||
|
option.setName('tokens')
|
||||||
|
.setDescription(`The max amount of tokens to be spent, defaults to ${COST_OF_COMMAND}`)
|
||||||
|
.setRequired(false)),
|
||||||
|
example: "chat [tell me a story] [0.07] [400]", // Need to figure out the tokens
|
||||||
|
isPrivileged: false,
|
||||||
|
requiresTokens: true,
|
||||||
|
defaultTokenUsage: 100,
|
||||||
|
deferInitialReply: true,
|
||||||
|
async execute(interaction) {
|
||||||
|
const promptText = interaction.options.getString('prompt');
|
||||||
|
const temperature = interaction.options.getNumber('temperature') ?? undefined;
|
||||||
|
const maxTokens = interaction.options.getNumber('tokens') ?? undefined;
|
||||||
|
const discordAccountId = interaction.member.id;
|
||||||
|
try {
|
||||||
|
submitTextPromptTransaction(promptText, temperature, maxTokens, discordAccountId, interaction, this, async (err, result) => {
|
||||||
|
if (err) throw err;
|
||||||
|
|
||||||
|
const gptEmbed = new EmmeliaEmbedBuilder()
|
||||||
|
.setColor(0x0099FF)
|
||||||
|
.setTitle(`New GPT response`)
|
||||||
|
.setDescription(`${interaction.member.user} sent: '${promptText}'`)
|
||||||
|
.addFields(
|
||||||
|
{ name: 'Generated Text', value: result.promptResult },
|
||||||
|
)
|
||||||
|
.addFields({ name: 'Tokens Used', value: `${result.totalTokens}`, inline: true })
|
||||||
|
|
||||||
|
await interaction.editReply({ embeds: [gptEmbed], ephemeral: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Needs reply code to reply to the generation
|
||||||
|
}catch(err){
|
||||||
|
log.ERROR(err)
|
||||||
|
//await interaction.reply(err.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
18
Server/commands/exit.js
Normal file
18
Server/commands/exit.js
Normal file
@@ -0,0 +1,18 @@
|
|||||||
|
const libUtils = require("../libUtils.js");
|
||||||
|
const discordAuth = require("../middleware/discordAuthorization");
|
||||||
|
const { SlashCommandBuilder } = require('discord.js');
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('exit')
|
||||||
|
.setDescription('Exit the current application.'),
|
||||||
|
example: "exit",
|
||||||
|
isPrivileged: true,
|
||||||
|
async execute(interaction) {
|
||||||
|
// TODO - Need to add middleware for admins
|
||||||
|
await interaction.reply(
|
||||||
|
`Goodbye world - Disconnection imminent.`
|
||||||
|
);
|
||||||
|
libUtils.runAfter(process.exit, 5000);
|
||||||
|
}
|
||||||
|
};
|
||||||
52
Server/commands/give-role.js
Normal file
52
Server/commands/give-role.js
Normal file
@@ -0,0 +1,52 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
63
Server/commands/help.js
Normal file
63
Server/commands/help.js
Normal file
@@ -0,0 +1,63 @@
|
|||||||
|
const fs = require('fs');
|
||||||
|
const path = require('node:path');
|
||||||
|
const { SlashCommandBuilder } = require('discord.js');
|
||||||
|
|
||||||
|
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||||
|
const log = new DebugBuilder("server", "help");
|
||||||
|
|
||||||
|
const { EmmeliaEmbedBuilder } = require("../libUtils");
|
||||||
|
|
||||||
|
const commandsPath = path.resolve(__dirname, '../commands'); // Resolves from either working dir or __dirname
|
||||||
|
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('help')
|
||||||
|
.setDescription('Display this help message'),
|
||||||
|
example: "help",
|
||||||
|
isPrivileged: false,
|
||||||
|
async execute(interaction) {
|
||||||
|
try{
|
||||||
|
generalCommandText = "";
|
||||||
|
paidCommandText = "";
|
||||||
|
|
||||||
|
for (const file of commandFiles) {
|
||||||
|
const filePath = path.join(commandsPath, file);
|
||||||
|
const command = require(filePath);
|
||||||
|
|
||||||
|
if (!command.isPrivileged){ // TODO - Need to add middleware for admins
|
||||||
|
if (!command.requiresTokens){
|
||||||
|
if (generalCommandText.length > 1 && generalCommandText.slice(-2) != `\n`){
|
||||||
|
generalCommandText += `\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
generalCommandText += `**/${command.data.name}** - *${command.data.description}*`;
|
||||||
|
|
||||||
|
if (command.example) generalCommandText += `\n\t\t***Usage:*** \`/${command.example}\``
|
||||||
|
}
|
||||||
|
else{
|
||||||
|
if (paidCommandText.length > 1 && paidCommandText.slice(-2) != `\n`){
|
||||||
|
paidCommandText += `\n\n`;
|
||||||
|
}
|
||||||
|
|
||||||
|
paidCommandText += `**/${command.data.name}** - *${command.data.description}*`;
|
||||||
|
|
||||||
|
if (command.example) paidCommandText += `\n\t\t***Usage:*** \`/${command.example}\``
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
const helpEmbed = new EmmeliaEmbedBuilder()
|
||||||
|
.setColor(0x0099FF)
|
||||||
|
.setTitle(`Help`)
|
||||||
|
.setDescription(`**General Commands**\n\n${generalCommandText}`)
|
||||||
|
.addFields(
|
||||||
|
{ name: 'Paid Commands', value: `${paidCommandText}` }
|
||||||
|
)
|
||||||
|
await interaction.reply({ embeds: [helpEmbed], ephemeral: true });
|
||||||
|
}catch(err){
|
||||||
|
log.ERROR(err)
|
||||||
|
//await interaction.reply(err.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
81
Server/commands/imagine.js
Normal file
81
Server/commands/imagine.js
Normal file
@@ -0,0 +1,81 @@
|
|||||||
|
const { submitImagePromptTransaction, DALLE_COLOR } = require("../controllers/openAiController");
|
||||||
|
const { SlashCommandBuilder } = require('discord.js');
|
||||||
|
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||||
|
const log = new DebugBuilder("server", "imagine");
|
||||||
|
const { EmmeliaEmbedBuilder } = require('../libUtils');
|
||||||
|
|
||||||
|
const COST_OF_COMMAND = 800;
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('imagine')
|
||||||
|
.setDescription(`Submit an image generation prompt to DALL-E`)
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('prompt')
|
||||||
|
.setDescription('The prompt to be sent to DALL-E')
|
||||||
|
.setRequired(true))
|
||||||
|
.addBooleanOption(option =>
|
||||||
|
option.setName('public')
|
||||||
|
.setDescription("Set this to false if you would like the message to only be visible to you. *defaults to public*")
|
||||||
|
.setRequired(false))
|
||||||
|
.addNumberOption(option =>
|
||||||
|
option.setName('images')
|
||||||
|
.setDescription('The number of images you wish to generate [1 - 10] *(defaults to 1)*')
|
||||||
|
.setRequired(false))
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('size')
|
||||||
|
.setDescription('The size of the images to be generated *defaults to 256px*')
|
||||||
|
.addChoices(
|
||||||
|
{ name: '1024px - 1000 tokens', value: '1024x1024' },
|
||||||
|
{ name: '512px - 900 tokens', value: '512x512' },
|
||||||
|
{ name: '256px - 800 tokens', value: '256x256' },
|
||||||
|
)
|
||||||
|
.setRequired(false)),
|
||||||
|
example: "imagine [the sinking of the titanic on acid] [4] [", // Need to figure out the tokens
|
||||||
|
isPrivileged: false,
|
||||||
|
requiresTokens: true,
|
||||||
|
defaultTokenUsage: COST_OF_COMMAND,
|
||||||
|
deferInitialReply: true,
|
||||||
|
async execute(interaction) {
|
||||||
|
const promptText = interaction.options.getString('prompt');
|
||||||
|
const images = interaction.options.getNumber('images') ?? undefined;
|
||||||
|
const size = interaction.options.getString('size') ?? undefined;
|
||||||
|
const discordAccountId = interaction.member.id;
|
||||||
|
try {
|
||||||
|
submitImagePromptTransaction(promptText, discordAccountId, images, size, interaction, this, async (err, imageResults) => {
|
||||||
|
if (err) throw err;
|
||||||
|
|
||||||
|
var dalleEmbeds = [];
|
||||||
|
log.DEBUG("Image Results: ", imageResults)
|
||||||
|
// Add the information post
|
||||||
|
dalleEmbeds.push(new EmmeliaEmbedBuilder()
|
||||||
|
.setColor(DALLE_COLOR)
|
||||||
|
.setTitle(`New Image Result`)
|
||||||
|
.setDescription(`${interaction.member.user} sent the prompt: '${promptText}'`)
|
||||||
|
);
|
||||||
|
// Add the images to the result
|
||||||
|
const imagesInResult = Array(imageResults.results).length
|
||||||
|
log.DEBUG("Images in the result: ", imagesInResult);
|
||||||
|
if (imagesInResult >= 1) {
|
||||||
|
for (const imageData of imageResults.results.data){
|
||||||
|
const imageUrl = imageData.url;
|
||||||
|
dalleEmbeds.push(new EmmeliaEmbedBuilder().setURL(imageUrl).setImage(imageUrl).setColor(DALLE_COLOR));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
// Add the information post
|
||||||
|
dalleEmbeds.push(new EmmeliaEmbedBuilder()
|
||||||
|
.setColor(DALLE_COLOR)
|
||||||
|
.addFields({ name: 'Tokens Used', value: `${imageResults.totalTokens}`, inline: true })
|
||||||
|
.addFields({ name: 'Images Generated', value: `${imagesInResult}`, inline: true })
|
||||||
|
.addFields({ name: 'Image Size Requested', value: `${imagesInResult}`, inline: true })
|
||||||
|
);
|
||||||
|
await interaction.editReply({ embeds: dalleEmbeds, ephemeral: false });
|
||||||
|
});
|
||||||
|
|
||||||
|
// Needs reply code to reply to the generation
|
||||||
|
}catch(err){
|
||||||
|
log.ERROR(err)
|
||||||
|
//await interaction.reply(err.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
58
Server/commands/join.js
Normal file
58
Server/commands/join.js
Normal file
@@ -0,0 +1,58 @@
|
|||||||
|
// Modules
|
||||||
|
const { SlashCommandBuilder } = require('discord.js');
|
||||||
|
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||||
|
const { filterAutocompleteValues, filterPresetsAvailable } = require("../utilities/utils");
|
||||||
|
const { getOnlineNodes, getAllConnections } = require("../utilities/mysqlHandler");
|
||||||
|
const { joinServerWrapper } = require("../controllers/adminController");
|
||||||
|
|
||||||
|
// Global Vars
|
||||||
|
const log = new DebugBuilder("server", "join");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.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)),
|
||||||
|
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');
|
||||||
|
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();
|
||||||
|
|
||||||
|
log.DEBUG("Current Connections: ", connections);
|
||||||
|
|
||||||
|
const selectedClientId = await joinServerWrapper(presetName, channelId, connections);
|
||||||
|
|
||||||
|
await interaction.editReply(`Ok, ${interaction.member}. **${selectedClientId.name}** is joining your channel.`);
|
||||||
|
//await interaction.channel.send('**Pong.**'); // This will send a message to the channel of the interaction outside of the initial reply
|
||||||
|
}catch(err){
|
||||||
|
log.ERROR(err)
|
||||||
|
//await interaction.reply(err.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
47
Server/commands/leave.js
Normal file
47
Server/commands/leave.js
Normal file
@@ -0,0 +1,47 @@
|
|||||||
|
// Modules
|
||||||
|
const { SlashCommandBuilder } = require('discord.js');
|
||||||
|
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||||
|
const { getAllClientIds, getKeyByArrayValue, filterAutocompleteValues } = require("../utilities/utils");
|
||||||
|
const { getAllConnections } = require('../utilities/mysqlHandler');
|
||||||
|
const { leaveServerWrapper } = require('../controllers/adminController');
|
||||||
|
|
||||||
|
// Global Vars
|
||||||
|
const log = new DebugBuilder("server", "leave");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('leave')
|
||||||
|
.setDescription('Disconnect a bot from the server')
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName("bot")
|
||||||
|
.setDescription("The bot to disconnect from the server")
|
||||||
|
.setAutocomplete(true)
|
||||||
|
.setRequired(true)),
|
||||||
|
example: "leave",
|
||||||
|
isPrivileged: false,
|
||||||
|
requiresTokens: false,
|
||||||
|
defaultTokenUsage: 0,
|
||||||
|
deferInitialReply: true,
|
||||||
|
async autocomplete(interaction) {
|
||||||
|
const connections = await getAllConnections();
|
||||||
|
const options = connections.map(conn => conn.clientObject.name);
|
||||||
|
await filterAutocompleteValues(interaction, options);
|
||||||
|
},
|
||||||
|
async execute(interaction) {
|
||||||
|
try{
|
||||||
|
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]);
|
||||||
|
await leaveServerWrapper(clinetIds[clientDiscordId]);
|
||||||
|
|
||||||
|
await interaction.editReply(`**${clinetIds[clientDiscordId].name}** has been disconnected`); // This will reply to the initial interaction
|
||||||
|
//await interaction.channel.send('**word.**'); // This will send a message to the channel of the interaction outside of the initial reply
|
||||||
|
}catch(err){
|
||||||
|
log.ERROR(err)
|
||||||
|
//await interaction.reply(err.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -1,8 +1,6 @@
|
|||||||
// Utilities
|
|
||||||
const { replyToInteraction } = require('../utilities/messageHandler.js');
|
|
||||||
const { SlashCommandBuilder } = require('discord.js');
|
const { SlashCommandBuilder } = require('discord.js');
|
||||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||||
const log = new DebugBuilder("client", "ping");
|
const log = new DebugBuilder("server", "ping");
|
||||||
|
|
||||||
module.exports = {
|
module.exports = {
|
||||||
data: new SlashCommandBuilder()
|
data: new SlashCommandBuilder()
|
||||||
@@ -17,9 +15,15 @@ module.exports = {
|
|||||||
*/
|
*/
|
||||||
example: "ping",
|
example: "ping",
|
||||||
isPrivileged: false,
|
isPrivileged: false,
|
||||||
|
requiresTokens: false,
|
||||||
|
defaultTokenUsage: 0,
|
||||||
|
deferInitialReply: false,
|
||||||
|
/*async autocomplete(interaction) {
|
||||||
|
const focusedValue = interaction.options.getFocused();
|
||||||
|
},*/
|
||||||
async execute(interaction) {
|
async execute(interaction) {
|
||||||
try{
|
try{
|
||||||
await replyToInteraction(interaction, "Pong! I have Aids and now you do too!"); // TODO - Add insults as the response to this command
|
await interaction.channel.send('**Pong.**'); // TODO - Add insults as the response to this command
|
||||||
}catch(err){
|
}catch(err){
|
||||||
log.ERROR(err)
|
log.ERROR(err)
|
||||||
//await interaction.reply(err.toString());
|
//await interaction.reply(err.toString());
|
||||||
34
Server/commands/pricing.js
Normal file
34
Server/commands/pricing.js
Normal file
@@ -0,0 +1,34 @@
|
|||||||
|
const { SlashCommandBuilder } = require('discord.js');
|
||||||
|
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||||
|
const log = new DebugBuilder("server", "pricing");
|
||||||
|
|
||||||
|
const { EmmeliaEmbedBuilder } = require("../libUtils");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('pricing')
|
||||||
|
.setDescription('Replies with the pricing for tokens'),
|
||||||
|
example: "pricing",
|
||||||
|
isPrivileged: false,
|
||||||
|
requiresTokens: false,
|
||||||
|
defaultTokenUsage: 0,
|
||||||
|
deferInitialReply: false,
|
||||||
|
async execute(interaction) {
|
||||||
|
try{
|
||||||
|
const pricingEmbed = new EmmeliaEmbedBuilder()
|
||||||
|
.setColor(0x0099FF)
|
||||||
|
.setTitle(`Emmelia's Pricing`)
|
||||||
|
.addFields(
|
||||||
|
{ name: 'Tokens', value: `Tokens are a shared currency that is used between all AI models. Each model is charges tokens differently however, so do keep this in mind. $1 = 45,000 tokens` },
|
||||||
|
{ name: 'Text (ChatGPT)', value: `Tokens are used in the prompt and in the response of a generation. The max tokens will not be breached by the combined prompt and response. Keep this is mind when using text generations. Each syllable is one token. This section is 50 tokens.` },
|
||||||
|
{ name: 'Images (DALL-E)', value: `Tokens are used for each generation, variation, and upscale. The image size also affects the amount of tokens used: 256px = 800 tokens, 512px = 900 tokens, 1024px = 1000 tokens` }
|
||||||
|
)
|
||||||
|
|
||||||
|
await interaction.reply({ embeds: [pricingEmbed] });
|
||||||
|
|
||||||
|
}catch(err){
|
||||||
|
log.ERROR(err)
|
||||||
|
//await interaction.reply(err.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
36
Server/commands/remove.js
Normal file
36
Server/commands/remove.js
Normal file
@@ -0,0 +1,36 @@
|
|||||||
|
var libCore = require("../libCore.js");
|
||||||
|
|
||||||
|
const { SlashCommandBuilder } = require('discord.js');
|
||||||
|
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||||
|
const log = new DebugBuilder("server", "remove");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('remove')
|
||||||
|
.setDescription('Remove an RSS source by it\'s title')
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('title')
|
||||||
|
.setDescription('The title of the source to remove')
|
||||||
|
.setRequired(true)),
|
||||||
|
example: "remove ['Leafly']",
|
||||||
|
isPrivileged: false,
|
||||||
|
requiresTokens: false,
|
||||||
|
async execute(interaction) {
|
||||||
|
try{
|
||||||
|
var title = interaction.options.getString("title");
|
||||||
|
|
||||||
|
libCore.deleteSource(title, (err, result) => {
|
||||||
|
log.DEBUG("Result from removing entry", result);
|
||||||
|
|
||||||
|
if (result) {
|
||||||
|
interaction.reply(`Removing ${title} from the list of RSS sources`);
|
||||||
|
} else {
|
||||||
|
interaction.reply(`${title} does not exist in the list of RSS sources`);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}catch(err){
|
||||||
|
log.ERROR(err)
|
||||||
|
interaction.reply(err.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
29
Server/commands/sources.js
Normal file
29
Server/commands/sources.js
Normal file
@@ -0,0 +1,29 @@
|
|||||||
|
var libCore = require("../libCore.js");
|
||||||
|
|
||||||
|
const { SlashCommandBuilder } = require('discord.js');
|
||||||
|
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||||
|
const log = new DebugBuilder("server", "sources");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('sources')
|
||||||
|
.setDescription('Reply with all of the available sources'),
|
||||||
|
example: "sources",
|
||||||
|
isPrivileged: false,
|
||||||
|
requiresTokens: false,
|
||||||
|
async execute(interaction) {
|
||||||
|
try{
|
||||||
|
var sourceArray = libCore.getSources();
|
||||||
|
var sourceString = "";
|
||||||
|
|
||||||
|
sourceArray.forEach(source => {
|
||||||
|
sourceString +=`[${source.title}](${source.link}) \n`;
|
||||||
|
});
|
||||||
|
|
||||||
|
await interaction.reply(sourceString);
|
||||||
|
}catch(err){
|
||||||
|
log.ERROR(err)
|
||||||
|
//await interaction.reply(err.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
37
Server/commands/start-record.js
Normal file
37
Server/commands/start-record.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
37
Server/commands/stop-record.js
Normal file
37
Server/commands/stop-record.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
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());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
37
Server/commands/weather.js
Normal file
37
Server/commands/weather.js
Normal file
@@ -0,0 +1,37 @@
|
|||||||
|
var libCore = require("../libCore.js");
|
||||||
|
|
||||||
|
const { SlashCommandBuilder } = require('discord.js');
|
||||||
|
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||||
|
const log = new DebugBuilder("server", "alert");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
data: new SlashCommandBuilder()
|
||||||
|
.setName('weather')
|
||||||
|
.setDescription('Get any current weather alerts in the state specified.')
|
||||||
|
.addStringOption(option =>
|
||||||
|
option.setName('state')
|
||||||
|
.setDescription('The state to get any current weather alerts from')
|
||||||
|
.setRequired(false)
|
||||||
|
.addChoices()),
|
||||||
|
example: "alert [state]",
|
||||||
|
isPrivileged: false,
|
||||||
|
requiresTokens: false,
|
||||||
|
async execute(interaction) {
|
||||||
|
try{
|
||||||
|
var question = encodeURIComponent(interaction.options.getString("state").join(" "));
|
||||||
|
|
||||||
|
var answerData = await libCore.weatherAlert(question);
|
||||||
|
answerData.forEach(feature => {
|
||||||
|
interaction.reply(`
|
||||||
|
${feature.properties.areaDesc}
|
||||||
|
${feature.properties.headline}
|
||||||
|
${feature.properties.description}
|
||||||
|
`);
|
||||||
|
});
|
||||||
|
|
||||||
|
}catch(err){
|
||||||
|
log.ERROR(err)
|
||||||
|
//await interaction.reply(err.toString());
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
@@ -1,8 +0,0 @@
|
|||||||
const databaseConfig = {
|
|
||||||
database_host: '100.20.1.45',
|
|
||||||
database_user: 'DRB_CNC',
|
|
||||||
database_password: 'baMbC6IAl$Rn7$h0PS',
|
|
||||||
database_database: 'DRB_CNC'
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = databaseConfig;
|
|
||||||
@@ -1,5 +0,0 @@
|
|||||||
const discordConfig = {
|
|
||||||
channelID: '367396189529833476'
|
|
||||||
}
|
|
||||||
|
|
||||||
module.exports = discordConfig;
|
|
||||||
85
Server/controllers/accountController.js
Normal file
85
Server/controllers/accountController.js
Normal file
@@ -0,0 +1,85 @@
|
|||||||
|
|
||||||
|
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||||
|
const log = new DebugBuilder("server", "accountController");
|
||||||
|
|
||||||
|
const { UserStorage } = require("../libStorage");
|
||||||
|
const userStorage = new UserStorage();
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Check to see if the discord ID has an account associated with it
|
||||||
|
* @param {*} _discordAccountId The discord account to look for
|
||||||
|
* @param {*} callback The callback function to be called
|
||||||
|
* @callback false|*
|
||||||
|
*/
|
||||||
|
exports.checkForAccount = (_discordAccountId, callback) => {
|
||||||
|
userStorage.getRecordBy("discord_account_id", _discordAccountId, (err, results) => {
|
||||||
|
if (err) return callback(err, undefined);
|
||||||
|
|
||||||
|
if (!results) return callback(undefined, false);
|
||||||
|
return callback(undefined, results);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Create an account from a discord ID
|
||||||
|
* @param {*} _discordAccountId
|
||||||
|
* @param {*} callback
|
||||||
|
*/
|
||||||
|
exports.createAccount = (_discordAccountId, callback) => {
|
||||||
|
if (!_discordAccountId) return callback(new Error("No discord account specified before creation"));
|
||||||
|
userStorage.saveAccount(_discordAccountId, (err, results) => {
|
||||||
|
if (err) return callback(err, undefined);
|
||||||
|
|
||||||
|
if (!results) return callback(new Error("No results from creating account"), undefined);
|
||||||
|
|
||||||
|
return callback(undefined, results);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.withdrawBalance = async (_withdrawAmount, _discordAccountId, callback) => {
|
||||||
|
userStorage.updateBalance('withdraw', _withdrawAmount, _discordAccountId, async (err, result) => {
|
||||||
|
if (err) return callback(err, undefined);
|
||||||
|
|
||||||
|
if(result) return callback(undefined, result);
|
||||||
|
|
||||||
|
return callback(undefined, undefined);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.verifyBalance = (_tokensToBeUsed, _accountId, callback) => {
|
||||||
|
userStorage.checkBalance(_tokensToBeUsed, _accountId, (err, results) => {
|
||||||
|
if (err) return callback(err, undefined);
|
||||||
|
|
||||||
|
if(!results) return callback(undefined, false);
|
||||||
|
|
||||||
|
return callback(undefined, true);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
/**
|
||||||
|
* Check the given account for the token balance
|
||||||
|
*
|
||||||
|
* @param {*} _discordAccountId
|
||||||
|
* @param {*} callback
|
||||||
|
*/
|
||||||
|
exports.checkBalance = (_discordAccountId, callback) => {
|
||||||
|
if (!_discordAccountId) return callback(new Error("No discord account given to check balance of"), undefined);
|
||||||
|
userStorage.getRecordBy("discord_account_id", _discordAccountId, (err, accountRecord) => {
|
||||||
|
if (err) return callback(err, undefined);
|
||||||
|
|
||||||
|
if (!accountRecord) return callback(new Error("No account record given"), undefined);
|
||||||
|
|
||||||
|
return callback(undefined, accountRecord.balance);
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.insufficientTokensResponse = (interaction) => {
|
||||||
|
log.DEBUG("INSUFFICIENT TOKENS RESPONSE")
|
||||||
|
|
||||||
|
return interaction.reply({ content: `Sorry ${interaction.member.user}, you don't have enough tokens for this purchase. Please contact your bot rep to increase your balance to have more fun playing around!`, ephemeral: true });
|
||||||
|
}
|
||||||
|
|
||||||
|
exports.welcomeResponse = (interaction) => {
|
||||||
|
log.DEBUG("WELCOME RESPONSE")
|
||||||
|
|
||||||
|
return interaction.reply({ content: `Hey there ${interaction.member.user}! You haven't ran any commands that require tokens before. I've gone ahead and created an account for you. When you get a chance reach out to your nearest bot rep to fill your balance to start playing around! In the meantime however, you can run \`/pricing\` to view the current pricing.`, ephemeral: true });
|
||||||
|
}
|
||||||
@@ -1,12 +1,12 @@
|
|||||||
// Config
|
// Config
|
||||||
const discordConfig = require("../config/discordConfig");
|
require('dotenv').config();
|
||||||
// Debug
|
// Debug
|
||||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||||
const log = new DebugBuilder("server", "adminController");
|
const log = new DebugBuilder("server", "adminController");
|
||||||
// Utilities
|
// Utilities
|
||||||
const mysqlHandler = require("../utilities/mysqlHandler");
|
const { getAllClientIds } = require("../utilities/utils");
|
||||||
const utils = require("../utilities/utils");
|
const { getOnlineNodes, updateNodeInfo, addNodeConnection, getConnectionByNodeId, getNodeInfoFromId, checkNodeConnectionByClientId, removeNodeConnectionByNodeId } = require("../utilities/mysqlHandler");
|
||||||
const requests = require("../utilities/httpRequests");
|
const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
|
||||||
|
|
||||||
/** Get the presets of all online nodes, can be used for functions
|
/** Get the presets of all online nodes, can be used for functions
|
||||||
*
|
*
|
||||||
@@ -14,61 +14,26 @@ const requests = require("../utilities/httpRequests");
|
|||||||
* @returns {*} A list of the systems online
|
* @returns {*} A list of the systems online
|
||||||
*/
|
*/
|
||||||
async function getPresetsOfOnlineNodes(callback) {
|
async function getPresetsOfOnlineNodes(callback) {
|
||||||
mysqlHandler.getOnlineNodes((onlineNodes) => {
|
getOnlineNodes((onlineNodes) => {
|
||||||
let systems = {};
|
return callback(onlineNodes);
|
||||||
onlineNodes.forEach(onlineNode => {
|
|
||||||
systems[onlineNode.id] = utils.BufferToJson(onlineNode.nearbySystems);
|
|
||||||
});
|
|
||||||
|
|
||||||
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": discordConfig.channelID,
|
|
||||||
"presetName": preset
|
|
||||||
}), (responseObject) => {
|
|
||||||
callback(responseObject)
|
|
||||||
});
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
async function getNodeBotStatus(nodeId, callback) {
|
async function getNodeBotStatus(nodeId, callback) {
|
||||||
mysqlHandler.getNodeInfoFromId(nodeId, (nodeObject) =>{
|
getNodeInfoFromId(nodeId, (nodeObject) =>{
|
||||||
reqOptions = new requests.requestOptions("/bot/status", "GET", nodeObject.ip, nodeObject.port, undefined, 5);
|
reqOptions = new requestOptions("/bot/status", "GET", nodeObject.ip, nodeObject.port, undefined, 5);
|
||||||
requests.sendHttpRequest(reqOptions, JSON.stringify({}), (responseObject) => {
|
sendHttpRequest(reqOptions, JSON.stringify({}), (responseObject) => {
|
||||||
if (responseObject === false) {
|
if (responseObject === false) {
|
||||||
// Bot is joined
|
// Bot is joined
|
||||||
}
|
}
|
||||||
else {
|
else {
|
||||||
// Bot is free
|
// Bot is free
|
||||||
}
|
}
|
||||||
callback(responseObject);
|
return callback(responseObject);
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
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) => {
|
|
||||||
callback(responseObject);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
|
||||||
else {
|
|
||||||
// Bot is free
|
|
||||||
callback(false);
|
|
||||||
}
|
|
||||||
})
|
|
||||||
}
|
|
||||||
|
|
||||||
|
|
||||||
/** Return to requests for the presets of all online nodes, cannot be used in functions
|
/** Return to requests for the presets of all online nodes, cannot be used in functions
|
||||||
*
|
*
|
||||||
@@ -87,43 +52,161 @@ exports.getAvailablePresets = async (req, res) => {
|
|||||||
*
|
*
|
||||||
* @param {*} req Express request parameter
|
* @param {*} req Express request parameter
|
||||||
* @var {*} req.body.preset The preset to join (REQ)
|
* @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.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
|
||||||
* @param {*} res Express response parameter
|
* @param {*} res Express response parameter
|
||||||
*/
|
*/
|
||||||
exports.joinPreset = async (req, res) => {
|
exports.joinPreset = async (req, res) => {
|
||||||
if (!req.body.preset) return res.status(400).json("No preset specified");
|
if (!req.body.preset) return res.status(400).json("No preset specified");
|
||||||
await getPresetsOfOnlineNodes((systems) => {
|
if (!req.body.nodeId) return res.status(400).json("No node ID specified");
|
||||||
const systemsWithSelectedPreset = Object.values(systems).filter(nodePresets => nodePresets.includes(req.body.preset)).length
|
if (!req.body.clientId) return res.status(400).json("No client ID specified");
|
||||||
if (!systemsWithSelectedPreset) return res.status(400).json("No system online with that preset");
|
if (!req.body.channelId) return res.status(400).json("No channel ID specified");
|
||||||
if (systemsWithSelectedPreset > 1) {
|
|
||||||
if (!req.body.nodeId) return res.status(175).json("Multiple locations with the selected channel, please specify a nodeID (nodeId)")
|
const preset = req.body.preset;
|
||||||
requestNodeListenToPreset(req.body.preset, req.body.nodeId, (responseObject) => {
|
const nodeId = req.body.nodeId;
|
||||||
if (responseObject === false) return res.status(400).json("Timeout reached");
|
const clientId = req.body.clientId;
|
||||||
return res.sendStatus(responseObject.statusCode);
|
const channelId = req.body.channelId;
|
||||||
});
|
|
||||||
}
|
const joinedClient = await joinServerWrapper(preset, channelId, clientId, nodeId);
|
||||||
else {
|
if (!joinedClient) return res.send(400).json("No joined client");
|
||||||
let nodeId;
|
return res.status(200).json(joinedClient);
|
||||||
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
|
/** Request a node to join the server listening to a specific preset
|
||||||
*
|
*
|
||||||
* @param {*} req Express request parameter
|
* @param {*} req Express request parameter
|
||||||
* @param {*} res Express response parameter
|
* @param {*} res Express response parameter
|
||||||
|
* @var {*} req.body.nodeId The ID of the node to disconnect
|
||||||
*/
|
*/
|
||||||
exports.leaveServer = async (req, res) => {
|
exports.leaveServer = async (req, res) => {
|
||||||
if (!req.body.nodeId) return res.status(400).json("No nodeID specified");
|
if (!req.body.nodeId) return res.status(400).json("No node ID specified");
|
||||||
|
|
||||||
requestNodeLeaveServer(req.body.nodeId, (responseObject) => {
|
const nodeId = req.body.nodeId;
|
||||||
if (responseObject === false) return res.status(400).json("Bot not joined to server");
|
const currentConnection = await getConnectionByNodeId(nodeId);
|
||||||
return res.sendStatus(responseObject.statusCode);
|
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;
|
||||||
@@ -2,33 +2,97 @@
|
|||||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||||
const log = new DebugBuilder("server", "nodesController");
|
const log = new DebugBuilder("server", "nodesController");
|
||||||
// Utilities
|
// Utilities
|
||||||
const mysqlHander = require("../utilities/mysqlHandler");
|
const { getAllNodes, addNewNode, updateNodeInfo, getNodeInfoFromId, getOnlineNodes } = require("../utilities/mysqlHandler");
|
||||||
const utils = require("../utilities/utils");
|
const utils = require("../utilities/utils");
|
||||||
|
const { sendHttpRequest, requestOptions } = require("../utilities/httpRequests.js");
|
||||||
|
const { nodeObject } = require("../utilities/recordHelper.js");
|
||||||
|
|
||||||
|
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
|
||||||
|
*/
|
||||||
exports.listAllNodes = async (req, res) => {
|
exports.listAllNodes = async (req, res) => {
|
||||||
mysqlHander.getAllNodes((allNodes) => {
|
getAllNodes((allNodes) => {
|
||||||
res.status(200).json({
|
res.status(200).json({
|
||||||
"nodes_online": allNodes
|
"nodes_online": allNodes
|
||||||
});
|
});
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
// Add a new node to the
|
/**
|
||||||
|
* Add a new node to the storage
|
||||||
|
* @param {*} req Default express req from router
|
||||||
|
* @param {*} res Defualt express res from router
|
||||||
|
*/
|
||||||
exports.newNode = async (req, res) => {
|
exports.newNode = async (req, res) => {
|
||||||
if (!req.body.name) return res.send(400)
|
if (!req.body.name) return res.status(400).json("No name specified for new node");
|
||||||
|
|
||||||
try {
|
try {
|
||||||
// Try to add the new user with defaults if missing options
|
// Try to add the new user with defaults if missing options
|
||||||
mysqlHander.addNewNode({
|
const newNode = new nodeObject({
|
||||||
'name': req.body.name,
|
_name: req.body.name,
|
||||||
'ip': req.body.ip ?? null,
|
_ip: req.body.ip ?? null,
|
||||||
'port': req.body.port ?? null,
|
_port: req.body.port ?? null,
|
||||||
'location': req.body.location ?? null,
|
_location: req.body.location ?? null,
|
||||||
'nearbySystems': req.body.nearbySystems ?? null,
|
_nearbySystems: req.body.nearbySystems ?? null,
|
||||||
'online': req.body.online ?? 0
|
_online: (req.body.online == "true" || req.body.online == "True") ? true : false
|
||||||
}, (queryResults) => {
|
});
|
||||||
|
|
||||||
|
addNewNode(newNode, (newNodeObject) => {
|
||||||
// Send back a success if the user has been added and the ID for the client to keep track of
|
// Send back a success if the user has been added and the ID for the client to keep track of
|
||||||
res.status(202).json({"nodeId": queryResults.insertId});
|
res.status(202).json({ "nodeId": newNodeObject.id });
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
catch (err) {
|
catch (err) {
|
||||||
@@ -41,40 +105,254 @@ exports.newNode = async (req, res) => {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
// Get the known info for the node specified
|
/** Get the known info for the node specified
|
||||||
|
*
|
||||||
|
* @param {*} req Default express req from router
|
||||||
|
* @param {*} res Defualt express res from router
|
||||||
|
*/
|
||||||
exports.getNodeInfo = async (req, res) => {
|
exports.getNodeInfo = async (req, res) => {
|
||||||
if (!req.query.id) return res.status(400).json("No id specified");
|
if (!req.params.id) return res.status(400).json("No id specified");
|
||||||
mysqlHander.getNodeInfoFromId(req.query.id, (nodeInfo) => {
|
getNodeInfoFromId(req.params.id, (nodeInfo) => {
|
||||||
res.status(200).json(nodeInfo);
|
res.status(200).json(nodeInfo);
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
// Updates the information received from the client based on ID
|
/** Adds a specific system/preset on a given node
|
||||||
exports.nodeCheckIn = async (req, res) => {
|
*
|
||||||
if (!req.body.id) return res.status(400).json("No id specified");
|
* @param {*} req Default express req from router
|
||||||
mysqlHander.getNodeInfoFromId(req.body.id, (nodeInfo) => {
|
* @param {*} res Defualt express res from router
|
||||||
let nodeObject = {};
|
* @param {*} req.params.nodeId The Node ID to add the preset/system to
|
||||||
// Convert the DB systems buffer to a JSON object to be worked with
|
* @param {*} req.body.systemName The name of the system to add
|
||||||
nodeInfo.nearbySystems = utils.BufferToJson(nodeInfo.nearbySystems)
|
* @param {*} req.body.mode The radio mode of the preset
|
||||||
// Convert the online status to a boolean to be worked with
|
* @param {*} req.body.frequencies The frequencies of the preset
|
||||||
nodeInfo.online = nodeInfo.online !== 0;
|
* @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'
|
||||||
|
|
||||||
if (req.body.name && req.body.name !== nodeInfo.name) nodeObject.name = req.body.name
|
log.DEBUG("Request body for adding node system: ", reqBody, reqOptions);
|
||||||
if (req.body.ip && req.body.ip !== nodeInfo.ip) nodeObject.ip = req.body.ip
|
sendHttpRequest(reqOptions, JSON.stringify(reqBody), async (responseObj) => {
|
||||||
if (req.body.port && req.body.port !== nodeInfo.port) nodeObject.port = req.body.port
|
if(responseObj){
|
||||||
if (req.body.location && req.body.location !== nodeInfo.location) nodeObject.location = req.body.location
|
// Good
|
||||||
if (req.body.nearbySystems && JSON.stringify(req.body.nearbySystems) !== JSON.stringify(nodeInfo.nearbySystems)) nodeObject.nearbySystems = req.body.nearbySystems
|
log.DEBUG("Response from adding node system: ", reqBody, responseObj);
|
||||||
if (req.body.online && req.body.online !== nodeInfo.online) nodeObject.online = req.body.online
|
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 = {};
|
||||||
|
// Convert the online status to a boolean to be worked with
|
||||||
|
log.DEBUG("REQ Body: ", req.body);
|
||||||
|
|
||||||
|
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 no changes are made tell the client
|
// If no changes are made tell the client
|
||||||
if (Object.keys(nodeObject).length === 0) return res.status(200).json("No keys updated");
|
if (!isObjectUpdated) return res.status(200).json("No keys updated");
|
||||||
|
|
||||||
log.INFO("Updating the following keys for ID: ", req.body.id, nodeObject);
|
log.INFO("Updating the following keys for ID: ", req.params.nodeId, checkInObject);
|
||||||
// Adding the ID key to the body so that the client can double-check their ID
|
|
||||||
nodeObject.id = req.body.id;
|
|
||||||
mysqlHander.updateNodeInfo(nodeObject, () => {
|
|
||||||
return res.status(202).json({"updatedKeys": nodeObject});
|
|
||||||
})
|
|
||||||
})
|
|
||||||
|
|
||||||
}
|
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
|
||||||
|
*
|
||||||
|
* @param {*} req Default express req from router
|
||||||
|
* @param {*} res Defualt express res from router
|
||||||
|
*/
|
||||||
|
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);
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* The node monitor service, this will periodically check in on the online nodes to make sure they are still online
|
||||||
|
*/
|
||||||
|
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));
|
||||||
|
|
||||||
|
log.INFO("Starting Node Monitor Service");
|
||||||
|
// Check in before starting the infinite loop
|
||||||
|
await 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 new Promise(resolve => setTimeout(resolve, refreshInterval / 4));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|||||||
179
Server/controllers/openAiController.js
Normal file
179
Server/controllers/openAiController.js
Normal file
@@ -0,0 +1,179 @@
|
|||||||
|
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||||
|
const log = new DebugBuilder("server", "openAiController");
|
||||||
|
const crypto = require('crypto')
|
||||||
|
|
||||||
|
const { createTransaction } = require("./transactionController");
|
||||||
|
const { authorizeTokenUsage } = require("../middleware/balanceAuthorization");
|
||||||
|
|
||||||
|
const { encode } = require("gpt-3-encoder")
|
||||||
|
const { Configuration, OpenAIApi } = require('openai');
|
||||||
|
const configuration = new Configuration({
|
||||||
|
organization: process.env.OPENAI_ORG,
|
||||||
|
apiKey: process.env.OPENAI_KEY
|
||||||
|
});
|
||||||
|
|
||||||
|
const openai = new OpenAIApi(configuration);
|
||||||
|
|
||||||
|
// Global Vars for Other functions
|
||||||
|
exports.DALLE_COLOR = 0x34c6eb;
|
||||||
|
exports.CHATGPT_COLOR = 0x34eb9b;
|
||||||
|
|
||||||
|
async function getImageGeneration(_prompt, { _images_to_generate = 1, _image_size = "256x256" }, callback){
|
||||||
|
const validImageSizes = ["256x256", "512x512", "1024x1024"];
|
||||||
|
|
||||||
|
if (!_prompt) callback(new Error("No prompt given before generating image"), undefined);
|
||||||
|
if (!validImageSizes.includes(_image_size)) callback(new Error("Image size given is not valid, valid size: ", validImageSizes));
|
||||||
|
if (!_images_to_generate || _images_to_generate === 0 || _images_to_generate > 10) callback(new Error("Invalid image count given"));
|
||||||
|
|
||||||
|
// Calculate token usage?
|
||||||
|
|
||||||
|
log.DEBUG("Getting image generation with these properties: ", _prompt, _images_to_generate, _image_size)
|
||||||
|
try{
|
||||||
|
const response = await openai.createImage({
|
||||||
|
prompt: _prompt,
|
||||||
|
n: _images_to_generate,
|
||||||
|
size: _image_size
|
||||||
|
})
|
||||||
|
|
||||||
|
|
||||||
|
if(!response?.data) return callback(new Error("Error in response data: ", response));
|
||||||
|
return callback(undefined, response.data);
|
||||||
|
} catch (err){
|
||||||
|
log.ERROR(err);
|
||||||
|
log.ERROR("Error when handing image model request");
|
||||||
|
return callback(err, undefined);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Get the response from GPT with the specified parameters
|
||||||
|
*
|
||||||
|
* @param {*} _prompt The text prompt to send to the model
|
||||||
|
* @param {*} callback The callback to call with errors or results
|
||||||
|
* @param {*} param2 Any parameters the user has changed for this request
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
async function getTextGeneration(_prompt, callback, { _model = "text-davinci-003", _temperature = 0, _max_tokens = 100}) {
|
||||||
|
// If the temperature is set to null
|
||||||
|
_temperature = _temperature ?? 0;
|
||||||
|
// If the tokens are set to null
|
||||||
|
_max_tokens = _max_tokens ?? 100;
|
||||||
|
|
||||||
|
const encodedPrompt = encode(_prompt);
|
||||||
|
const promptTokens = encodedPrompt.length;
|
||||||
|
log.DEBUG("Tokens in prompt: ", promptTokens);
|
||||||
|
if (promptTokens >= _max_tokens) return callback(new Error("Tokens of request are greater than the set max tokens", promptTokens, _max_tokens));
|
||||||
|
|
||||||
|
_max_tokens = _max_tokens - promptTokens;
|
||||||
|
log.DEBUG("Updated max tokens: ", _max_tokens);
|
||||||
|
|
||||||
|
log.DEBUG("Getting chat with these properties: ", _prompt, _model, _temperature, _max_tokens)
|
||||||
|
try{
|
||||||
|
const response = await openai.createCompletion({
|
||||||
|
model: _model,
|
||||||
|
prompt: _prompt,
|
||||||
|
temperature: _temperature,
|
||||||
|
max_tokens: _max_tokens
|
||||||
|
});
|
||||||
|
if(!response?.data) return callback(new Error("Error in response data: ", response));
|
||||||
|
return callback(undefined, response.data);
|
||||||
|
} catch (err){
|
||||||
|
log.ERROR("Error when handing text model request: ", err);
|
||||||
|
return callback(err, undefined);
|
||||||
|
}
|
||||||
|
//var responseData = response.data.choices[0].text;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Use ChatGPT to generate a response
|
||||||
|
*
|
||||||
|
* @param {*} _prompt The use submitted text prompt
|
||||||
|
* @param {*} param1 Default parameters can be modified
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
exports.submitTextPromptTransaction = async (prompt, temperature, max_tokens, discord_account_id, interaction, command, callback) => {
|
||||||
|
|
||||||
|
getTextGeneration(prompt, (err, gptResult) => {
|
||||||
|
if (err) callback(err, undefined);
|
||||||
|
|
||||||
|
// TODO - Use the pricing table to calculate discord tokens
|
||||||
|
log.DEBUG("GPT Response", gptResult);
|
||||||
|
const discordTokensUsed = gptResult.usage.total_tokens;
|
||||||
|
|
||||||
|
if (gptResult){
|
||||||
|
createTransaction(gptResult.id, discord_account_id, discordTokensUsed, gptResult.usage.total_tokens, 1, async (err, transactionResult) => {
|
||||||
|
if (err) callback(err, undefined);
|
||||||
|
|
||||||
|
if (transactionResult){
|
||||||
|
log.DEBUG("Transaction Created: ", transactionResult);
|
||||||
|
callback(undefined, ({ promptResult: gptResult.choices[0].text, totalTokens: discordTokensUsed}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
}, { _temperature: temperature, _max_tokens: max_tokens });
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper to generate an image from a prompt and params and store this information in a transaction
|
||||||
|
*
|
||||||
|
* @param {*} prompt The prompt of the image
|
||||||
|
* @param {*} images_to_generate The number of images to generate
|
||||||
|
* @param {*} image_size The size of the image ["256x256" | "512x512" | "1024x1024"]
|
||||||
|
* @param {*} callback
|
||||||
|
*/
|
||||||
|
exports.submitImagePromptTransaction = async (prompt, discord_account_id, images_to_generate, image_size, interaction, command, callback) => {
|
||||||
|
let pricePerImage = 800;
|
||||||
|
log.DEBUG(image_size)
|
||||||
|
switch(image_size){
|
||||||
|
case "1024x1024":
|
||||||
|
log.DEBUG("1024 selected");
|
||||||
|
pricePerImage = 1000;
|
||||||
|
break;
|
||||||
|
case "512x512":
|
||||||
|
pricePerImage = 900;
|
||||||
|
log.DEBUG("512 selected");
|
||||||
|
break;
|
||||||
|
case "256x256":
|
||||||
|
log.DEBUG("256 selected");
|
||||||
|
pricePerImage = 800;
|
||||||
|
break;
|
||||||
|
default:
|
||||||
|
log.DEBUG("256px defaulted");
|
||||||
|
pricePerImage = 800;
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!images_to_generate) images_to_generate = 1;
|
||||||
|
if (!image_size) image_size = "256x256";
|
||||||
|
|
||||||
|
totalTokensToBeUsed = pricePerImage * images_to_generate;
|
||||||
|
|
||||||
|
log.DEBUG("Total tokens to be used", totalTokensToBeUsed, pricePerImage, images_to_generate);
|
||||||
|
|
||||||
|
authorizeTokenUsage(interaction, command, totalTokensToBeUsed, (isAuthorized) => {
|
||||||
|
if (isAuthorized) {
|
||||||
|
getImageGeneration(prompt, {
|
||||||
|
_image_size: image_size,
|
||||||
|
_images_to_generate: images_to_generate
|
||||||
|
}, (err, dalleResult) => {
|
||||||
|
if (err) callback(err, undefined);
|
||||||
|
|
||||||
|
// TODO - Use the pricing table to calculate discord tokens
|
||||||
|
log.DEBUG("DALL-E Result", dalleResult);
|
||||||
|
|
||||||
|
const dalleResultHash = crypto.createHash('sha1').update(JSON.stringify({ discord_account_id : prompt, images_to_generate: image_size })).digest('hex')
|
||||||
|
|
||||||
|
if (dalleResult){
|
||||||
|
createTransaction(dalleResultHash, discord_account_id, totalTokensToBeUsed, totalTokensToBeUsed, 2, async (err, transactionResult) => {
|
||||||
|
if (err) callback(err, undefined);
|
||||||
|
|
||||||
|
if (transactionResult){
|
||||||
|
log.DEBUG("Transaction Created: ", transactionResult);
|
||||||
|
callback(undefined, ({ results: dalleResult, totalTokens: totalTokensToBeUsed}));
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
38
Server/controllers/rssController.js
Normal file
38
Server/controllers/rssController.js
Normal file
@@ -0,0 +1,38 @@
|
|||||||
|
//Will handle updating feeds in all channels
|
||||||
|
|
||||||
|
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||||
|
const log = new DebugBuilder("server", "rssController");
|
||||||
|
|
||||||
|
const libCore = require("../libCore");
|
||||||
|
const libUtils = require("../libUtils");
|
||||||
|
|
||||||
|
const refreshInterval = process.env.RSS_REFRESH_INTERVAL ?? 300000;
|
||||||
|
|
||||||
|
exports.RSSController = class RSSController {
|
||||||
|
constructor(_client) {
|
||||||
|
this.client = _client;
|
||||||
|
}
|
||||||
|
|
||||||
|
async start(){
|
||||||
|
// Wait for the refresh period before starting rss feeds, so the rest of the bot can start
|
||||||
|
await new Promise(resolve => setTimeout(resolve, refreshInterval));
|
||||||
|
|
||||||
|
log.INFO("Starting RSS Controller");
|
||||||
|
// Get initial feeds before the starting the infinite loop
|
||||||
|
await libCore.updateFeeds(this.client);
|
||||||
|
|
||||||
|
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 this.collectLatestPosts();
|
||||||
|
await new Promise(resolve => setTimeout(resolve, refreshInterval / 4));
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
async collectLatestPosts(){
|
||||||
|
log.INFO("Updating sources");
|
||||||
|
await libCore.updateFeeds(this.client)
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
}
|
||||||
35
Server/controllers/transactionController.js
Normal file
35
Server/controllers/transactionController.js
Normal file
@@ -0,0 +1,35 @@
|
|||||||
|
// Controller for managing transactions
|
||||||
|
|
||||||
|
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||||
|
const log = new DebugBuilder("server", "transactionController");
|
||||||
|
|
||||||
|
const { TransactionStorage } = require("../libStorage");
|
||||||
|
const transactionStorage = new TransactionStorage();
|
||||||
|
|
||||||
|
const { BaseTransaction } = require("../utilities/recordHelper");
|
||||||
|
const { withdrawBalance } = require("./accountController");
|
||||||
|
|
||||||
|
exports.createTransaction = async (_provider_transaction_id, _account_id, _discord_tokens_used, _provider_tokens_used, _provider_id, callback) => {
|
||||||
|
if (!_provider_transaction_id && !_account_id && !_discord_tokens_used && !_provider_id) return callback(new Error("Invalid vars when creating transaction", {vars: [_provider_transaction_id, _account_id, _discord_tokens_used, _provider_id, callback]}))
|
||||||
|
|
||||||
|
const newTransaction = new BaseTransaction(_provider_transaction_id, _account_id, _discord_tokens_used, _provider_tokens_used, _provider_id, callback);
|
||||||
|
log.DEBUG("New Transaction Object: ", newTransaction);
|
||||||
|
withdrawBalance(newTransaction.discord_tokens_used, newTransaction.account_id, (err, withdrawResult) => {
|
||||||
|
if (err) return callback(err, undefined);
|
||||||
|
|
||||||
|
if (withdrawResult){
|
||||||
|
log.DEBUG("New withdraw result: ", withdrawResult);
|
||||||
|
transactionStorage.createTransaction(newTransaction, async (err, transactionResult) =>{
|
||||||
|
if (err) return callback(err, undefined);
|
||||||
|
|
||||||
|
if(transactionResult){
|
||||||
|
log.DEBUG("New transaction result: ", transactionResult);
|
||||||
|
return callback(undefined, transactionResult);
|
||||||
|
}
|
||||||
|
})
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
return callback(undefined, undefined);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
@@ -1,150 +0,0 @@
|
|||||||
//Config
|
|
||||||
import { getTOKEN, getGuildID, getApplicationID } from './utilities/configHandler.js.js';
|
|
||||||
// Commands
|
|
||||||
import ping from './commands/ping.js';
|
|
||||||
import join from './commands/join.js.js';
|
|
||||||
import leave from './commands/leave.js.js';
|
|
||||||
import status from './commands/status.js.js';
|
|
||||||
// Debug
|
|
||||||
import ModuleDebugBuilder from "./utilities/moduleDebugBuilder.js.js";
|
|
||||||
const log = new ModuleDebugBuilder("bot", "app");
|
|
||||||
// Modules
|
|
||||||
import { Client, GatewayIntentBits } from 'discord.js';
|
|
||||||
// Utilities
|
|
||||||
import registerCommands from './utilities/registerCommands.js.js';
|
|
||||||
|
|
||||||
/**
|
|
||||||
* Host Process Object Builder
|
|
||||||
*
|
|
||||||
* This constructor is used to easily construct responses to the host process
|
|
||||||
*/
|
|
||||||
class HPOB {
|
|
||||||
/**
|
|
||||||
* Build an object to be passed to the host process
|
|
||||||
* @param command The command to that was run ("Status", "Join", "Leave", "ChgPreSet")
|
|
||||||
* @param response The response from the command that was run
|
|
||||||
*/
|
|
||||||
constructor(command = "Status"||"Join"||"Leave"||"ChgPreSet", response) {
|
|
||||||
this.cmd = command;
|
|
||||||
this.msg = response;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
// Create the Discord client
|
|
||||||
const client = new Client({
|
|
||||||
intents: [
|
|
||||||
GatewayIntentBits.Guilds,
|
|
||||||
GatewayIntentBits.GuildMessages,
|
|
||||||
GatewayIntentBits.MessageContent,
|
|
||||||
GatewayIntentBits.GuildVoiceStates
|
|
||||||
]
|
|
||||||
});
|
|
||||||
|
|
||||||
/**
|
|
||||||
* When the parent process sends a message, this will interpret the message and act accordingly
|
|
||||||
*
|
|
||||||
* DRB IPC Message Structure:
|
|
||||||
* msg.cmd = The command keyword; Commands covered on the server side
|
|
||||||
* msg.params = An array containing the parameters for the command
|
|
||||||
*
|
|
||||||
*/
|
|
||||||
process.on('message', (msg) => {
|
|
||||||
log.DEBUG('IPC Message: ', msg);
|
|
||||||
const guildID = getGuilds()[0];
|
|
||||||
|
|
||||||
log.DEBUG("Guild Name: ", getGuildNameFromID(guildID));
|
|
||||||
switch (msg.cmd) {
|
|
||||||
// Check the status of the bot
|
|
||||||
case "Status":
|
|
||||||
log.INFO("Status command run from IPC");
|
|
||||||
|
|
||||||
status({guildID: guildID, callback: (statusObj) => {
|
|
||||||
log.DEBUG("Status Object string: ", statusObj);
|
|
||||||
if (!statusObj.voiceConnection) return process.send(new HPOB("Status", "VDISCONN"));
|
|
||||||
}});
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Check the params for a server ID and if so join the server
|
|
||||||
case "Join":
|
|
||||||
log.INFO("Join command run from IPC");
|
|
||||||
|
|
||||||
join({guildID: guildID, guildObj: client.guilds.cache.get(guildID), channelID: msg.params.channelID, callback: () => {
|
|
||||||
process.send(new HPOB("Join", "AIDS"));
|
|
||||||
}})
|
|
||||||
break;
|
|
||||||
|
|
||||||
// Check to see if the bot is in a server and if so leave
|
|
||||||
case "Leave":
|
|
||||||
log.INFO("Leave command run from IPC");
|
|
||||||
|
|
||||||
leave({guildID: guildID, callback: (response) => {
|
|
||||||
process.send(new HPOB("Leave", response));
|
|
||||||
}});
|
|
||||||
break;
|
|
||||||
|
|
||||||
default:
|
|
||||||
// Command doesn't exist
|
|
||||||
log.INFO("Unknown command run from IPC");
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
// When the client is connected and ready
|
|
||||||
client.on('ready', () =>{
|
|
||||||
log.INFO(`${client.user.tag} is ready`)
|
|
||||||
process.send({'msg': "INIT READY"});
|
|
||||||
});
|
|
||||||
|
|
||||||
/*
|
|
||||||
* Saved For later
|
|
||||||
client.on('messageCreate', (message) => {
|
|
||||||
log.DEBUG(`Message Sent by: ${message.author.tag}\n\t'${message.content}'`);
|
|
||||||
});
|
|
||||||
*/
|
|
||||||
|
|
||||||
// When a command is sent
|
|
||||||
client.on('interactionCreate', (interaction) => {
|
|
||||||
if (interaction.isChatInputCommand()){
|
|
||||||
switch (interaction.commandName) {
|
|
||||||
case "ping":
|
|
||||||
ping(interaction);
|
|
||||||
break;
|
|
||||||
case "join":
|
|
||||||
join({ interaction: interaction });
|
|
||||||
break;
|
|
||||||
case "leave":
|
|
||||||
leave({ interaction: interaction });
|
|
||||||
break;
|
|
||||||
case "status":
|
|
||||||
status({ interaction: interaction });
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
interaction.reply({ content: 'Command not found, try one that exists', fetchReply: true })
|
|
||||||
.then((message) => log.DEBUG(`Reply sent with content ${message.content}`))
|
|
||||||
.catch((err) => log.ERROR(err));
|
|
||||||
}
|
|
||||||
}
|
|
||||||
})
|
|
||||||
|
|
||||||
function loginBot(){
|
|
||||||
client.login(getTOKEN());
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGuilds() {
|
|
||||||
return client.guilds.cache.map(guild => guild.id)
|
|
||||||
}
|
|
||||||
|
|
||||||
function getGuildNameFromID(guildID) {
|
|
||||||
return client.guilds.cache.map((guild) => {
|
|
||||||
if (guild.id === guildID) return guild.name;
|
|
||||||
})[0]
|
|
||||||
}
|
|
||||||
|
|
||||||
function main(){
|
|
||||||
registerCommands(() => {
|
|
||||||
loginBot();
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
main();
|
|
||||||
//module.exports = client;
|
|
||||||
@@ -1,6 +0,0 @@
|
|||||||
// Utilities
|
|
||||||
import { replyToInteraction } from '../utilities/messageHandler.js.js';
|
|
||||||
|
|
||||||
export default function ping(interaction) {
|
|
||||||
return replyToInteraction(interaction, "Pong! I have Aids and now you do too!");
|
|
||||||
}
|
|
||||||
@@ -1,7 +0,0 @@
|
|||||||
{
|
|
||||||
"TOKEN": "OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY",
|
|
||||||
"ApplicationID": "943742040255115304",
|
|
||||||
"GuildID": "367396189529833472",
|
|
||||||
"DeviceID": "5",
|
|
||||||
"DeviceName": "VoiceMeeter Aux Output (VB-Audi"
|
|
||||||
}
|
|
||||||
60
Server/events/interactionCreate.js
Normal file
60
Server/events/interactionCreate.js
Normal file
@@ -0,0 +1,60 @@
|
|||||||
|
const { Events } = require('discord.js');
|
||||||
|
const { authorizeCommand } = require('../middleware/discordAuthorization');
|
||||||
|
const { authorizeTokenUsage } = require('../middleware/balanceAuthorization');
|
||||||
|
|
||||||
|
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||||
|
const log = new DebugBuilder("server", "interactionCreate");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: Events.InteractionCreate,
|
||||||
|
async execute(interaction) {
|
||||||
|
const command = interaction.client.commands.get(interaction.commandName);
|
||||||
|
log.VERBOSE("Interaction created for command: ", command);
|
||||||
|
|
||||||
|
// Execute autocomplete if the user is checking autocomplete
|
||||||
|
if (interaction.isAutocomplete()) {
|
||||||
|
log.DEBUG("Running autocomplete for command: ", command.data.name);
|
||||||
|
return await command.autocomplete(interaction);
|
||||||
|
}
|
||||||
|
|
||||||
|
// Check if the interaction is a command
|
||||||
|
if (!interaction.isChatInputCommand()) return;
|
||||||
|
|
||||||
|
if (!command) {
|
||||||
|
log.ERROR(`No command matching ${interaction.commandName} was found.`);
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
log.DEBUG(`${interaction.member.user} is running '${interaction.commandName}'`);
|
||||||
|
|
||||||
|
await authorizeCommand(interaction, command, async () => {
|
||||||
|
await authorizeTokenUsage(interaction, command, undefined, async () => {
|
||||||
|
try {
|
||||||
|
if (command.deferInitialReply) {
|
||||||
|
try {
|
||||||
|
if (interaction.options.getBool('public') && interaction.options.getBool('public') == false) await interaction.deferReply({ ephemeral: true });
|
||||||
|
else await interaction.deferReply({ ephemeral: false });
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
if (err instanceof TypeError) {
|
||||||
|
// The public option doesn't exist in this command
|
||||||
|
await interaction.deferReply({ ephemeral: false });
|
||||||
|
} else {
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
|
||||||
|
}
|
||||||
|
}
|
||||||
|
command.execute(interaction);
|
||||||
|
} catch (error) {
|
||||||
|
log.ERROR(error);
|
||||||
|
if (interaction.replied || interaction.deferred) {
|
||||||
|
interaction.followUp({ content: 'There was an error while executing this command!', ephemeral: true });
|
||||||
|
} else {
|
||||||
|
interaction.reply({ content: 'There was an error while executing this command!', ephemeral: true });
|
||||||
|
}
|
||||||
|
}
|
||||||
|
})
|
||||||
|
});
|
||||||
|
},
|
||||||
|
};
|
||||||
14
Server/events/messageCreate.js
Normal file
14
Server/events/messageCreate.js
Normal file
@@ -0,0 +1,14 @@
|
|||||||
|
const { Events } = require('discord.js');
|
||||||
|
const { authorizeCommand } = require('../middleware/discordAuthorization');
|
||||||
|
const { authorizeTokenUsage } = require('../middleware/balanceAuthorization');
|
||||||
|
const { linkCop } = require("../modules/linkCop");
|
||||||
|
|
||||||
|
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||||
|
const log = new DebugBuilder("server", "messageCreate");
|
||||||
|
|
||||||
|
module.exports = {
|
||||||
|
name: Events.MessageCreate,
|
||||||
|
async execute(interaction) {
|
||||||
|
//await linkCop(interaction);
|
||||||
|
},
|
||||||
|
};
|
||||||
7
Server/extras/linkCopInsults.js
Normal file
7
Server/extras/linkCopInsults.js
Normal file
@@ -0,0 +1,7 @@
|
|||||||
|
exports.linkCopInsults = [
|
||||||
|
"{%mtn_user%}, tsk tsk. Links belong here:\n '{%ref_og_msg%}'",
|
||||||
|
"{%mtn_user%}, the channel is quite literally called 'links':\n '{%ref_og_msg%}'",
|
||||||
|
"{%mtn_user%}. Well, well, well, if it isn't the man who's been posting links in the wrong channel.\n'{%ref_og_msg%}'",
|
||||||
|
"{%mtn_user%}, isn't this convenient. A whole channel for links and you put links in, and you put {%ref_links%} in {%ref_og_channel%}.\n'{%ref_og_msg%}'",
|
||||||
|
"{%mtn_user%}, that's odd. I don't recall {%ref_og_channel%} being called 'links. Maybe I misread?\n'{%ref_og_msg%}'",
|
||||||
|
]
|
||||||
171
Server/index.js
Normal file
171
Server/index.js
Normal file
@@ -0,0 +1,171 @@
|
|||||||
|
// Modules
|
||||||
|
var createError = require('http-errors');
|
||||||
|
var express = require('express');
|
||||||
|
var path = require('path');
|
||||||
|
var cookieParser = require('cookie-parser');
|
||||||
|
var logger = require('morgan');
|
||||||
|
var http = require('http');
|
||||||
|
const fs = require('fs');
|
||||||
|
require('dotenv').config();
|
||||||
|
// Utilities
|
||||||
|
const { RSSController } = require("./controllers/rssController");
|
||||||
|
const libUtils = require("./libUtils");
|
||||||
|
const deployCommands = require("./utilities/deployCommands");
|
||||||
|
const { nodeMonitorService } = require('./controllers/nodesController');
|
||||||
|
// Debug
|
||||||
|
const { DebugBuilder } = require("./utilities/debugBuilder");
|
||||||
|
const log = new DebugBuilder("server", "index");
|
||||||
|
|
||||||
|
const {
|
||||||
|
Client,
|
||||||
|
Events,
|
||||||
|
Collection,
|
||||||
|
GatewayIntentBits,
|
||||||
|
MessageActionRow,
|
||||||
|
MessageButton
|
||||||
|
} = require('discord.js');
|
||||||
|
|
||||||
|
const client = new Client({
|
||||||
|
intents: [GatewayIntentBits.GuildMessages, GatewayIntentBits.Guilds, GatewayIntentBits.GuildPresences, GatewayIntentBits.GuildMembers]
|
||||||
|
});
|
||||||
|
|
||||||
|
prefix = process.env.PREFIX
|
||||||
|
discordToken = process.env.TOKEN;
|
||||||
|
rssTimeoutValue = process.env.RSS_TIMEOUT_VALUE ?? 300000;
|
||||||
|
|
||||||
|
var indexRouter = require('./routes/index');
|
||||||
|
var nodesRouter = require('./routes/nodes');
|
||||||
|
var adminRouter = require('./routes/admin');
|
||||||
|
|
||||||
|
// HTTP Server Config
|
||||||
|
var app = express();
|
||||||
|
|
||||||
|
// view engine setup
|
||||||
|
app.set('views', path.join(__dirname, 'views'));
|
||||||
|
app.set('view engine', 'ejs');
|
||||||
|
|
||||||
|
app.use(logger('dev'));
|
||||||
|
app.use(express.json());
|
||||||
|
app.use(express.urlencoded({ extended: false }));
|
||||||
|
app.use(cookieParser());
|
||||||
|
app.use(express.static(path.join(__dirname, 'public')));
|
||||||
|
|
||||||
|
// Web Interface
|
||||||
|
app.use('/', indexRouter);
|
||||||
|
|
||||||
|
// Nodes API
|
||||||
|
app.use('/nodes', nodesRouter);
|
||||||
|
|
||||||
|
// Admin API
|
||||||
|
app.use('/admin', adminRouter);
|
||||||
|
|
||||||
|
// catch 404 and forward to error handler
|
||||||
|
app.use((req, res, next) => {
|
||||||
|
next(createError(404));
|
||||||
|
});
|
||||||
|
|
||||||
|
var port = libUtils.normalizePort(process.env.HTTP_PORT || '3000');
|
||||||
|
app.set('port', port);
|
||||||
|
|
||||||
|
// error handler
|
||||||
|
app.use((err, req, res, next) => {
|
||||||
|
// set locals, only providing error in development
|
||||||
|
res.locals.message = err.message;
|
||||||
|
res.locals.error = req.app.get('env') === 'development' ? err : {};
|
||||||
|
|
||||||
|
// render the error page
|
||||||
|
res.status(err.status || 500);
|
||||||
|
res.render('error');
|
||||||
|
});
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the HTTP background server
|
||||||
|
*/
|
||||||
|
async function runHTTPServer() {
|
||||||
|
var server = http.createServer(app);
|
||||||
|
server.listen(port);
|
||||||
|
|
||||||
|
server.on('error', libUtils.onError);
|
||||||
|
|
||||||
|
server.on('listening', () => {
|
||||||
|
log.INFO("HTTP server started!");
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the node monitoring service
|
||||||
|
*/
|
||||||
|
async function runNodeMonitorService(){
|
||||||
|
const monitor = new nodeMonitorService();
|
||||||
|
monitor.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Start the RSS background process
|
||||||
|
*/
|
||||||
|
async function runRssService() {
|
||||||
|
const rssController = new RSSController(client);
|
||||||
|
rssController.start();
|
||||||
|
}
|
||||||
|
|
||||||
|
// Discord bot config
|
||||||
|
|
||||||
|
// Setup commands for the Discord bot
|
||||||
|
client.commands = new Collection();
|
||||||
|
const commandsPath = path.join(__dirname, 'commands');
|
||||||
|
const commandFiles = fs.readdirSync(commandsPath).filter(file => file.endsWith('.js'));
|
||||||
|
//const commandFiles = fs.readdirSync('./commands').filter(file => file.endsWith('.js'));
|
||||||
|
for (const file of commandFiles) {
|
||||||
|
const filePath = path.join(commandsPath, file);
|
||||||
|
const command = require(filePath);
|
||||||
|
if (command.data instanceof Promise) {
|
||||||
|
command.data.then(async (builder) => {
|
||||||
|
command.data = builder;
|
||||||
|
log.DEBUG("Importing command: ", command.data.name, command);
|
||||||
|
// Set a new item in the Collection
|
||||||
|
// With the key as the command name and the value as the exported module
|
||||||
|
client.commands.set(command.data.name, command);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
log.DEBUG("Importing command: ", command.data.name, command);
|
||||||
|
// Set a new item in the Collection
|
||||||
|
// With the key as the command name and the value as the exported module
|
||||||
|
client.commands.set(command.data.name, command);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
// Run when the bot is ready
|
||||||
|
client.on('ready', () => {
|
||||||
|
log.DEBUG(`Discord server up and running with client: ${client.user.tag}`);
|
||||||
|
log.INFO(`Logged in as ${client.user.tag}!`);
|
||||||
|
|
||||||
|
// Deploy slash commands
|
||||||
|
log.DEBUG("Deploying slash commands");
|
||||||
|
deployCommands.deploy(client.user.id, client.guilds.cache.map(guild => guild.id));
|
||||||
|
|
||||||
|
log.DEBUG(`Starting HTTP Server`);
|
||||||
|
runHTTPServer();
|
||||||
|
|
||||||
|
log.DEBUG("Starting Node Monitoring Service");
|
||||||
|
runNodeMonitorService();
|
||||||
|
|
||||||
|
log.DEBUG("Starting RSS watcher");
|
||||||
|
runRssService();
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup any additional event handlers
|
||||||
|
const eventsPath = path.join(__dirname, 'events');
|
||||||
|
const eventFiles = fs.readdirSync(eventsPath).filter(file => file.endsWith('.js'));
|
||||||
|
|
||||||
|
for (const file of eventFiles) {
|
||||||
|
const filePath = path.join(eventsPath, file);
|
||||||
|
const event = require(filePath);
|
||||||
|
if (event.once) {
|
||||||
|
client.once(event.name, (...args) => event.execute(...args));
|
||||||
|
} else {
|
||||||
|
client.on(event.name, (...args) => event.execute(...args));
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
client.login(discordToken); //Load Client Discord Token
|
||||||
404
Server/libCore.js
Normal file
404
Server/libCore.js
Normal file
@@ -0,0 +1,404 @@
|
|||||||
|
const { all } = require('axios');
|
||||||
|
const axios = require('axios');
|
||||||
|
|
||||||
|
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 UserAgent = require("user-agents");
|
||||||
|
process.env.USER_AGENT_STRING = new UserAgent({ platform: 'Win32' }).toString();
|
||||||
|
|
||||||
|
log.DEBUG("Generated User Agent string:", process.env.USER_AGENT_STRING);
|
||||||
|
|
||||||
|
// Initiate the parser
|
||||||
|
let Parser = require('rss-parser');
|
||||||
|
let parser = new Parser({
|
||||||
|
headers: {
|
||||||
|
'User-Agent': process.env.USER_AGENT_STRING,
|
||||||
|
"Accept": "application/rss+xml,application/xhtml+xml,application/xml"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
// Setup Storage handlers
|
||||||
|
var feedStorage = new FeedStorage();
|
||||||
|
var postStorage = new PostStorage();
|
||||||
|
|
||||||
|
// Initiate a running array of objects to keep track of sources that have no feeds/posts
|
||||||
|
/*
|
||||||
|
var runningPostsToRemove = [{
|
||||||
|
"{SOURCE URL}": {NUMBER OF TIMES IT'S BEEN REMOVED}
|
||||||
|
}]
|
||||||
|
*/
|
||||||
|
var runningPostsToRemove = {};
|
||||||
|
const sourceFailureLimit = process.env.SOURCE_FAILURE_LIMIT ?? 15;
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Wrapper for feeds that cause errors. By default it will wait over a day for the source to come back online before deleting it.
|
||||||
|
*
|
||||||
|
* @param {string} sourceURL The URL of the feed source causing issues
|
||||||
|
*/
|
||||||
|
exports.removeSource = function removeSource(sourceURL) {
|
||||||
|
log.INFO("Removing source URL: ", sourceURL);
|
||||||
|
// Check to see if this is the first time this source has been attempted
|
||||||
|
if (!Object.keys(runningPostsToRemove).includes(sourceURL)) {
|
||||||
|
runningPostsToRemove[sourceURL] = { count: 1, timestamp: Date.now(), ignoredAttempts: 0 };
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
const backoffDateTimeDifference = (Date.now() - new Date(runningPostsToRemove[sourceURL].timestamp));
|
||||||
|
const backoffWaitTime = (runningPostsToRemove[sourceURL].count * 30000);
|
||||||
|
|
||||||
|
log.DEBUG("Datetime", runningPostsToRemove[sourceURL], backoffDateTimeDifference, backoffWaitTime);
|
||||||
|
|
||||||
|
// Check to see if the last error occurred within the backoff period or if we should try again
|
||||||
|
if (backoffDateTimeDifference <= backoffWaitTime) {
|
||||||
|
runningPostsToRemove[sourceURL].ignoredAttempts +=1;
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
// Increase the retry counter
|
||||||
|
if (runningPostsToRemove[sourceURL].count < sourceFailureLimit) {
|
||||||
|
runningPostsToRemove[sourceURL].count += 1;
|
||||||
|
runningPostsToRemove[sourceURL].timestamp = Date.now();
|
||||||
|
return;
|
||||||
|
}
|
||||||
|
|
||||||
|
feedStorage.getRecordBy('link', sourceURL, (err, record) => {
|
||||||
|
if (err) log.ERROR("Error getting record from feedStorage", err);
|
||||||
|
|
||||||
|
if (!record) log.ERROR("No source returned from feedStorage");
|
||||||
|
feedStorage.destroy(record.id, (err, results) => {
|
||||||
|
if (err) log.ERROR("Error removing ID from results", err);
|
||||||
|
|
||||||
|
if (!results) log.WARN("No results from remove entry");
|
||||||
|
|
||||||
|
log.DEBUG("Source exceeded the limit of retries and has been removed", sourceURL);
|
||||||
|
return;
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Unset a source URL from deletion if the source has not already been deleted
|
||||||
|
* @param {*} sourceURL The source URL to be unset from deletion
|
||||||
|
*/
|
||||||
|
exports.unsetRemoveSource = function unsetRemoveSource(sourceURL) {
|
||||||
|
log.INFO("Unsetting source URL from deletion (if not already deleted): ", sourceURL);
|
||||||
|
if (!Object.keys(runningPostsToRemove).includes(sourceURL)) return;
|
||||||
|
|
||||||
|
delete runningPostsToRemove[sourceURL];
|
||||||
|
|
||||||
|
return
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Adds or updates new source url to configured storage
|
||||||
|
* @constructor
|
||||||
|
* @param {string} title - Title/Name of the RSS feed.
|
||||||
|
* @param {string} link - URL of RSS feed.
|
||||||
|
* @param {string} category - Category of RSS feed.
|
||||||
|
*/
|
||||||
|
exports.addSource = async (title, link, category, guildId, channelId, callback) => {
|
||||||
|
feedStorage.create([{
|
||||||
|
"fields": {
|
||||||
|
"title": title,
|
||||||
|
"link": link,
|
||||||
|
"category": category,
|
||||||
|
'guild_id': guildId,
|
||||||
|
"channel_id": channelId
|
||||||
|
}
|
||||||
|
}], function (err, record) {
|
||||||
|
if (err) {
|
||||||
|
log.ERROR("Error in create:", err);
|
||||||
|
return callback(err, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!record) return callback(undefined, false);
|
||||||
|
|
||||||
|
log.DEBUG("Record ID:", record.getId());
|
||||||
|
|
||||||
|
return callback(undefined, record);
|
||||||
|
});
|
||||||
|
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Deletes a new source url by title
|
||||||
|
* @constructor
|
||||||
|
* @param {string} title - Title/Name of the RSS feed.
|
||||||
|
*/
|
||||||
|
exports.deleteSource = function (title, callback) {
|
||||||
|
feedStorage.getRecordBy('title', title, (err, results) => {
|
||||||
|
if (err) return callback(err, undefined);
|
||||||
|
|
||||||
|
if (!results?.id) {
|
||||||
|
log.DEBUG("No record found for title: ", title)
|
||||||
|
return callback(undefined, undefined);
|
||||||
|
}
|
||||||
|
|
||||||
|
feedStorage.destroy(results.id, function (err, deletedRecord) {
|
||||||
|
if (err) {
|
||||||
|
log.ERROR(err);
|
||||||
|
return callback(err, undefined);
|
||||||
|
}
|
||||||
|
log.DEBUG("Deleted Record: ", deletedRecord);
|
||||||
|
return callback(undefined, deletedRecord ?? true);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Update channels with new posts from sources
|
||||||
|
*/
|
||||||
|
exports.updateFeeds = (client) => {
|
||||||
|
if (!client) throw new Error("Client object not passed");
|
||||||
|
// Create a temp pool to use for all connections while updating the feed
|
||||||
|
var tempConnection = mysql.createPool({
|
||||||
|
host: process.env.EM_DB_HOST,
|
||||||
|
user: process.env.EM_DB_USER,
|
||||||
|
password: process.env.EM_DB_PASS,
|
||||||
|
database: process.env.EM_DB_NAME,
|
||||||
|
connectionLimit: 10
|
||||||
|
});
|
||||||
|
|
||||||
|
const tempFeedStorage = new FeedStorage(tempConnection);
|
||||||
|
const tempPostStorage = new PostStorage(tempConnection);
|
||||||
|
|
||||||
|
// Array of promises to wait on before closing the connection
|
||||||
|
var recordPromiseArray = [];
|
||||||
|
var sourcePromiseArray = [];
|
||||||
|
|
||||||
|
tempFeedStorage.getAllRecords(async (err, records) => {
|
||||||
|
// Load the posts from each RSS source
|
||||||
|
for (const source of records) {
|
||||||
|
sourcePromiseArray.push(new Promise((resolve, reject) => {
|
||||||
|
log.DEBUG('Record title: ', source.title);
|
||||||
|
log.DEBUG('Record link: ', source.link);
|
||||||
|
log.DEBUG('Record category: ', source.category);
|
||||||
|
log.DEBUG('Record guild ID: ', source.guild_id);
|
||||||
|
log.DEBUG('Record channel ID: ', source.channel_id);
|
||||||
|
// Parse the RSS feed
|
||||||
|
parser.parseURL(source.link, async (err, parsedFeed) => {
|
||||||
|
if (err) {
|
||||||
|
log.ERROR("Parser Error: ", runningPostsToRemove, source, err);
|
||||||
|
// Call the wrapper to make sure the site isn't just down at the time it checks and is back up the next time
|
||||||
|
this.removeSource(source.link);
|
||||||
|
reject;
|
||||||
|
}
|
||||||
|
try {
|
||||||
|
if (parsedFeed?.items){
|
||||||
|
this.unsetRemoveSource(source.link);
|
||||||
|
for (const post of parsedFeed.items.reverse()){
|
||||||
|
recordPromiseArray.push(new Promise((recordResolve, recordReject) => {
|
||||||
|
log.DEBUG("Parsed Source Keys", Object.keys(post), post?.title);
|
||||||
|
log.VERBOSE("Post from feed: ", post);
|
||||||
|
if (!post.title || !post.link) return recordReject("Missing information from the post");
|
||||||
|
if (!post.content || !post['content:encoded']) log.WARN("There is no content for post: ", post.title);
|
||||||
|
|
||||||
|
post.postId = post.postId ?? post.guid ?? post.id ?? libUtils.returnHash(post.title, post.link, post.pubDate ?? Date.now());
|
||||||
|
tempPostStorage.getRecordBy('post_guid', post.postId, (err, existingRecord) => {
|
||||||
|
if (err) throw err;
|
||||||
|
|
||||||
|
log.DEBUG("Existing post record: ", existingRecord);
|
||||||
|
if (existingRecord) return recordResolve("Existing record found for this post");
|
||||||
|
|
||||||
|
const channel = client.channels.cache.get(source.channel_id);
|
||||||
|
libUtils.sendPost(post, source, channel, (err, sendResults) =>{
|
||||||
|
if (err) throw err;
|
||||||
|
|
||||||
|
if (!sendResults) {
|
||||||
|
log.ERROR("No sending results from sending a post: ", sendResults, existingRecord, post);
|
||||||
|
return recordReject("No sending results from sending a post");
|
||||||
|
}
|
||||||
|
|
||||||
|
log.DEBUG("Saving post to database: ", sendResults, post.title, source.channel_id);
|
||||||
|
|
||||||
|
tempPostStorage.savePost(post, (err, saveResults) => {
|
||||||
|
if(err) throw err;
|
||||||
|
|
||||||
|
if (saveResults) {
|
||||||
|
log.DEBUG("Saved results: ", saveResults);
|
||||||
|
return recordResolve("Saved results", saveResults);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
})
|
||||||
|
})
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
}
|
||||||
|
else {
|
||||||
|
this.removeSource(source.link);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
catch (err) {
|
||||||
|
log.ERROR("Error Parsing Feed: ", source.link, err);
|
||||||
|
this.removeSource(source.link);
|
||||||
|
throw err;
|
||||||
|
}
|
||||||
|
Promise.all(recordPromiseArray).then((values) => {
|
||||||
|
log.DEBUG("All posts finished for: ", source.title, values);
|
||||||
|
return resolve(source.title);
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}))
|
||||||
|
}
|
||||||
|
|
||||||
|
// Wait for all connections to finish then close the temp connections
|
||||||
|
|
||||||
|
Promise.all(sourcePromiseArray).then((values) => {
|
||||||
|
log.DEBUG("All sources finished, closing temp connections: ", values);
|
||||||
|
tempConnection.end();
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search a state for any weather alerts
|
||||||
|
*
|
||||||
|
* @param {*} state The state to search for any weather alerts in
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
exports.weatherAlert = async function (state) {
|
||||||
|
|
||||||
|
var answerURL = `https://api.weather.gov/alerts/active?area=${state}`;
|
||||||
|
log.DEBUG(answerURL);
|
||||||
|
answerData = [];
|
||||||
|
|
||||||
|
await axios.get(answerURL)
|
||||||
|
.then(response => {
|
||||||
|
response.data.features.forEach(feature => {
|
||||||
|
answerData.push(feature);
|
||||||
|
})
|
||||||
|
|
||||||
|
return answerData;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
log.DEBUG(error);
|
||||||
|
});
|
||||||
|
return answerData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Gets a random food recipe
|
||||||
|
*
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
exports.getFood = async function () {
|
||||||
|
|
||||||
|
var answerURL = `https://www.themealdb.com/api/json/v1/1/random.php`;
|
||||||
|
log.DEBUG(answerURL);
|
||||||
|
answerData = {
|
||||||
|
text: `No answer found try using a simpler search term`,
|
||||||
|
source: ``
|
||||||
|
}
|
||||||
|
await axios.get(answerURL)
|
||||||
|
.then(response => {
|
||||||
|
//log.DEBUG(response.data.RelatedTopics[0].Text);
|
||||||
|
//log.DEBUG(response.data.RelatedTopics[0].FirstURL);
|
||||||
|
|
||||||
|
// if (response.data.meals.length != 0) {
|
||||||
|
|
||||||
|
answerData = {
|
||||||
|
strMeal: `No Data`,
|
||||||
|
strSource: `-`,
|
||||||
|
strInstructions: `-`,
|
||||||
|
strMealThumb: `-`,
|
||||||
|
strCategory: `-`
|
||||||
|
}
|
||||||
|
|
||||||
|
answerData = {
|
||||||
|
strMeal: `${unescape(response.data.meals[0].strMeal)}`,
|
||||||
|
strSource: `${unescape(response.data.meals[0].strSource)}`,
|
||||||
|
strInstructions: `${unescape(response.data.meals[0].strInstructions)}`,
|
||||||
|
strMealThumb: `${unescape(response.data.meals[0].strMealThumb)}`,
|
||||||
|
strCategory: `${unescape(response.data.meals[0].strCategory)}`
|
||||||
|
}
|
||||||
|
// } else {
|
||||||
|
|
||||||
|
//}
|
||||||
|
return answerData;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
log.DEBUG(error);
|
||||||
|
});
|
||||||
|
return answerData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* Search Urban dictionary for a phrase
|
||||||
|
*
|
||||||
|
* @param {*} question The phrase to search urban dictionary for
|
||||||
|
* @returns
|
||||||
|
*/
|
||||||
|
exports.getSlang = async function (question) {
|
||||||
|
|
||||||
|
var answerURL = `https://api.urbandictionary.com/v0/define?term=${question}`;
|
||||||
|
log.DEBUG(answerURL);
|
||||||
|
slangData = {
|
||||||
|
definition: `No answer found try using a simpler search term`,
|
||||||
|
example: ``
|
||||||
|
}
|
||||||
|
await axios.get(answerURL)
|
||||||
|
.then(response => {
|
||||||
|
log.DEBUG(response.data.list[0]);
|
||||||
|
|
||||||
|
slangData = {
|
||||||
|
definition: `${unescape(response.data.list[0].definition) ? unescape(response.data.list[0].definition) : ''}`,
|
||||||
|
example: `${unescape(response.data.list[0].example) ? unescape(response.data.list[0].example) : ''}`,
|
||||||
|
thumbs_down: `${unescape(response.data.list[0].thumbs_down)}`,
|
||||||
|
thumbs_up: `${unescape(response.data.list[0].thumbs_up)}`
|
||||||
|
}
|
||||||
|
|
||||||
|
return slangData;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
log.DEBUG(error);
|
||||||
|
});
|
||||||
|
return slangData;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getSources - Get the RSS sources currently in use
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
exports.getSources = function () {
|
||||||
|
feedStorage.getAllRecords((err, records) => {
|
||||||
|
if (err) throw err;
|
||||||
|
return records;
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getQuotes - Get a random quote from the local list
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
exports.getQuotes = async function (quote_url) {
|
||||||
|
|
||||||
|
var data = [];
|
||||||
|
await axios.get(quote_url)
|
||||||
|
.then(response => {
|
||||||
|
log.DEBUG(response.data[0].q);
|
||||||
|
log.DEBUG(response.data[0].a);
|
||||||
|
data = response.data;
|
||||||
|
|
||||||
|
return data;
|
||||||
|
})
|
||||||
|
.catch(error => {
|
||||||
|
log.DEBUG(error);
|
||||||
|
});
|
||||||
|
return data;
|
||||||
|
}
|
||||||
|
|
||||||
|
/**
|
||||||
|
* getCategories - Returns feed categories
|
||||||
|
* @constructor
|
||||||
|
*/
|
||||||
|
exports.getCategories = async (callback) => {
|
||||||
|
feedStorage.getUniqueByKey("category", (err, results) => {
|
||||||
|
if (err) return callback(err, undefined);
|
||||||
|
|
||||||
|
return callback(undefined, results);
|
||||||
|
});
|
||||||
|
}
|
||||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user