Compare commits
109 Commits
184458608d
...
feature/#1
| Author | SHA1 | Date | |
|---|---|---|---|
|
|
96b0bf6adb | ||
|
|
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 | ||
|
|
1316c109e9 | ||
|
|
c380bb2770 | ||
|
|
a20d7cfaec | ||
|
|
07ca9e88e0 | ||
|
|
5e31346bc9 | ||
|
|
6e05dde743 | ||
|
|
82ba506749 | ||
|
|
f995cd3578 | ||
|
|
c15d8ca5ee | ||
|
|
030953f692 | ||
|
|
6c294fe803 | ||
|
|
e27cfdfdd4 | ||
|
|
cfba6ffa61 | ||
|
|
f5076d40cc | ||
|
|
8d68f87ec0 | ||
|
|
9377edd518 | ||
|
|
2e8b699339 | ||
|
|
e537ce8778 | ||
|
|
95c714f0aa | ||
|
|
4a0e5e26de | ||
|
|
67aa65d60d | ||
|
|
5bbcc3ffcc | ||
|
|
ccb42e7fba | ||
|
|
988683ab72 | ||
|
|
7b821687a9 | ||
|
|
fd3de1ede4 | ||
|
|
444346e2b5 | ||
|
|
eebd058eae | ||
|
|
3ddf9ea782 | ||
|
|
d3d62b8d28 | ||
|
|
b296da629b | ||
|
|
403b533a33 | ||
|
|
faec0f2291 | ||
|
|
9a2416e2ff | ||
|
|
a8be6598f2 |
4
.gitignore
vendored
4
.gitignore
vendored
@@ -5,4 +5,6 @@ node_modules/
|
||||
# Development files
|
||||
*.log
|
||||
*.txt
|
||||
|
||||
*.env
|
||||
!requirements.txt
|
||||
*testOP25Dir/
|
||||
@@ -1 +0,0 @@
|
||||
DEBUG="client:*";
|
||||
20
Client/.env.example
Normal file
20
Client/.env.example
Normal file
@@ -0,0 +1,20 @@
|
||||
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=""
|
||||
@@ -3,17 +3,26 @@ var express = require('express');
|
||||
var path = require('path');
|
||||
var cookieParser = require('cookie-parser');
|
||||
var logger = require('morgan');
|
||||
var http = require('http');
|
||||
require('dotenv').config();
|
||||
const { DebugBuilder } = require("./utilities/debugBuilder");
|
||||
const { checkIn } = require("./controllers/clientController");
|
||||
|
||||
var indexRouter = require('./routes/index');
|
||||
var botRouter = require('./routes/bot');
|
||||
var clientRouter = require('./routes/client');
|
||||
var radioRouter = require('./routes/radio');
|
||||
var { attachRadioSessionToRequest } = require('./controllers/radioController');
|
||||
|
||||
const log = new DebugBuilder("client", "app");
|
||||
|
||||
var app = express();
|
||||
var port = process.env.HTTP_PORT || '3010';
|
||||
|
||||
// view engine setup
|
||||
app.set('views', path.join(__dirname, 'views'));
|
||||
app.set('view engine', 'ejs');
|
||||
app.set('port', port);
|
||||
|
||||
app.use(logger('dev'));
|
||||
app.use(express.json());
|
||||
@@ -21,24 +30,25 @@ app.use(express.urlencoded({ extended: false }));
|
||||
app.use(cookieParser());
|
||||
app.use(express.static(path.join(__dirname, 'public')));
|
||||
|
||||
// Default route
|
||||
app.use('/', indexRouter);
|
||||
|
||||
// Discord bot control route
|
||||
app.use('/bot', botRouter);
|
||||
app.use('/bot', attachRadioSessionToRequest, botRouter);
|
||||
|
||||
// Local client control route
|
||||
app.use("/client", clientRouter);
|
||||
|
||||
// Local radio controller route
|
||||
app.use("/radio", radioRouter);
|
||||
app.use("/radio", attachRadioSessionToRequest, radioRouter);
|
||||
|
||||
// catch 404 and forward to error handler
|
||||
app.use(function(req, res, next) {
|
||||
app.use((req, res, next) => {
|
||||
next(createError(404));
|
||||
});
|
||||
|
||||
// error handler
|
||||
app.use(function(err, req, res, next) {
|
||||
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 : {};
|
||||
@@ -48,4 +58,44 @@ app.use(function(err, req, res, next) {
|
||||
res.render('error');
|
||||
});
|
||||
|
||||
module.exports = app;
|
||||
/**
|
||||
* Start the HTTP background server
|
||||
*/
|
||||
async function runHTTPServer() {
|
||||
var server = http.createServer(app);
|
||||
server.listen(port);
|
||||
|
||||
server.on('error', (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':
|
||||
log.ERROR(bind + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
log.ERROR(bind + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
});
|
||||
|
||||
server.on('listening', () => {
|
||||
log.INFO("HTTP server started!");
|
||||
})
|
||||
}
|
||||
|
||||
log.DEBUG(`Starting HTTP Server`);
|
||||
runHTTPServer();
|
||||
|
||||
log.DEBUG("Checking in with the master server")
|
||||
checkIn();
|
||||
|
||||
@@ -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,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 +1 @@
|
||||
{"Westchester Cty. Simulcast":{"frequencies":[470575000,470375000,470525000,470575000,470550000],"mode":"p25","trunkFile":"trunk.tsv"},"coppies":{"frequencies":[154875000],"mode":"nbfm","trunkFile":"none"}}
|
||||
{"Westchester Cty. Simulcast":{"frequencies":[470575000,470375000,470525000,470575000,470550000],"mode":"p25","trunkFile":"trunk.tsv"},"coppies":{"frequencies":[154690000],"mode":"nbfm","trunkFile":"none"},"poopoo":{"frequencies":[479135500],"mode":"nbfm","trunkFile":"none"},"ppeeeeeeeeee":{"frequencies":[479135500,133990000,133000000,555999000],"mode":"p25","trunkFile":"none"}}
|
||||
@@ -1,87 +1,84 @@
|
||||
// Debug
|
||||
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
|
||||
const path = require('path');
|
||||
const fork = require('child_process').fork;
|
||||
const discordBotPath = path.resolve('discord-bot/app.js');
|
||||
const spawn = require('child_process').spawn;
|
||||
const { resolve } = require("path");
|
||||
require('dotenv').config();
|
||||
const { closeProcessWrapper } = require("../utilities/utilities");
|
||||
|
||||
let botChildProcess, tempRes;
|
||||
|
||||
/**
|
||||
* Bot Process Object Builder
|
||||
*
|
||||
* This construnctor is used to easily pass commands to the bot process
|
||||
*/
|
||||
class BPOB {
|
||||
/**
|
||||
* Build an object to be passed to the bot process
|
||||
* @param command The command to be run ("Status", "Join", "Leave", "ChgPreSet")
|
||||
* @param parameters Depending on the command being run, there parameters required in order to be run
|
||||
*/
|
||||
constructor(command = "Status"||"Join"||"Leave"||"ChgPreSet", parameters = []||undefined) {
|
||||
this.cmd = command;
|
||||
if (parameters) this.params = parameters;
|
||||
}
|
||||
}
|
||||
// Global vars
|
||||
let pythonProcess;
|
||||
|
||||
/**
|
||||
* Get Status of the discord process
|
||||
*/
|
||||
exports.getStatus = (req, res) => {
|
||||
if (!botChildProcess) return res.sendStatus(200);
|
||||
botChildProcess.send(new BPOB("Status"));
|
||||
tempRes = res;
|
||||
log.INFO("Getting the status of the bot");
|
||||
if (pythonProcess) return res.sendStatus(200);
|
||||
return res.sendStatus(201);
|
||||
}
|
||||
|
||||
/**
|
||||
* Start the bot and join the server and preset specified
|
||||
*/
|
||||
exports.joinServer = (req, res) => {
|
||||
const channelID = req.body.channelID;
|
||||
exports.joinServer = async (req, res) => {
|
||||
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 NGThreshold = req.body.NGThreshold ?? 50
|
||||
|
||||
if (!channelID || !presetName) return res.status(400).json({'message': "Channel ID or Preset Name not present in the request"});
|
||||
// Start the bot
|
||||
botChildProcess = fork(discordBotPath);
|
||||
// Joining the discord server
|
||||
log.INFO("Join requested to: ", deviceId, channelId, clientId, presetName, NGThreshold);
|
||||
if (process.platform === "win32") {
|
||||
log.DEBUG("Starting Windows Python");
|
||||
pythonProcess = await spawn('python.exe', [resolve(__dirname, "../pdab/main.py"), deviceId, channelId, clientId, '-n', NGThreshold], { 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 ], { cwd: resolve(__dirname, "../pdab/") });
|
||||
}
|
||||
|
||||
// Handle bot responses
|
||||
botChildProcess.on('message', (msg) => {
|
||||
log.DEBUG('Child response: ', msg);
|
||||
if (msg.msg === "INIT READY") {
|
||||
// Discord bot has started and is ready.
|
||||
botChildProcess.send(new BPOB("Join", {"channelID": channelID, "presetName": presetName}))
|
||||
tempRes = res;
|
||||
}
|
||||
switch (msg.cmd){
|
||||
case "Status":
|
||||
if (msg.msg === "VDISCONN") tempRes.sendStatus(201); // VDISCONN == Voice DISCONNected
|
||||
else tempRes.sendStatus(202);
|
||||
tempRes = undefined;
|
||||
return;
|
||||
case "Join":
|
||||
tempRes.sendStatus(202);
|
||||
tempRes = undefined;
|
||||
return;
|
||||
case "Leave":
|
||||
tempRes.sendStatus(202);
|
||||
tempRes = undefined;
|
||||
botChildProcess.kill();
|
||||
botChildProcess = undefined;
|
||||
return;
|
||||
case "ChgPreSet":
|
||||
tempRes.sendStatus(200);
|
||||
tempRes = undefined;
|
||||
return;
|
||||
}
|
||||
})
|
||||
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
|
||||
*/
|
||||
exports.leaveServer = (req, res) => {
|
||||
if (!botChildProcess) return res.sendStatus(200)
|
||||
botChildProcess.send(new BPOB("Leave"));
|
||||
tempRes = res;
|
||||
exports.leaveServer = async (req, res) => {
|
||||
log.INFO("Leaving the server");
|
||||
if (!pythonProcess) return res.sendStatus(200)
|
||||
|
||||
pythonProcess = await closeProcessWrapper(pythonProcess);
|
||||
|
||||
return res.sendStatus(202);
|
||||
}
|
||||
@@ -1,13 +1,17 @@
|
||||
// Debug
|
||||
// const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||
// const log = new DebugBuilder("client", "clientController");
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||
const log = new DebugBuilder("client", "clientController");
|
||||
// Configs
|
||||
const config = require("../config/clientConfig");
|
||||
require('dotenv').config();
|
||||
const modes = require("../config/modes");
|
||||
// Modules
|
||||
const { executeAsyncConsoleCommand, nodeObject } = require("../utilities/utilities");
|
||||
// Utilities
|
||||
const updateConfig = require("../utilities/updateConfig");
|
||||
const updatePreset = require("../utilities/updatePresets");
|
||||
const requests = require("../utilities/httpRequests");
|
||||
const { updateId, updateConfig } = require("../utilities/updateConfig");
|
||||
const { updatePreset, addNewPreset, getPresets, removePreset } = require("../utilities/updatePresets");
|
||||
const { onHttpError, requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
|
||||
|
||||
var runningClientConfig = new nodeObject({_id: process.env.CLIENT_ID, _ip: process.env.CLIENT_IP, _name: process.env.CLIENT_NAME, _port: process.env.CLIENT_PORT, _location: process.env.CLIENT_LOCATION, _nearbySystems: getPresets(), _online: process.env.CLIENT_ONLINE});
|
||||
|
||||
/**
|
||||
* Check the body for the required fields to update or add a preset
|
||||
@@ -29,38 +33,108 @@ function checkBodyForPresetFields(req, res, callback) {
|
||||
return callback();
|
||||
}
|
||||
|
||||
async function checkLocalIP() {
|
||||
if (process.platform === "win32") {
|
||||
// Windows
|
||||
var networkConfig = await executeAsyncConsoleCommand("ipconfig");
|
||||
log.DEBUG('Network Config: ', networkConfig);
|
||||
var networkConfigLines = await networkConfig.split("\n").filter(line => {
|
||||
if (!line.includes(":")) return false;
|
||||
|
||||
line = line.split(":");
|
||||
|
||||
if (!line.length === 2) return false;
|
||||
|
||||
return true;
|
||||
}).map(line => {
|
||||
line = String(line).split(':', 2);
|
||||
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 {
|
||||
// Linux
|
||||
var networkConfig = await executeAsyncConsoleCommand("ip addr");
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Checks the config file for all required fields or gets and updates the required fields
|
||||
*/
|
||||
exports.checkConfig = async function checkConfig() {
|
||||
if (!runningClientConfig.ip) {
|
||||
const ipAddr = await checkLocalIP();
|
||||
updateConfig('ip', ipAddr);
|
||||
runningClientConfig.ip = ipAddr;
|
||||
}
|
||||
|
||||
if(!runningClientConfig.name) {
|
||||
const lastOctet = await String(checkLocalIP()).spit('.')[-1];
|
||||
const name = `Radio-Node-${lastOctet}`;
|
||||
updateConfig('name', name);
|
||||
runningClientConfig.name = name;
|
||||
}
|
||||
|
||||
if(!runningClientConfig.port) {
|
||||
const port = 3010;
|
||||
updateConfig('port', port);
|
||||
runningClientConfig.port = port;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/** Check in with the server
|
||||
* If the bot has a saved ID, check in with the server to update any information or just check back in
|
||||
* If the bot does not have a saved ID, it will attempt to request a new ID from the server
|
||||
*/
|
||||
exports.checkIn = async () => {
|
||||
let reqOptions;
|
||||
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
|
||||
if (config.clientConfig.id === 0) {
|
||||
try {
|
||||
if (runningClientConfig.id === 0) {
|
||||
// ID was not found in the config, creating a new node
|
||||
reqOptions = new requests.requestOptions("/nodes/newNode", "POST");
|
||||
delete config.clientConfig.id;
|
||||
requests.sendHttpRequest(reqOptions, JSON.stringify(config.clientConfig), (responseObject) => {
|
||||
reqOptions = new requestOptions("/nodes/newNode", "POST");
|
||||
sendHttpRequest(reqOptions, JSON.stringify(), (responseObject) => {
|
||||
// Update the client's ID if the server accepted it
|
||||
if (responseObject.statusCode === 202) {
|
||||
config.clientConfig.id = responseObject.body.nodeId;
|
||||
updateConfig.updateId(responseObject.body.nodeId);
|
||||
runningClientConfig.id = responseObject.body.nodeId;
|
||||
updateId(responseObject.body.nodeId);
|
||||
}
|
||||
|
||||
if (responseObject.statusCode >= 300) {
|
||||
// Server threw an error
|
||||
onHttpError(responseObject.statusCode);
|
||||
}
|
||||
|
||||
});
|
||||
}
|
||||
else {
|
||||
// ID is in the config, checking in with the server
|
||||
reqOptions = new requests.requestOptions("/nodes/nodeCheckIn", "POST");
|
||||
requests.sendHttpRequest(reqOptions, JSON.stringify(config.clientConfig), (responseObject) => {
|
||||
reqOptions = new requestOptions("/nodes/nodeCheckIn", "POST");
|
||||
sendHttpRequest(reqOptions, JSON.stringify(runningClientConfig), (responseObject) => {
|
||||
if (responseObject.statusCode === 202) {
|
||||
// Server accepted an update
|
||||
}
|
||||
if (responseObject.statusCode === 200) {
|
||||
// Server accepted the response but there were no keys to be updated
|
||||
}
|
||||
if (responseObject.statusCode >= 300) {
|
||||
// Server threw an error
|
||||
onHttpError(responseObject.statusCode);
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
catch (err) {
|
||||
log.ERROR("Error checking in: ", err);
|
||||
}
|
||||
}
|
||||
|
||||
/** Controller for the /client/requestCheckIn endpoint
|
||||
* This is the endpoint wrapper to queue a check in
|
||||
@@ -74,7 +148,7 @@ exports.requestCheckIn = async (req, res) => {
|
||||
* This is the endpoint wrapper to get the presets object
|
||||
*/
|
||||
exports.getPresets = async (req, res) => {
|
||||
return res.status(200).json(updatePreset.getPresets());
|
||||
return res.status(200).json(getPresets());
|
||||
}
|
||||
|
||||
/** Controller for the /client/updatePreset endpoint
|
||||
@@ -82,7 +156,7 @@ exports.getPresets = async (req, res) => {
|
||||
*/
|
||||
exports.updatePreset = async (req, res) => {
|
||||
checkBodyForPresetFields(req, res, () => {
|
||||
updatePreset.updatePreset(req.body.systemName, () => {
|
||||
updatePreset(req.body.systemName, () => {
|
||||
return res.sendStatus(200);
|
||||
}, {frequencies: req.body.frequencies, mode: req.body.mode, trunkFile: req.body.trunkFile});
|
||||
})
|
||||
@@ -93,10 +167,28 @@ exports.updatePreset = async (req, res) => {
|
||||
*/
|
||||
exports.addNewPreset = async (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, () => {
|
||||
return res.sendStatus(200);
|
||||
}, 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, () => {
|
||||
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);
|
||||
}
|
||||
|
||||
@@ -2,146 +2,72 @@
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||
const log = new DebugBuilder("client", "radioController");
|
||||
// Modules
|
||||
const { resolve, dirname } = require('path');
|
||||
const fs = require('fs');
|
||||
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");
|
||||
require('dotenv').config();
|
||||
const { closeProcessWrapper, changeCurrentConfigWrapper, openRadioSessionWrapper } = require("../utilities/utilities");
|
||||
|
||||
let radioChildProcess, tempRes, radioConfigPath;
|
||||
let radioChildProcess;
|
||||
|
||||
/**
|
||||
* Closes the radio executable if it's in one
|
||||
*/
|
||||
exports.closeRadioSession = (req, res) => {
|
||||
if (!radioChildProcess) return res.sendStatus(200)
|
||||
tempRes = res;
|
||||
radioChildProcess.kill();
|
||||
radioChildProcess = undefined;
|
||||
exports.closeRadioSession = async (req, res) => {
|
||||
if (!radioChildProcess || !req.body.radioSession) return res.sendStatus(204);
|
||||
if (radioChildProcess) radioChildProcess = await closeProcessWrapper(radioChildProcess);
|
||||
if (req.body.radioSession) req.body.radioSession = await closeProcessWrapper(req.body.radioSession);
|
||||
if (!radioChildProcess) return res.sendStatus(200);
|
||||
}
|
||||
|
||||
/**
|
||||
* Change the current 'cfg.json' file to the preset specified
|
||||
* @param {string} presetName
|
||||
*/
|
||||
exports.changeCurrentConfig = (req, res) => {
|
||||
// Check if the given config is saved
|
||||
log.DEBUG("[/radio/changeCurrentConfig] - Checking if provided preset is in the config");
|
||||
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
|
||||
exports.changeCurrentConfig = async (req, res) => {
|
||||
const presetName = req.body.presetName;
|
||||
if (!presetName) return res.status(500).json("You must include the preset name")
|
||||
|
||||
// Check if the current config is the same as the preset given
|
||||
const currentConfig = readOP25Config();
|
||||
if (currentConfig.channels && currentConfig.channels.name === req.body.presetName) {
|
||||
log.DEBUG("[/radio/changeCurrentConfig] - Current config is the same as the preset given");
|
||||
return res.sendStatus(202);
|
||||
const updatedConfigObject = await changeCurrentConfigWrapper(presetName);
|
||||
|
||||
// No change was made to the config
|
||||
if (!updatedConfigObject) return res.sendStatus(200);
|
||||
|
||||
// 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
|
||||
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);
|
||||
})
|
||||
return res.sendStatus(202);
|
||||
}
|
||||
|
||||
/**
|
||||
* Open a new OP25 process tuned to the specified system
|
||||
*/
|
||||
exports.openRadioSession = () => {
|
||||
if (radioChildProcess) closeRadioSession();
|
||||
radioChildProcess = spawn(getRadioBinPath());
|
||||
exports.openRadioSession = async (req, res) => {
|
||||
const presetName = req.body.presetName;
|
||||
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(){
|
||||
return resolve(radioConfig.bin);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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;
|
||||
exports.attachRadioSessionToRequest = async (req, res, next) => {
|
||||
req.body.radioSession = radioChildProcess;
|
||||
next();
|
||||
}
|
||||
|
||||
|
||||
@@ -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;
|
||||
@@ -1,59 +0,0 @@
|
||||
// Debug
|
||||
import ModuleDebugBuilder from "../utilities/moduleDebugBuilder.js";
|
||||
const log = new ModuleDebugBuilder("bot", "join");
|
||||
// Modules
|
||||
import { joinVoiceChannel, VoiceConnectionStatus } from "@discordjs/voice";
|
||||
import {replyToInteraction} from "../utilities/messageHandler.js";
|
||||
import {createAudioInstance} from "../controllers/audioController.js";
|
||||
import OpusEncoderPkg from "@discordjs/opus";
|
||||
|
||||
// Declare the encoder (module is incompatible modern import method)
|
||||
const { OpusEncoder } = OpusEncoderPkg;
|
||||
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
|
||||
*/
|
||||
export default 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 = createAudioInstance();
|
||||
|
||||
audioInstance.on('data', buffer => {
|
||||
buffer = Buffer.from(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.quit();
|
||||
})
|
||||
|
||||
audioInstance.start();
|
||||
|
||||
if (guildID && callback) callback();
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
import {getVoiceConnection} from "@discordjs/voice";
|
||||
import {replyToInteraction} from "../utilities/messageHandler.js";
|
||||
// Debug
|
||||
//import debugBuilder from "../utilities/moduleDebugBuilder.js";
|
||||
//const log = new debugBuilder("bot", "leave");
|
||||
|
||||
/**
|
||||
* If in a voice channel for the specified guild, leave
|
||||
*
|
||||
* @param interaction Message interaction from discord
|
||||
* @param guildID
|
||||
* @param callback
|
||||
*/
|
||||
export default 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);
|
||||
}
|
||||
@@ -1,6 +0,0 @@
|
||||
// Utilities
|
||||
import { replyToInteraction } from '../utilities/messageHandler.js';
|
||||
|
||||
export default function ping(interaction) {
|
||||
return replyToInteraction(interaction, "Pong! I have Aids and now you do too!");
|
||||
}
|
||||
@@ -1,31 +0,0 @@
|
||||
// Debug
|
||||
import ModuleDebugBuilder from "../utilities/moduleDebugBuilder.js";
|
||||
const log = new ModuleDebugBuilder("bot", "status");
|
||||
// Modules
|
||||
import {getVoiceConnection} from "@discordjs/voice";
|
||||
// Utilities
|
||||
import { replyToInteraction } from '../utilities/messageHandler.js';
|
||||
|
||||
|
||||
export default 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": voiceConnection
|
||||
}
|
||||
|
||||
//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);
|
||||
}
|
||||
}
|
||||
@@ -1,7 +0,0 @@
|
||||
{
|
||||
"TOKEN": "OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY",
|
||||
"ApplicationID": "943742040255115304",
|
||||
"GuildID": "367396189529833472",
|
||||
"DeviceID": "5",
|
||||
"DeviceName": "VoiceMeeter Aux Output (VB-Audi"
|
||||
}
|
||||
@@ -1,56 +0,0 @@
|
||||
// Config
|
||||
import { getDeviceID } from '../utilities/configHandler.js';
|
||||
// Modules
|
||||
import alsaInstance from 'alsa-capture';
|
||||
import executeAsyncConsoleCommand from "../utilities/executeConsoleCommand.js";
|
||||
// Debug
|
||||
import ModuleDebugBuilder from "../utilities/moduleDebugBuilder.js";
|
||||
// Global Vars
|
||||
const log = new ModuleDebugBuilder("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}
|
||||
*/
|
||||
export function confirmAudioDevice({deviceName = undefined, deviceId = undefined}){
|
||||
const deviceList = 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);
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a list of the audio devices connected with input channels
|
||||
*
|
||||
* @returns {unknown[]}
|
||||
*/
|
||||
export function getAudioDevices(){
|
||||
// Exec output contains both stderr and stdout outputs
|
||||
const deviceList = executeAsyncConsoleCommand("arecord -L");
|
||||
log.DEBUG("Device list: ", deviceList);
|
||||
}
|
||||
|
||||
/**
|
||||
* 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
|
||||
*/
|
||||
export function createAudioInstance() {
|
||||
const selectedDevice = 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.id ?? "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
|
||||
});
|
||||
}
|
||||
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,50 +0,0 @@
|
||||
// Debug
|
||||
import ModuleDebugBuilder from "./moduleDebugBuilder.js";
|
||||
const log = new ModuleDebugBuilder("bot", "configHandler");
|
||||
// Modules
|
||||
import { readFileSync } from 'fs';
|
||||
import path from "path";
|
||||
|
||||
export function getConfig() {
|
||||
return JSON.parse(readFileSync(path.resolve("discord-bot/config/botConfig.json")));
|
||||
}
|
||||
|
||||
export function getTOKEN() {
|
||||
const parsedJSON = getConfig();
|
||||
const token = parsedJSON.TOKEN;
|
||||
|
||||
log.DEBUG("Discord API Token: ", token)
|
||||
return token;
|
||||
}
|
||||
|
||||
export function getGuildID() {
|
||||
const parsedJSON = getConfig();
|
||||
const guildID = parsedJSON.GuildID;
|
||||
|
||||
log.DEBUG("Guild ID: ", guildID);
|
||||
return guildID;
|
||||
}
|
||||
|
||||
export function getApplicationID() {
|
||||
const parsedJSON = getConfig();
|
||||
const appID = parsedJSON.ApplicationID;
|
||||
|
||||
log.DEBUG("Application ID: ", appID);
|
||||
return appID;
|
||||
}
|
||||
|
||||
export function getDeviceID(){
|
||||
const parsedJSON = getConfig();
|
||||
const deviceID = parseInt(parsedJSON.DeviceID);
|
||||
|
||||
log.DEBUG("Device ID: ", deviceID);
|
||||
return deviceID;
|
||||
}
|
||||
|
||||
export function getDeviceName(){
|
||||
const parsedJSON = getConfig();
|
||||
const deviceName = parsedJSON.DeviceName;
|
||||
|
||||
log.DEBUG("Device Name: ", deviceName);
|
||||
return deviceName;
|
||||
}
|
||||
@@ -1,28 +0,0 @@
|
||||
// Modules
|
||||
import { promisify } from 'util';
|
||||
import { exec } from "child_process";
|
||||
// Debug
|
||||
import ModuleDebugBuilder from "../utilities/moduleDebugBuilder.js";
|
||||
// Global Vars
|
||||
const log = new ModuleDebugBuilder("bot", "executeConsoleCommand");
|
||||
const execCommand = promisify(exec);
|
||||
|
||||
|
||||
export default 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;
|
||||
}
|
||||
@@ -1,9 +0,0 @@
|
||||
// Debug
|
||||
import ModuleDebugBuilder from "./moduleDebugBuilder.js";
|
||||
const log = new ModuleDebugBuilder("bot", "messageHandler");
|
||||
|
||||
export 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));
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
}
|
||||
2533
Client/package-lock.json
generated
2533
Client/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -2,17 +2,24 @@
|
||||
"name": "client",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"main": "app.js",
|
||||
"scripts": {
|
||||
"start": "node ./bin/www"
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"preinstall": "echo preinstall",
|
||||
"postinstall": "echo postinstall"
|
||||
},
|
||||
"dependencies": {
|
||||
"convert-units": "^2.3.4",
|
||||
"cookie-parser": "~1.4.4",
|
||||
"debug": "~2.6.9",
|
||||
"dotenv": "16.0.3",
|
||||
"debug": "^4.3.4",
|
||||
"ejs": "~2.6.1",
|
||||
"express": "~4.16.1",
|
||||
"http-errors": "~1.6.3",
|
||||
"morgan": "~1.9.1",
|
||||
"replace-in-file": "~6.3.5"
|
||||
"replace-in-file": "~6.3.5",
|
||||
"@discordjs/builders": "^1.4.0",
|
||||
"@discordjs/rest": "^1.4.0",
|
||||
"discord.js": "^14.7.1"
|
||||
}
|
||||
}
|
||||
|
||||
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]}")
|
||||
75
Client/pdab/main.py
Normal file
75
Client/pdab/main.py
Normal file
@@ -0,0 +1,75 @@
|
||||
import argparse, platform, os
|
||||
from discord import Intents, Client, Member, opus
|
||||
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()
|
||||
print("Processor: ", processor)
|
||||
if os.name == 'nt':
|
||||
if processor == "AMD64":
|
||||
print(f"Loaded OPUS library for AMD64")
|
||||
opus.load_opus('./opus/libopus_amd64.dll')
|
||||
return "AMD64"
|
||||
else:
|
||||
if processor == "aarch64":
|
||||
print(f"Loaded OPUS library for aarch64")
|
||||
opus.load_opus('./opus/libopus_aarcch64.so')
|
||||
return "aarch64"
|
||||
elif processor == "armv7l":
|
||||
print(f"Loaded OPUS library for armv7l")
|
||||
opus.load_opus('./opus/libopus_armv7l.so')
|
||||
return "armv7l"
|
||||
|
||||
|
||||
def main(clientId='OTQzNzQyMDQwMjU1MTE1MzA0.Yg3eRA.ZxEbRr55xahjfaUmPY8pmS-RHTY', channelId=367396189529833476, NGThreshold=50, deviceId=1):
|
||||
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}')
|
||||
|
||||
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)
|
||||
|
||||
parser = argparse.ArgumentParser()
|
||||
parser.add_argument("deviceId", type=int, help="The ID of the audio device to use")
|
||||
parser.add_argument("channelId", type=int, help="The ID of the voice channel to use")
|
||||
parser.add_argument("clientId", type=str, help="The discord client ID")
|
||||
parser.add_argument("-n", "--NGThreshold", type=int, help="Change the noisegate threshold. This defaults to 50")
|
||||
args = parser.parse_args()
|
||||
|
||||
if (not args.NGThreshold):
|
||||
args.NGThreshold = 50
|
||||
|
||||
print("Arguments:", args)
|
||||
|
||||
main(
|
||||
clientId=args.clientId,
|
||||
channelId=args.channelId,
|
||||
NGThreshold=args.NGThreshold,
|
||||
deviceId=args.deviceId
|
||||
)
|
||||
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.
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
|
||||
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
|
||||
* 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
|
||||
*/
|
||||
router.get('/status', botController.getStatus);
|
||||
|
||||
@@ -2,22 +2,21 @@
|
||||
const express = require('express');
|
||||
const router = express.Router();
|
||||
// Controllers
|
||||
const clientController = require("../controllers/clientController");
|
||||
const { requestCheckIn, getPresets, updatePreset, addNewPreset, removePreset, updateClient } = require("../controllers/clientController");
|
||||
|
||||
/** GET Request a check in from the client
|
||||
* Queue the client to check in with the server
|
||||
*
|
||||
* The status of the checkin request: 200 = Queued
|
||||
*/
|
||||
router.get('/requestCheckIn', clientController.requestCheckIn);
|
||||
router.get('/requestCheckIn', requestCheckIn);
|
||||
|
||||
/** GET Object of all known presets
|
||||
* Query the client to get all the known presets
|
||||
*/
|
||||
router.get('/presets', clientController.getPresets);
|
||||
router.get('/presets', getPresets);
|
||||
|
||||
/** POST Update to preset
|
||||
* Join the channel specified listening to the specified freq/mode
|
||||
*
|
||||
* @param req The request sent from the master
|
||||
* @param {string} req.body.systemName The name of the system to be updated
|
||||
@@ -25,7 +24,7 @@ router.get('/presets', clientController.getPresets);
|
||||
* @param {string} req.body.mode The listening mode for the SDR
|
||||
* @param {string} req.body.trunkFile If the listening mode is digital this can be set to identify the communications
|
||||
*/
|
||||
router.post('/updatePreset', clientController.updatePreset);
|
||||
router.post('/updatePreset', updatePreset);
|
||||
|
||||
/** POST Add new preset
|
||||
* Join the channel specified listening to the specified freq/mode
|
||||
@@ -36,6 +35,20 @@ router.post('/updatePreset', clientController.updatePreset);
|
||||
* @param {string} req.body.mode The listening mode for the SDR
|
||||
* @param {string} req.body.trunkFile If the listening mode is digital this can be set to identify the communications
|
||||
*/
|
||||
router.post('/addPreset', clientController.addNewPreset);
|
||||
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;
|
||||
|
||||
@@ -15,6 +15,7 @@ router.post('/start', radioController.openRadioSession);
|
||||
|
||||
/**
|
||||
* POST Close the current radio session
|
||||
* Response from the radio: 200: closed; 204: not connected
|
||||
*/
|
||||
router.post('/stop', radioController.closeRadioSession);
|
||||
|
||||
|
||||
78
Client/setup.sh
Normal file
78
Client/setup.sh
Normal file
@@ -0,0 +1,78 @@
|
||||
#!/bin/bash
|
||||
|
||||
# Check if the user is root
|
||||
if [ "$EUID" -ne 0 ]
|
||||
then echo "Please run as root"
|
||||
exit
|
||||
fi
|
||||
SCRIPT_DIR=$( cd -- "$( dirname -- "${BASH_SOURCE[0]}" )" &> /dev/null && pwd )
|
||||
|
||||
ls -ld $SCRIPT_DIR | awk '{print $3}' >> ./config/installerName
|
||||
|
||||
# Setup user for service
|
||||
useradd -M RadioNode
|
||||
usermod -s -L RadioNode
|
||||
|
||||
# Create the .env file from the example
|
||||
cp $SCRIPT_DIR/.env.example $SCRIPT_DIR/.env
|
||||
|
||||
# Change the ownership of the directory to the service user
|
||||
chown RadioNode -R $SCRIPT_DIR
|
||||
|
||||
# Check for updates
|
||||
apt-get update
|
||||
|
||||
# Install Node Repo
|
||||
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
|
||||
|
||||
# Ensure pulse audio is running
|
||||
pulseaudio
|
||||
|
||||
# Install the node packages from the project
|
||||
npm i
|
||||
|
||||
# Install the python packages needed for the bot
|
||||
pip install -r
|
||||
|
||||
# Setup bot service
|
||||
echo "[Unit]
|
||||
Description=Radio Node Service
|
||||
After=network.target
|
||||
|
||||
[Service]
|
||||
WorkingDirectory="$SCRIPT_DIR"
|
||||
ExecStart=/usr/bin/node .
|
||||
Restart=always
|
||||
RestartDelay=10
|
||||
User=RadioNode
|
||||
Environment=DEBUG='client:*'
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target" >> /etc/systemd/system/RadioNode.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
|
||||
User=RadioNode
|
||||
|
||||
[Install]
|
||||
WantedBy=multi-user.target" >> /etc/systemd/system/RadioNodeUpdater.service
|
||||
|
||||
# Enable the service
|
||||
systemctl enable RadioNode.service
|
||||
|
||||
# Start the service
|
||||
systemctl start RadioNode.service
|
||||
32
Client/update.sh
Normal file
32
Client/update.sh
Normal file
@@ -0,0 +1,32 @@
|
||||
#!/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
|
||||
|
||||
# Update the git Repo
|
||||
installUser=$(cat ./config/installerName)
|
||||
sudo su -l $installUser -c 'git fetch -a -p'
|
||||
sudo su -l $installUser -c 'git pull'
|
||||
|
||||
# Install any new libraries
|
||||
npm i
|
||||
|
||||
# Start the service
|
||||
systemctl start RadioNode
|
||||
|
||||
# Update complete message
|
||||
echo "<!--- UPDATE COMPLETE! ---!>"
|
||||
36
Client/utilities/configHandler.js
Normal file
36
Client/utilities/configHandler.js
Normal file
@@ -0,0 +1,36 @@
|
||||
// Debug
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||
const log = new DebugBuilder("client", "configController");
|
||||
// Modules
|
||||
const { readFileSync } = require('fs');
|
||||
const path = require("path");
|
||||
require('dotenv').config();
|
||||
|
||||
const GuildID = process.env.GUILD_ID;
|
||||
const ApplicationID = process.env.APPLICATION_ID;
|
||||
const DeviceID = parseInt(process.env.AUDIO_DEVICE_ID);
|
||||
const DeviceName = process.env.AUDIO_DEVICE_NAME;
|
||||
|
||||
function getGuildID() {
|
||||
log.DEBUG("Guild ID: ", GuildID);
|
||||
return GuildID;
|
||||
}
|
||||
exports.getGuildID = getGuildID;
|
||||
|
||||
function getApplicationID() {
|
||||
log.DEBUG("Application ID: ", ApplicationID);
|
||||
return ApplicationID;
|
||||
}
|
||||
exports.getApplicationID = getApplicationID;
|
||||
|
||||
function getDeviceID(){
|
||||
log.DEBUG("Device ID: ", DeviceID);
|
||||
return DeviceID;
|
||||
}
|
||||
exports.getDeviceID = getDeviceID;
|
||||
|
||||
function getDeviceName(){
|
||||
log.DEBUG("Device Name: ", DeviceName);
|
||||
return DeviceName;
|
||||
}
|
||||
exports.getDeviceName = getDeviceID;
|
||||
@@ -1,5 +1,27 @@
|
||||
// Debug
|
||||
const debug = require('debug');
|
||||
// Read .env file to process.env
|
||||
require('dotenv').config();
|
||||
// Modules
|
||||
const { writeFile } = require('fs');
|
||||
|
||||
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
|
||||
@@ -9,9 +31,38 @@ const debug = require('debug');
|
||||
*/
|
||||
exports.DebugBuilder = class DebugBuilder {
|
||||
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`);
|
||||
this.INFO = (...messageParts) => {
|
||||
const _info = debug(`${appName}:${fileName}:INFO`);
|
||||
_info(messageParts);
|
||||
writeToLog(`${appName}:${fileName}:INFO\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
|
||||
}
|
||||
|
||||
this.DEBUG = (...messageParts) => {
|
||||
const _debug = debug(`${appName}:${fileName}:DEBUG`);
|
||||
_debug(messageParts);
|
||||
writeToLog(`${appName}:${fileName}:DEBUG\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
|
||||
}
|
||||
|
||||
this.VERBOSE = (...messageParts) => {
|
||||
const _verbose = debug(`${appName}:${fileName}:VERBOSE`);
|
||||
_verbose(messageParts);
|
||||
writeToLog(`${appName}:${fileName}:VERBOSE\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
|
||||
}
|
||||
|
||||
this.WARN = (...messageParts) => {
|
||||
const _warn = debug(`${appName}:${fileName}:WARNING`);
|
||||
_warn(messageParts);
|
||||
writeToLog(`${appName}:${fileName}:WARNING\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
|
||||
}
|
||||
|
||||
this.ERROR = (...messageParts) => {
|
||||
const _error = debug(`${appName}:${fileName}:ERROR`);
|
||||
_error(messageParts);
|
||||
writeToLog(`${appName}:${fileName}:ERROR\t-\t${messageParts.map((messagePart, index, array) => {return JSON.stringify(messagePart)})}`, appName);
|
||||
if (process.env.EXIT_ON_ERROR && process.env.EXIT_ON_ERROR > 0) {
|
||||
writeToLog("!--- EXITING ---!", appName);
|
||||
setTimeout(process.exit, process.env.EXIT_ON_ERROR_DELAY ?? 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -2,20 +2,20 @@
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||
const log = new DebugBuilder("client", "httpRequests");
|
||||
// Config
|
||||
const config = require("../config/clientConfig");
|
||||
require('dotenv').config();
|
||||
// Modules
|
||||
const http = require("http");
|
||||
|
||||
exports.requestOptions = class requestOptions {
|
||||
constructor(path, method, hostname = undefined, headers = undefined, port = undefined) {
|
||||
if (method === "POST"){
|
||||
this.hostname = hostname ?? config.serverConfig.hostname
|
||||
this.path = path
|
||||
this.port = port ?? config.serverConfig.port
|
||||
this.method = method
|
||||
this.hostname = hostname ?? process.env.SERVER_HOSTNAME ?? process.env.SERVER_IP;
|
||||
this.path = path;
|
||||
this.port = port ?? process.env.SERVER_PORT;
|
||||
this.method = method;
|
||||
this.headers = headers ?? {
|
||||
'Content-Type': 'application/json',
|
||||
}
|
||||
};
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -31,12 +31,22 @@ exports.sendHttpRequest = function sendHttpRequest(requestOptions, data, callbac
|
||||
// Create the request
|
||||
const req = http.request(requestOptions, res => {
|
||||
res.on('data', (data) => {
|
||||
if (res.statusCode >= 200 && res.statusCode <= 299) {
|
||||
const responseObject = {
|
||||
"statusCode": res.statusCode,
|
||||
"body": JSON.parse(data)
|
||||
};
|
||||
log.DEBUG("Response Object: ", responseObject);
|
||||
callback(responseObject);
|
||||
}
|
||||
if (res.statusCode >= 300) {
|
||||
const responseObject = {
|
||||
"statusCode": res.statusCode,
|
||||
"body": data
|
||||
};
|
||||
log.DEBUG("Response Object: ", responseObject);
|
||||
callback(responseObject);
|
||||
}
|
||||
})
|
||||
}).on('error', err => {
|
||||
log.ERROR('Error: ', err.message)
|
||||
@@ -47,3 +57,17 @@ exports.sendHttpRequest = function sendHttpRequest(requestOptions, data, callbac
|
||||
req.write(data)
|
||||
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;
|
||||
}
|
||||
|
||||
}
|
||||
@@ -134,7 +134,7 @@ class audioConfig {
|
||||
"instance_name": "audio0",
|
||||
"device_name": deviceName,
|
||||
"udp_port": port,
|
||||
"audio_gain": 1.0,
|
||||
"audio_gain": 2.0,
|
||||
"number_channels": 1
|
||||
}];
|
||||
}
|
||||
|
||||
@@ -6,12 +6,12 @@ const replace = require('replace-in-file');
|
||||
|
||||
class Options {
|
||||
constructor(key, updatedValue) {
|
||||
this.files = "./config/clientConfig.js";
|
||||
this.files = "./.env";
|
||||
// A regex of the line containing the key in the config file
|
||||
this.from = new RegExp(`"${key}": (.+),`, "g");
|
||||
this.from = new RegExp(`${key}="(.+)",`, "g");
|
||||
// Check to see if the value is a string and needs to be wrapped in double quotes
|
||||
if (typeof updatedValue === "string") this.to = `"${key}": "${updatedValue}",`;
|
||||
else this.to = `"${key}": ${updatedValue},`;
|
||||
if (Array(["string", "number"]).includes(typeof updatedValue)) this.to = `${key}="${updatedValue}",`;
|
||||
else this.to = `${key}=${updatedValue},`;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -20,10 +20,20 @@ class Options {
|
||||
* @param updatedId The updated ID assigned to the bot
|
||||
*/
|
||||
exports.updateId = (updatedId) => {
|
||||
const options = new Options("id", updatedId);
|
||||
this.updateConfig('id', updatedId);
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @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) => {
|
||||
// Do Something
|
||||
log.DEBUG("Updated config file: ", updatedFiles);
|
||||
})
|
||||
}
|
||||
|
||||
|
||||
@@ -48,12 +48,14 @@ function convertFrequencyToHertz(frequency){
|
||||
if (Number.isInteger(frequency)) {
|
||||
log.DEBUG(`${frequency} is an integer.`);
|
||||
// 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 {
|
||||
log.DEBUG(`${frequency} is a float value.`);
|
||||
// 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 {
|
||||
log.DEBUG(`${frequency} is not a number`);
|
||||
@@ -68,8 +70,9 @@ function convertFrequencyToHertz(frequency){
|
||||
* @returns {any} The object containing the different systems the bot is near
|
||||
*/
|
||||
exports.getPresets = function getPresets() {
|
||||
log.DEBUG(`Getting presets from directory: '${__dirname}'`);
|
||||
return JSON.parse(fs.readFileSync( "./config/radioPresets.json"));
|
||||
const presetDir = path.resolve("./config/radioPresets.json");
|
||||
log.DEBUG(`Getting presets from directory: '${presetDir}'`);
|
||||
return JSON.parse(fs.readFileSync(presetDir));
|
||||
}
|
||||
|
||||
/**
|
||||
@@ -115,5 +118,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);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
|
||||
264
Client/utilities/utilities.js
Normal file
264
Client/utilities/utilities.js
Normal file
@@ -0,0 +1,264 @@
|
||||
// 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._online An integer representation of the online status of the bot, ie 0=off, 1=on
|
||||
* @param {*} param0._nearbySystems An object array of nearby systems
|
||||
*/
|
||||
constructor({ _id = null, _name = null, _ip = null, _port = null, _location = null, _nearbySystems = null, _online = null }) {
|
||||
this.id = _id;
|
||||
this.name = _name;
|
||||
this.ip = _ip;
|
||||
this.port = _port;
|
||||
this.location = _location;
|
||||
this.nearbySystems = _nearbySystems;
|
||||
this.online = _online;
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @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;
|
||||
}
|
||||
@@ -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);
|
||||
}
|
||||
};
|
||||
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`)
|
||||
.addFields(
|
||||
{ name: 'General Commands', value: `${generalCommandText}` },
|
||||
{ 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());
|
||||
}
|
||||
}
|
||||
};
|
||||
120
Server/commands/join.js
Normal file
120
Server/commands/join.js
Normal file
@@ -0,0 +1,120 @@
|
||||
// Modules
|
||||
const { customSlashCommandBuilder } = require('../utilities/customSlashCommandBuilder');
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const { getMembersInRole, getAllClientIds } = require("../utilities/utils");
|
||||
const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
|
||||
const { getOnlineNodes, updateNodeInfo, addNodeConnection, getConnectionByNodeId } = require("../utilities/mysqlHandler");
|
||||
|
||||
// Global Vars
|
||||
const log = new DebugBuilder("server", "join");
|
||||
|
||||
/**
|
||||
* * This wrapper will check if there is an available node with the requested preset and if so checks for an available client ID to join with
|
||||
*
|
||||
* @param {*} presetName The preset name to listen to on the client
|
||||
* @param {*} channelId The channel ID to join the bot to
|
||||
* @param {*} clientIdsUsed EITHER A collection of clients that are currently connected OR a single discord client ID (NOT dev portal ID) that should be used to join the server with
|
||||
* @returns
|
||||
*/
|
||||
async function joinServerWrapper(presetName, channelId, clientIdsUsed) {
|
||||
// Get nodes online
|
||||
var onlineNodes = await new Promise((recordResolve, recordReject) => {
|
||||
getOnlineNodes((nodeRows) => {
|
||||
recordResolve(nodeRows);
|
||||
});
|
||||
});
|
||||
|
||||
// Check which nodes have the selected preset
|
||||
onlineNodes = onlineNodes.filter(node => node.nearbySystems.includes(presetName));
|
||||
log.DEBUG("Filtered Online Nodes: ", onlineNodes);
|
||||
|
||||
// Check if any nodes with this preset are available
|
||||
var nodesCurrentlyAvailable = [];
|
||||
for (const node of onlineNodes) {
|
||||
const currentConnection = await getConnectionByNodeId(node.id);
|
||||
log.DEBUG("Checking to see if there is a connection for Node: ", node, currentConnection);
|
||||
if(!currentConnection) nodesCurrentlyAvailable.push(node);
|
||||
}
|
||||
log.DEBUG("Nodes Currently Available: ", nodesCurrentlyAvailable);
|
||||
|
||||
// If not, let the user know
|
||||
if (!nodesCurrentlyAvailable.length > 0) return Error("All nodes with this channel are unavailable, consider swapping one of the currently joined bots.");
|
||||
|
||||
// If so, join with the first node
|
||||
var availableClientIds = await getAllClientIds();
|
||||
log.DEBUG("All clients: ", Object.keys(availableClientIds));
|
||||
|
||||
var selectedClientId;
|
||||
if (typeof clientIdsUsed === 'string') {
|
||||
for (const availableClientId of availableClientIds) {
|
||||
if (availableClientId.discordId != clientIdsUsed ) selectedClientId = availableClientId;
|
||||
}
|
||||
}
|
||||
else {
|
||||
log.DEBUG("Client IDs Used: ", clientIdsUsed.keys());
|
||||
for (const usedClientId of clientIdsUsed.keys()) {
|
||||
log.DEBUG("Used Client ID: ", usedClientId);
|
||||
availableClientIds = availableClientIds.filter(cid => cid.discordId != usedClientId);
|
||||
}
|
||||
|
||||
log.DEBUG("Available Client IDs: ", availableClientIds);
|
||||
|
||||
if (!Object.keys(availableClientIds).length > 0) return log.ERROR("All client ID have been used, consider swapping one of the curretly joined bots or adding more Client IDs to the pool.")
|
||||
selectedClientId = availableClientIds[0];
|
||||
}
|
||||
|
||||
const selectedNode = nodesCurrentlyAvailable[0];
|
||||
|
||||
const reqOptions = new requestOptions("/bot/join", "POST", selectedNode.ip, selectedNode.port);
|
||||
const postObject = {
|
||||
"channelId": channelId,
|
||||
"clientId": selectedClientId.clientId,
|
||||
"presetName": presetName
|
||||
};
|
||||
log.INFO("Post Object: ", postObject);
|
||||
sendHttpRequest(reqOptions, JSON.stringify(postObject), async (responseObj) => {
|
||||
log.VERBOSE("Response Object from node ", selectedNode, responseObj);
|
||||
if (!responseObj || !responseObj.statusCode == 200) return false;
|
||||
// Node has connected to discord
|
||||
// Updating node Object in DB
|
||||
const updatedNode = await updateNodeInfo(selectedNode);
|
||||
log.DEBUG("Updated Node: ", updatedNode);
|
||||
|
||||
// Adding a new node connection
|
||||
const nodeConnection = await addNodeConnection(selectedNode, selectedClientId);
|
||||
log.DEBUG("Node Connection: ", nodeConnection);
|
||||
});
|
||||
}
|
||||
exports.joinServerWrapper = joinServerWrapper;
|
||||
|
||||
module.exports = {
|
||||
data: new customSlashCommandBuilder()
|
||||
.setName('join')
|
||||
.setDescription('Join the channel you are in with the preset you choose')
|
||||
.addAllSystemPresetOptions(),
|
||||
example: "join",
|
||||
isPrivileged: false,
|
||||
requiresTokens: false,
|
||||
defaultTokenUsage: 0,
|
||||
deferInitialReply: true,
|
||||
async execute(interaction) {
|
||||
try{
|
||||
const guildId = interaction.guild.id;
|
||||
const presetName = interaction.options.getString('preset');
|
||||
if (!interaction.member.voice.channel.id) return interaction.editReply(`You need to be in a voice channel, ${interaction.user}`)
|
||||
const channelId = interaction.member.voice.channel.id;
|
||||
log.DEBUG(`Join requested by: ${interaction.user.username}, to: '${presetName}', in channel: ${channelId} / ${guildId}`);
|
||||
|
||||
const onlineBots = await getMembersInRole(interaction);
|
||||
|
||||
log.DEBUG("Online Bots: ", onlineBots);
|
||||
|
||||
await joinServerWrapper(presetName, channelId, onlineBots.online);
|
||||
await interaction.editReply('**Pong.**');
|
||||
//await interaction.channel.send('**Pong.**'); // This will send a message to the channel of the interaction outside of the initial reply
|
||||
}catch(err){
|
||||
log.ERROR(err)
|
||||
//await interaction.reply(err.toString());
|
||||
}
|
||||
}
|
||||
}
|
||||
77
Server/commands/leave.js
Normal file
77
Server/commands/leave.js
Normal file
@@ -0,0 +1,77 @@
|
||||
// Modules
|
||||
const { customSlashCommandBuilder } = require('../utilities/customSlashCommandBuilder');
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const { getAllClientIds, getKeyByArrayValue } = require("../utilities/utils");
|
||||
const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
|
||||
const { checkNodeConnectionByClientId, removeNodeConnectionByNodeId, updateNodeInfo, getConnectedNodes, getAllConnections } = require('../utilities/mysqlHandler');
|
||||
|
||||
// Global Vars
|
||||
const log = new DebugBuilder("server", "leave");
|
||||
const logAC = new DebugBuilder("server", "leave_autocorrect");
|
||||
|
||||
async function leaveServerWrapper(clientIdObject) {
|
||||
if (!clientIdObject.clientId || !clientIdObject.name) return log.ERROR("Tried to leave server without client ID and/or Name");
|
||||
|
||||
const node = await checkNodeConnectionByClientId(clientIdObject);
|
||||
|
||||
reqOptions = new requestOptions("/bot/leave", "POST", node.ip, node.port);
|
||||
|
||||
const responseObj = await new Promise((recordResolve, recordReject) => {
|
||||
sendHttpRequest(reqOptions, JSON.stringify({}), async (responseObj) => {
|
||||
recordResolve(responseObj);
|
||||
});
|
||||
});
|
||||
|
||||
log.VERBOSE("Response Object from node ", node, responseObj);
|
||||
if (!responseObj || !responseObj.statusCode == 202) return false;
|
||||
// Node has disconnected from discor
|
||||
// Removing the node connection from the DB
|
||||
const removedConnection = removeNodeConnectionByNodeId(node.id);
|
||||
log.DEBUG("Removed Node Connection: ", removedConnection);
|
||||
|
||||
return;
|
||||
}
|
||||
exports.leaveServerWrapper = leaveServerWrapper;
|
||||
|
||||
module.exports = {
|
||||
data: new customSlashCommandBuilder()
|
||||
.setName('leave')
|
||||
.setDescription('Disconnect a bot from the server')
|
||||
.addStringOption(option =>
|
||||
option.setName("bot")
|
||||
.setDescription("The bot to disconnect from the server")
|
||||
.setAutocomplete(true)),
|
||||
example: "leave",
|
||||
isPrivileged: false,
|
||||
requiresTokens: false,
|
||||
defaultTokenUsage: 0,
|
||||
deferInitialReply: true,
|
||||
async autocomplete(interaction) {
|
||||
const focusedValue = interaction.options.getFocused();
|
||||
const connections = await getAllConnections();
|
||||
const filtered = connections.filter(conn => String(conn.clientObject.name).startsWith(focusedValue)).map(conn => conn.clientObject.name);
|
||||
logAC.DEBUG("Focused Value: ", focusedValue, connections, filtered);
|
||||
await interaction.respond(
|
||||
filtered.map(option => ({ name: option, value: option })),
|
||||
);
|
||||
},
|
||||
async execute(interaction) {
|
||||
try{
|
||||
const guildId = interaction.guild.id;
|
||||
const botName = interaction.options.getString('bot');
|
||||
log.DEBUG("Bot Name: ", botName)
|
||||
const clinetIds = await getAllClientIds();
|
||||
log.DEBUG("Client names: ", clinetIds);
|
||||
const clientDiscordId = getKeyByArrayValue(clinetIds, {'name': botName});
|
||||
log.DEBUG("Selected bot: ", clinetIds[clientDiscordId]);
|
||||
// Need to create a table in DB to keep track of what bots have what IDs or an endpoint on the clients to return what ID they are running with
|
||||
await leaveServerWrapper(clinetIds[clientDiscordId]);
|
||||
|
||||
await interaction.editReply(`**${clinetIds[clientDiscordId].name}** has been disconnected`); // This will reply to the initial interaction
|
||||
//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());
|
||||
}
|
||||
}
|
||||
}
|
||||
32
Server/commands/ping.js
Normal file
32
Server/commands/ping.js
Normal file
@@ -0,0 +1,32 @@
|
||||
const { SlashCommandBuilder } = require('discord.js');
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "ping");
|
||||
|
||||
module.exports = {
|
||||
data: new SlashCommandBuilder()
|
||||
.setName('ping')
|
||||
.setDescription('Replies with your input!'),
|
||||
/*
|
||||
.addStringOption(option =>
|
||||
option.setName('input')
|
||||
.setDescription('The input to echo back')
|
||||
.setRequired(false)
|
||||
.addChoices()),
|
||||
*/
|
||||
example: "ping",
|
||||
isPrivileged: false,
|
||||
requiresTokens: false,
|
||||
defaultTokenUsage: 0,
|
||||
deferInitialReply: false,
|
||||
/*async autocomplete(interaction) {
|
||||
const focusedValue = interaction.options.getFocused();
|
||||
},*/
|
||||
async execute(interaction) {
|
||||
try{
|
||||
await interaction.channel.send('**Pong.**'); // TODO - Add insults as the response to this command
|
||||
}catch(err){
|
||||
log.ERROR(err)
|
||||
//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\' 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/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,5 +1,5 @@
|
||||
// Config
|
||||
const discordConfig = require("../config/discordConfig");
|
||||
require('dotenv').config();
|
||||
// Debug
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||
const log = new DebugBuilder("server", "adminController");
|
||||
@@ -20,7 +20,7 @@ async function getPresetsOfOnlineNodes(callback) {
|
||||
systems[onlineNode.id] = utils.BufferToJson(onlineNode.nearbySystems);
|
||||
});
|
||||
|
||||
callback(systems);
|
||||
return callback(systems);
|
||||
});
|
||||
}
|
||||
|
||||
@@ -28,10 +28,10 @@ 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,
|
||||
"channelID": process.env.DEFAULT_VOICE_CHANNEL_ID,
|
||||
"presetName": preset
|
||||
}), (responseObject) => {
|
||||
callback(responseObject)
|
||||
return callback(responseObject)
|
||||
});
|
||||
})
|
||||
}
|
||||
@@ -46,7 +46,7 @@ async function getNodeBotStatus(nodeId, callback) {
|
||||
else {
|
||||
// Bot is free
|
||||
}
|
||||
callback(responseObject);
|
||||
return callback(responseObject);
|
||||
});
|
||||
});
|
||||
}
|
||||
@@ -58,13 +58,13 @@ async function requestNodeLeaveServer(nodeId, callback) {
|
||||
mysqlHandler.getNodeInfoFromId(nodeId, (nodeObject) =>{
|
||||
reqOptions = new requests.requestOptions("/bot/leave", "POST", nodeObject.ip, nodeObject.port);
|
||||
requests.sendHttpRequest(reqOptions, JSON.stringify({}), (responseObject) => {
|
||||
callback(responseObject);
|
||||
return callback(responseObject);
|
||||
});
|
||||
});
|
||||
}
|
||||
else {
|
||||
// Bot is free
|
||||
callback(false);
|
||||
return callback(false);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
@@ -2,31 +2,43 @@
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder.js");
|
||||
const log = new DebugBuilder("server", "nodesController");
|
||||
// Utilities
|
||||
const mysqlHander = require("../utilities/mysqlHandler");
|
||||
const {getAllNodes, addNewNode, updateNodeInfo, getNodeInfoFromId, getOnlineNodes } = require("../utilities/mysqlHandler");
|
||||
const utils = require("../utilities/utils");
|
||||
const { sendHttpRequest, requestOptions } = require("../utilities/httpRequests.js");
|
||||
const { nodeObject } = require("../utilities/recordHelper.js");
|
||||
const { joinServerWrapper } = require("../commands/join");
|
||||
|
||||
const refreshInterval = process.env.NODE_MONITOR_REFRESH_INTERVAL ?? 1200000;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} req
|
||||
* @param {*} res
|
||||
*/
|
||||
exports.listAllNodes = async (req, res) => {
|
||||
mysqlHander.getAllNodes((allNodes) => {
|
||||
getAllNodes((allNodes) => {
|
||||
res.status(200).json({
|
||||
"nodes_online": allNodes
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
// Add a new node to the
|
||||
// Add a new node to the storage
|
||||
exports.newNode = async (req, res) => {
|
||||
if (!req.body.name) return res.send(400)
|
||||
|
||||
try {
|
||||
// Try to add the new user with defaults if missing options
|
||||
mysqlHander.addNewNode({
|
||||
'name': req.body.name,
|
||||
'ip': req.body.ip ?? null,
|
||||
'port': req.body.port ?? null,
|
||||
'location': req.body.location ?? null,
|
||||
'nearbySystems': req.body.nearbySystems ?? null,
|
||||
'online': req.body.online ?? 0
|
||||
}, (queryResults) => {
|
||||
const newNode = new nodeObject({
|
||||
_name: req.body.name,
|
||||
_ip: req.body.ip ?? null,
|
||||
_port: req.body.port ?? null,
|
||||
_location: req.body.location ?? null,
|
||||
_nearbySystems: req.body.nearbySystems ?? null,
|
||||
_online: req.body.online ?? 0
|
||||
});
|
||||
|
||||
addNewNode(newNode, (queryResults) => {
|
||||
// Send back a success if the user has been added and the ID for the client to keep track of
|
||||
res.status(202).json({"nodeId": queryResults.insertId});
|
||||
})
|
||||
@@ -44,7 +56,7 @@ exports.newNode = async (req, res) => {
|
||||
// Get the known info for the node specified
|
||||
exports.getNodeInfo = async (req, res) => {
|
||||
if (!req.query.id) return res.status(400).json("No id specified");
|
||||
mysqlHander.getNodeInfoFromId(req.query.id, (nodeInfo) => {
|
||||
getNodeInfoFromId(req.query.id, (nodeInfo) => {
|
||||
res.status(200).json(nodeInfo);
|
||||
})
|
||||
}
|
||||
@@ -52,29 +64,92 @@ exports.getNodeInfo = async (req, res) => {
|
||||
// Updates the information received from the client based on ID
|
||||
exports.nodeCheckIn = async (req, res) => {
|
||||
if (!req.body.id) return res.status(400).json("No id specified");
|
||||
mysqlHander.getNodeInfoFromId(req.body.id, (nodeInfo) => {
|
||||
let nodeObject = {};
|
||||
getNodeInfoFromId(req.body.id, (nodeInfo) => {
|
||||
let checkInObject = new nodeObject();
|
||||
// Convert the DB systems buffer to a JSON object to be worked with
|
||||
nodeInfo.nearbySystems = utils.BufferToJson(nodeInfo.nearbySystems)
|
||||
// Convert the online status to a boolean to be worked with
|
||||
nodeInfo.online = nodeInfo.online !== 0;
|
||||
|
||||
if (req.body.name && req.body.name !== nodeInfo.name) nodeObject.name = req.body.name
|
||||
if (req.body.ip && req.body.ip !== nodeInfo.ip) nodeObject.ip = req.body.ip
|
||||
if (req.body.port && req.body.port !== nodeInfo.port) nodeObject.port = req.body.port
|
||||
if (req.body.location && req.body.location !== nodeInfo.location) nodeObject.location = req.body.location
|
||||
if (req.body.nearbySystems && JSON.stringify(req.body.nearbySystems) !== JSON.stringify(nodeInfo.nearbySystems)) nodeObject.nearbySystems = req.body.nearbySystems
|
||||
if (req.body.online && req.body.online !== nodeInfo.online) nodeObject.online = req.body.online
|
||||
if (req.body.name && req.body.name !== nodeInfo.name) checkInObject.name = req.body.name
|
||||
if (req.body.ip && req.body.ip !== nodeInfo.ip) checkInObject.ip = req.body.ip
|
||||
if (req.body.port && req.body.port !== nodeInfo.port) checkInObject.port = req.body.port
|
||||
if (req.body.location && req.body.location !== nodeInfo.location) checkInObject.location = req.body.location
|
||||
if (req.body.nearbySystems && JSON.stringify(req.body.nearbySystems) !== JSON.stringify(nodeInfo.nearbySystems)) checkInObject.nearbySystems = req.body.nearbySystems
|
||||
if (req.body.online && req.body.online !== nodeInfo.online) checkInObject.online = req.body.online
|
||||
|
||||
// If no changes are made tell the client
|
||||
if (Object.keys(nodeObject).length === 0) return res.status(200).json("No keys updated");
|
||||
if (Object.keys(checkInObject).length === 0) return res.status(200).json("No keys updated");
|
||||
|
||||
log.INFO("Updating the following keys for ID: ", req.body.id, nodeObject);
|
||||
log.INFO("Updating the following keys for ID: ", req.body.id, checkInObject);
|
||||
// Adding the ID key to the body so that the client can double-check their ID
|
||||
nodeObject.id = req.body.id;
|
||||
mysqlHander.updateNodeInfo(nodeObject, () => {
|
||||
return res.status(202).json({"updatedKeys": nodeObject});
|
||||
checkInObject.id = req.body.id;
|
||||
updateNodeInfo(checkInObject, () => {
|
||||
return res.status(202).json({"updatedKeys": checkInObject});
|
||||
})
|
||||
})
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
* Request the node to join the specified server/channel and listen to the specified resource
|
||||
*
|
||||
* @param req.body.clientId The client ID to join discord with (NOT dev portal ID, right click user -> Copy ID)
|
||||
* @param req.body.channelId The Channel ID to join in Discord
|
||||
* @param req.body.presetName The preset name to listen to in Discord
|
||||
*/
|
||||
exports.requestNodeJoinServer = async (req, res) => {
|
||||
if (!req.body.clientId || !req.body.channelId || !req.body.presetName) return res.status(400).json("Missing information in request, requires clientId, channelId, presetName");
|
||||
await joinServerWrapper(req.body.presetName, req.body.channelId, req.body.clientId)
|
||||
}
|
||||
|
||||
/**
|
||||
* 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() {
|
||||
}
|
||||
|
||||
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 this.checkInWithOnlineNodes();
|
||||
|
||||
|
||||
while(true){
|
||||
// Wait for the refresh interval, then wait for the posts to return, then wait a quarter of the refresh interval to make sure everything is cleared up
|
||||
await new Promise(resolve => setTimeout(resolve, refreshInterval));
|
||||
await this.checkInWithOnlineNodes();
|
||||
await new Promise(resolve => setTimeout(resolve, refreshInterval / 4));
|
||||
continue;
|
||||
}
|
||||
}
|
||||
|
||||
async checkInWithOnlineNodes(){
|
||||
getOnlineNodes((nodes) => {
|
||||
log.DEBUG("Online Nodes: ", nodes);
|
||||
for (const node of nodes) {
|
||||
const reqOptions = new requestOptions("/client/requestCheckIn", "GET", node.ip, node.port)
|
||||
const request = sendHttpRequest(reqOptions, "", (responseObj) => {
|
||||
if (responseObj) {
|
||||
log.DEBUG("Response from: ", node.name, responseObj);
|
||||
}
|
||||
else {
|
||||
log.DEBUG("No response from node, assuming it's offline");
|
||||
const offlineNode = new nodeObject({ _online: 0, _id: node.id });
|
||||
log.DEBUG("Offline node update object: ", offlineNode);
|
||||
updateNodeInfo(offlineNode, (sqlResponse) => {
|
||||
if (!sqlResponse) log.ERROR("No response from SQL object");
|
||||
|
||||
log.DEBUG("Updated node: ", sqlResponse);
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
return;
|
||||
});
|
||||
}
|
||||
}
|
||||
|
||||
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 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
|
||||
382
Server/libCore.js
Normal file
382
Server/libCore.js
Normal file
@@ -0,0 +1,382 @@
|
||||
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("mysql");
|
||||
|
||||
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 ?? 3;
|
||||
|
||||
/**
|
||||
*
|
||||
* @param {*} sourceURL
|
||||
*/
|
||||
exports.removeSource = function removeSource(sourceURL) {
|
||||
log.INFO("Removing source URL: ", sourceURL);
|
||||
if (!sourceURL in runningPostsToRemove) {runningPostsToRemove[sourceURL] = 1; return;}
|
||||
|
||||
if (runningPostsToRemove[sourceURL] < sourceFailureLimit) {runningPostsToRemove[sourceURL] += 1; 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
|
||||
* @returns {*}
|
||||
*/
|
||||
exports.unsetRemoveSource = function unsetRemoveSource(sourceURL) {
|
||||
log.INFO("Unsetting source URL from deletion (if not already deleted): ", sourceURL);
|
||||
if (!sourceURL in runningPostsToRemove) return;
|
||||
|
||||
if (runningPostsToRemove[sourceURL] > sourceFailureLimit) return delete runningPostsToRemove[sourceURL];
|
||||
}
|
||||
|
||||
/**
|
||||
* 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);
|
||||
});
|
||||
}
|
||||
500
Server/libStorage.js
Normal file
500
Server/libStorage.js
Normal file
@@ -0,0 +1,500 @@
|
||||
// Customizable storage module for any mode of storage
|
||||
// Update the functions here to change the storage medium
|
||||
|
||||
// Import modules
|
||||
const { DebugBuilder } = require("./utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "libStorage");
|
||||
const { RSSSourceRecord, RSSPostRecord } = require("./utilities/recordHelper");
|
||||
|
||||
// Storage Specific Modules
|
||||
// MySQL
|
||||
const mysql = require("mysql");
|
||||
|
||||
const rssFeedsTable = process.env.DB_RSS_FEEDS_TABLE;
|
||||
const rssPostsTable = process.env.DB_RSS_POSTS_TABLE;
|
||||
const accountsTable = process.env.DB_ACCOUNTS_TABLE;
|
||||
const transactionsTable = process.env.DB_TRANSACTIONS_TABLE;
|
||||
const pricingTable = process.env.DB_PRICING_TABLE;
|
||||
|
||||
var Connection = 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
|
||||
});
|
||||
|
||||
// Helper Functions
|
||||
/**
|
||||
* Function to run and handle SQL errors
|
||||
* @param {string} sqlQuery The SQL query string
|
||||
* @param {*} connection The SQL connection to be used to query
|
||||
* @param {function} callback The callback function to be called with an error or the results
|
||||
* @param {number} _retry Set by error retry, increments the number a query has been retried to increase wait time and track a specific query
|
||||
*/
|
||||
function runSQL(sqlQuery, connection, callback = (err, rows) => {
|
||||
log.ERROR(err);
|
||||
throw err;
|
||||
}, _retry = 0) {
|
||||
// Start the MySQL Connection
|
||||
if (!connection) connection = Connection;
|
||||
connection.query(sqlQuery, (err, rows) => {
|
||||
if (err) {
|
||||
if (err.code === "EHOSTUNREACH") {
|
||||
// DB Connection is unavailable
|
||||
let retryTimeout;
|
||||
switch(_retry){
|
||||
case 0:
|
||||
retryTimeout = 30000;
|
||||
break;
|
||||
case retry < 15:
|
||||
retryTimeout = 30000 + retry * 15000;
|
||||
break;
|
||||
default:
|
||||
log.ERROR("Retried Database 15 times over, please check connection status and restart the app", sqlQuery, err);
|
||||
return callback(err, undefined);
|
||||
}
|
||||
log.WARN(`Database connection is unavailable, waiting ${ retryTimeout / 1000 } seconds...`);
|
||||
_retry += 1
|
||||
// Wait for the retry timeout before trying the query again
|
||||
setTimeout(runSQL(sqlQuery, connection, callback, _retry));
|
||||
}
|
||||
else return callback(err, undefined);
|
||||
}
|
||||
log.VERBOSE(`SQL result for query '${sqlQuery}':`, rows);
|
||||
return callback(undefined, rows);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Return a formatted date time string from now for MySQL
|
||||
*
|
||||
* @returns Date string for now formatted for MySQL
|
||||
*/
|
||||
function returnMysqlTime(){
|
||||
return new Date().toISOString().slice(0, 19).replace('T', ' ');
|
||||
}
|
||||
|
||||
class Storage {
|
||||
constructor(_dbTable, _connection) {
|
||||
this.dbTable = _dbTable;
|
||||
this.connection = _connection;
|
||||
this.validKeys = [];
|
||||
|
||||
var sqlQuery = `SHOW COLUMNS FROM ${this.dbTable};`;
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return log.ERROR("Error getting column names: ", err);
|
||||
if (rows){
|
||||
for (const validKey of rows){
|
||||
this.validKeys.push(validKey.Field);
|
||||
}
|
||||
log.VERBOSE(`Database rows for '${this.dbTable}': `, rows);
|
||||
log.DEBUG(`Keys for '${this.dbTable}': `, this.validKeys);
|
||||
}
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to delete an entry using the storage method configured
|
||||
* @param {} entryID The ID of the entry to be deleted
|
||||
* @param {function} callback The callback function to be called with the record when deleted
|
||||
*/
|
||||
destroy(entryID, callback) {
|
||||
if (!entryID) return callback(Error("No entry ID given"), undefined);
|
||||
|
||||
this.getRecordBy('id', entryID, (err, entryRecord) => {
|
||||
this.removeEntry(entryRecord.id, (err, results) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, results);
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Get a record by a specified key
|
||||
* @param {*} key The key to search for
|
||||
* @param {*} keyValue The value of the key to search for
|
||||
* @param {*} callback The callback function
|
||||
*/
|
||||
getRecordBy(key, keyValue, callback) {
|
||||
if (!this.validKeys.includes(key)) return callback(new Error("Given key not valid", key), undefined);
|
||||
|
||||
const sqlQuery = `SELECT * FROM ${this.dbTable} WHERE ${key} = "${keyValue}"`;
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
if (rows[0]?.[key]) return callback(undefined, rows[0]);
|
||||
else return callback(undefined, false);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all records stored
|
||||
* @param {function} callback
|
||||
*/
|
||||
getAllRecords(callback) {
|
||||
log.INFO("Getting all records from: ", this.dbTable);
|
||||
const sqlQuery = `SELECT * FROM ${this.dbTable}`
|
||||
|
||||
let records = [];
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
for (const row of rows) {
|
||||
if (this.dbTable == rssFeedsTable){
|
||||
records.push(new RSSSourceRecord(row.id, row.title, row.link, row.category, row.guild_id, row.channel_id));
|
||||
}
|
||||
else {
|
||||
records.push(rows);
|
||||
}
|
||||
}
|
||||
log.VERBOSE("All records:", records);
|
||||
return callback(undefined, records);
|
||||
});
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all unique rows in the given key
|
||||
* @param {*} key
|
||||
* @param {*} callback
|
||||
*/
|
||||
getUniqueByKey(key, callback){
|
||||
log.INFO("Getting all unique values in column: ", key);
|
||||
const sqlQuery = `SELECT DISTINCT ${key} FROM ${this.dbTable}`
|
||||
|
||||
let records = [];
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
for (const row of rows) {
|
||||
if (this.dbTable == rssFeedsTable){
|
||||
records.push(new RSSSourceRecord(row.id, row.title, row.link, row.category, row.guild_id, row.channel_id));
|
||||
}
|
||||
else {
|
||||
records.push(rows);
|
||||
}
|
||||
}
|
||||
log.VERBOSE("All records:", records);
|
||||
return callback(undefined, records);
|
||||
});
|
||||
}
|
||||
|
||||
closeConnection() {
|
||||
try {
|
||||
this.connection.end();
|
||||
}
|
||||
catch (err) {
|
||||
log.ERROR("Error closing connection :", this.connection, err);
|
||||
throw err;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.UserStorage = class UserStorage extends Storage {
|
||||
constructor(connection = undefined) {
|
||||
super(accountsTable, connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save a new account to the database
|
||||
* @param {*} _discordAccountId The Discord ID the the user
|
||||
* @param {*} callback The callback to be sent
|
||||
* @callback Error|Array|*
|
||||
*/
|
||||
saveAccount(_discordAccountId, callback){
|
||||
const sqlQuery = `INSERT INTO ${this.dbTable} (discord_account_id, balance) VALUES ("${_discordAccountId}", ${0});`;
|
||||
|
||||
log.DEBUG(`Adding new entry with SQL query: '${sqlQuery}'`)
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
if (rows?.affectedRows > 0) return callback(undefined, rows);
|
||||
return callback(undefined, undefined);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Check or return the balance of a given account ID
|
||||
*
|
||||
* @param {*} _tokensToBeUsed The amount of tokens to be used, set to 0 to return the balance
|
||||
* @param {*} _account_id The account ID to check or return the balance of
|
||||
* @param {*} callback
|
||||
*/
|
||||
checkBalance(_tokensToBeUsed, _account_id, callback) {
|
||||
if (!_account_id) return callback(new Error("Account not specified when checking account balance"), undefined);
|
||||
log.DEBUG("Tokens to verify against balance", _tokensToBeUsed, _account_id);
|
||||
if (!_tokensToBeUsed && !_tokensToBeUsed >= 0) return callback(new Error("Specified tokens are invalid when checking account balance"), undefined);
|
||||
this.getRecordBy('account_id', _account_id, (err, record) => {
|
||||
if (err) return callback(err, undefined);
|
||||
|
||||
// Check to see if the account has a balance greater than what was given
|
||||
if(_tokensToBeUsed > 0){
|
||||
if (record?.balance && record.balance > _tokensToBeUsed) return callback(undefined, true);
|
||||
else return callback(undefined, false);
|
||||
}
|
||||
|
||||
return callback(undefined, record.balance)
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Update a user's account Balance
|
||||
*
|
||||
* @param {string} _updateType The type of update to make to the account [ withdraw | deposit ]
|
||||
* @param {number} _updateAmount The amount to update the account
|
||||
* @param {number} _account_id The ID of the discord account to update
|
||||
* @param {function} callback The callback function to call with the results
|
||||
* @returns Result from the SQL query or false
|
||||
*/
|
||||
updateBalance(_updateType, _updateAmount, _discord_account_id, callback){
|
||||
var sqlQuery = "";
|
||||
switch(_updateType){
|
||||
case "withdraw":
|
||||
// Code here to withdraw funds
|
||||
sqlQuery = `UPDATE ${this.dbTable} SET balance=balance-${_updateAmount} WHERE discord_account_id = ${_discord_account_id};`;
|
||||
break;
|
||||
case "deposit":
|
||||
// Code here to withdraw funds
|
||||
sqlQuery = `UPDATE ${this.dbTable} SET balance=balance+${_updateAmount} WHERE discord_account_id = ${_discord_account_id};`;
|
||||
break;
|
||||
default:
|
||||
log.ERROR('Update type not valid: ', _updateType);
|
||||
return callback(new Error("Update type not valid"));
|
||||
}
|
||||
|
||||
if(!sqlQuery) return callback(new Error("SQL Query empty"), undefined);
|
||||
|
||||
log.DEBUG("Updating Balance with SQL Query: ", sqlQuery);
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
if (!rows?.affectedRows > 0) return callback(new Error("Error updating Balance", rows), undefined);
|
||||
return callback(undefined, rows);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
exports.TransactionStorage = class TransactionStorage extends Storage {
|
||||
constructor(connection = undefined) {
|
||||
super(transactionsTable, connection);
|
||||
}
|
||||
|
||||
createTransaction(transaction, callback){
|
||||
var sqlQuery = `INSERT INTO ${this.dbTable} (transaction_id, account_id, discord_tokens_used, provider_tokens_used, provider_id, order_date) VALUES ("${transaction.transaction_id}", "${transaction.account_id}", "${transaction.discord_tokens_used}", "${transaction.provider_tokens_used}", "${transaction.provider_id}", "${returnMysqlTime()}");`;
|
||||
|
||||
log.DEBUG(`Adding new entry with SQL query: '${sqlQuery}'`)
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
if (rows?.affectedRows > 0) return callback(undefined, rows);
|
||||
return callback(undefined, undefined);
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
exports.FeedStorage = class FeedStorage extends Storage {
|
||||
constructor(connection = undefined) {
|
||||
super(rssFeedsTable, connection);
|
||||
}
|
||||
|
||||
/**
|
||||
* Wrapper to save a new entry using the storage method configured
|
||||
* @param {Array} toBeSaved Entry or Entries to be added
|
||||
* @param {function} callback The callback function to be called with the record when saved
|
||||
*/
|
||||
create(toBeSaved, callback) {
|
||||
log.DEBUG("To be saved:", toBeSaved);
|
||||
log.DEBUG("to be saved length:", toBeSaved.length);
|
||||
// If the request was for the Feeds Table
|
||||
if (!toBeSaved[0].fields?.title) return callback(Error("No title given"), undefined);
|
||||
let newRecords = []
|
||||
for (var entry of toBeSaved) {
|
||||
entry = entry.fields;
|
||||
log.DEBUG("Entry:", entry);
|
||||
this.returnRecord(undefined, entry.title, entry.link, entry.category, entry.guild_id, entry.channel_id, (err, record) => {
|
||||
if (err) return callback(err, undefined);
|
||||
newRecords.push(record);
|
||||
if (toBeSaved.length === 1) {
|
||||
log.DEBUG("One record to callback with:", record);
|
||||
return callback(undefined, record);
|
||||
}
|
||||
}, false) // Do not update the if it exists
|
||||
}
|
||||
if (!toBeSaved.length === 1) {
|
||||
return callback(undefined, newRecords);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Check to see if an entry exists in the storage method configured
|
||||
* @param {*} title The title of the entry to check if it exists
|
||||
* @returns {true|false|*}
|
||||
*/
|
||||
checkForTitle(title, callback) {
|
||||
if (!title) return callback(new Error("No title given when checking for title"), undefined)
|
||||
|
||||
this.getRecordBy("title", title, callback);
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the given entry to the storage medium
|
||||
* @param {Object} entryObject The entry object to be saved
|
||||
* @param {function} callback The callback to be called with either an error or undefined if successful
|
||||
*/
|
||||
saveEntry(entryObject, callback) {
|
||||
log.DEBUG("Saving entry:", entryObject);
|
||||
if (!entryObject?.title || !entryObject?.link || !entryObject?.category) {
|
||||
return callback(new Error("Entry object malformed, check the object before saving it"), undefined)
|
||||
}
|
||||
|
||||
const sqlQuery = `INSERT INTO ${this.dbTable} (title, link, category, guild_id, channel_id) VALUES ("${entryObject.title}", "${entryObject.link}", "${entryObject.category}", "${entryObject.guild_id}", "${entryObject.channel_id}");`;
|
||||
|
||||
log.DEBUG(`Adding new entry with SQL query: '${sqlQuery}'`)
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, rows);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Save the given entry to the storage medium
|
||||
* @param {Object} entryObject The entry object to be saved
|
||||
* @param {function} callback The callback to be called with either an error or undefined if successful
|
||||
*/
|
||||
updateEntry(entryObject, callback) {
|
||||
let queryParams = [];
|
||||
if (!entryObject.title) return callback(new Error("No title given before updating"), undefined);
|
||||
queryParams.push(`title = "${entryObject.title}"`);
|
||||
if (!entryObject.link) return callback(new Error("No link given before updating"), undefined);
|
||||
queryParams.push(`link = "${entryObject.link}"`);
|
||||
if (entryObject.category) queryParams.push(`category = "${entryObject.category}"`);
|
||||
if (entryObject.guild_id) queryParams.push(`guild_id = "${entryObject.guild_id}"`);
|
||||
if (entryObject.channel_id) queryParams.push(`channel_id = "${entryObject.channel_id}"`);
|
||||
|
||||
let sqlQuery = `UPDATE ${this.dbTable} SET`;
|
||||
|
||||
let i = 0;
|
||||
for (const param of queryParams) {
|
||||
if (i === queryParams.length-1) {
|
||||
sqlQuery = `${sqlQuery} ${param}`
|
||||
i += 1;
|
||||
}
|
||||
else {
|
||||
sqlQuery = `${sqlQuery} ${param},`
|
||||
i += 1;
|
||||
}
|
||||
}
|
||||
|
||||
sqlQuery = `${sqlQuery} WHERE title = "${entryObject.title}";`
|
||||
|
||||
log.DEBUG(`Updating entry with SQL query: '${sqlQuery}'`)
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, rows);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete the given entry from the storage medium
|
||||
* @param {*} id The title of the entry to be deleted
|
||||
* @param {function} callback The callback to be called with either an error or undefined if successful
|
||||
*/
|
||||
removeEntry(id, callback) {
|
||||
if (!id) {
|
||||
return callback(new Error("No entry id given before deleting"), undefined)
|
||||
}
|
||||
|
||||
const sqlQuery = `DELETE FROM ${this.dbTable} WHERE id = "${id}";`;
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, rows[0]);
|
||||
})
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a record class for the given information, if there's no ID, it will create it
|
||||
* @param {*} _id The ID / line number of the record in the storage medium (OPT)
|
||||
* @param {*} _title The title of the record
|
||||
* @param {*} _link The link to the RSS feed
|
||||
* @param {*} _category The category of the record
|
||||
* @param {*} callback Callback function to return an error or the record
|
||||
*/
|
||||
returnRecord(_id, _title, _link, _category, _guild_id, _channel_id, callback, updateEnabled = true) {
|
||||
log.DEBUG(`Return record for these values: ID: '${_id}', Title: '${_title}', Category: '${_category}', Link: '${_link}', Guild: '${_guild_id}', Channel:'${_channel_id}', Update Enabled: `, updateEnabled)
|
||||
if (!_link && !_title && !_guild_id && !_channel_id) return callback(new Error("No link or title given when creating a record"), undefined);
|
||||
let entryObject = {
|
||||
"title": _title,
|
||||
"link": _link,
|
||||
"guild_id": _guild_id,
|
||||
"channel_id": _channel_id
|
||||
}
|
||||
if (_category) entryObject.category = _category;
|
||||
|
||||
if (_id) {
|
||||
entryObject.id = _id;
|
||||
if (!updateEnabled) return callback(undefined, undefined);
|
||||
|
||||
this.updateEntry(entryObject, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
this.getRecordBy('id', entryObject.id, (err, record) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, new RSSSourceRecord(record.id, record.title, record.link, record.category, record.guild_id, record.channel_id));
|
||||
})
|
||||
})
|
||||
}
|
||||
else {
|
||||
this.checkForTitle(_title, (err, titleExists) => {
|
||||
if (!titleExists) {
|
||||
log.DEBUG("Entry doesn't exist, making one now", entryObject);
|
||||
this.saveEntry(entryObject, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
this.getRecordBy("title", entryObject.title, (err, record) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, new RSSSourceRecord(record.id, record.title, record.link, record.category, record.guild_id, record.channel_id));
|
||||
})
|
||||
});
|
||||
}
|
||||
else{
|
||||
if (!updateEnabled) return callback(undefined, undefined);
|
||||
|
||||
this.updateEntry(entryObject, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
this.getRecordBy('title', entryObject.title, (err, record) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, new RSSSourceRecord(record.id, record.title, record.link, record.category, record.guild_id, record.channel_id));
|
||||
})
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
exports.PostStorage = class PostStorage extends Storage {
|
||||
constructor(connection = undefined) {
|
||||
super(rssPostsTable, connection);
|
||||
}
|
||||
|
||||
savePost(_postObject, callback){
|
||||
const tempCreationDate = returnMysqlTime();
|
||||
log.DEBUG("Saving Post Object:", _postObject);
|
||||
if (!_postObject?.postId || !_postObject?.link) {
|
||||
return callback(new Error("Post object malformed, check the object before saving it", _postObject), undefined)
|
||||
}
|
||||
|
||||
if (_postObject.link.length > 250) _postObject.link = _postObject.link.substring(0, 250);
|
||||
|
||||
const sqlQuery = `INSERT INTO ${this.dbTable} (post_guid, post_link, post_sent_date) VALUES ("${_postObject.postId}","${_postObject.link}","${tempCreationDate}");`;
|
||||
|
||||
log.DEBUG(`Adding new post with SQL query: '${sqlQuery}'`)
|
||||
|
||||
runSQL(sqlQuery, this.connection, (err, rows) => {
|
||||
if (err) return callback(err, undefined);
|
||||
return callback(undefined, rows);
|
||||
})
|
||||
}
|
||||
}
|
||||
177
Server/libUtils.js
Normal file
177
Server/libUtils.js
Normal file
@@ -0,0 +1,177 @@
|
||||
const { EmbedBuilder } = require('discord.js');
|
||||
const { DebugBuilder } = require("./utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "libUtils");
|
||||
const { NodeHtmlMarkdown } = require('node-html-markdown');
|
||||
const { parse } = require("node-html-parser");
|
||||
const crypto = require("crypto");
|
||||
require('dotenv').config();
|
||||
|
||||
const imageRegex = /(http(s?):)([/|.|\w|\s|-])*((\.(?:jpg|gif|png|webm))|(\/gallery\/(?:[/|.|\w|\s|-])*))/g;
|
||||
const youtubeVideoRegex = /((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube(-nocookie)?\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)/g
|
||||
|
||||
exports.EmmeliaEmbedBuilder = class PostEmbedBuilder extends EmbedBuilder {
|
||||
constructor() {
|
||||
super()
|
||||
this.setTimestamp();
|
||||
this.setFooter({ text: 'Brought to you by Emmelia.' });
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* sleep - sleep/wait
|
||||
* @constructor
|
||||
*/
|
||||
exports.runAfter = async (toRun, timeout = 10000) => {
|
||||
log.DEBUG(`Running '${toRun}' after ${timeout / 1000} seconds`);
|
||||
setTimeout(toRun, timeout)
|
||||
}
|
||||
|
||||
/**
|
||||
* Normalize a port into a number, string, or false.
|
||||
*
|
||||
* @param {*} val Value to be normalized
|
||||
* @returns Normalized value
|
||||
*/
|
||||
exports.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.
|
||||
*/
|
||||
exports.onError = (error) => {
|
||||
if (error.syscall !== 'listen') {
|
||||
throw error;
|
||||
}
|
||||
|
||||
var port = process.env.HTTP_PORT;
|
||||
var bind = typeof port === 'string'
|
||||
? 'Pipe ' + port
|
||||
: 'Port ' + port;
|
||||
|
||||
// handle specific listen errors with friendly messages
|
||||
switch (error.code) {
|
||||
case 'EACCES':
|
||||
log.ERROR(bind + ' requires elevated privileges');
|
||||
process.exit(1);
|
||||
break;
|
||||
case 'EADDRINUSE':
|
||||
log.ERROR(bind + ' is already in use');
|
||||
process.exit(1);
|
||||
break;
|
||||
default:
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
exports.sendPost = (post, source, channel, callback) => {
|
||||
log.DEBUG("Sending post from source: ", post, source);
|
||||
const postTitle = String(post.title).substring(0, 150);
|
||||
const postLink = post.link;
|
||||
let postContent;
|
||||
|
||||
if (post.content) {
|
||||
// Reset the content parameter with the encoded parameter
|
||||
post.content = parse(post['content:encoded'] ?? post.content);
|
||||
// Get the post content and trim it to length or add a placeholder if necessary
|
||||
var postText = String(post.content.text);
|
||||
if (postText.length >= 3800) postText = `${postText.slice(0, 3800).substring(0, Math.min(String(post.content.text).length, String(post.content.text).lastIndexOf(" ")))} [...](${post.link})`;
|
||||
else if (postText.length === 0) postText = `*This post has no content* [Direct Link](${post.link})`;
|
||||
postContent = postText;
|
||||
}
|
||||
else postContent = `*This post has no content* [Direct Link](${post.link})`;
|
||||
|
||||
// Check for embedded youtube videos and add the first four as links
|
||||
const ytVideos = String(post.content).match(youtubeVideoRegex);
|
||||
if (ytVideos) {
|
||||
for (var ytVideo of ytVideos.slice(0,4)){
|
||||
// If the video is an embed, replace the embed to make it watchable
|
||||
if (ytVideo.includes("embed")) ytVideo = ytVideo.replace("embed/", "watch?v=");
|
||||
postContent += `\nEmbeded Video from Post: [YouTube](${ytVideo})`
|
||||
}
|
||||
}
|
||||
log.DEBUG("Post content: ", postContent);
|
||||
|
||||
const postId = post.postId;
|
||||
if (!post.pubDate) post.pubDate = Date.now();
|
||||
const postPubDate = new Date(post.pubDate).toISOString();
|
||||
|
||||
var postSourceLink = source.title;
|
||||
var postImage = post.image ?? undefined;
|
||||
|
||||
if (!postImage){
|
||||
if (post.content){
|
||||
const linksInPost = post.content.querySelectorAll("a");
|
||||
if (linksInPost) {
|
||||
log.DEBUG("Found links in post:", linksInPost);
|
||||
for (const link of linksInPost) {
|
||||
// Check to see if this link is a youtube video that was already found, if so skip it
|
||||
if (ytVideos?.includes(link)) continue;
|
||||
const images = String(link.getAttribute("href")).match(imageRegex);
|
||||
log.DEBUG("Images found in post:", images);
|
||||
if (images) {
|
||||
postImage = images[0];
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
log.DEBUG("Sending an RSS post to discord", postTitle, postId, postContent)
|
||||
try{
|
||||
const rssMessage = new this.EmmeliaEmbedBuilder()
|
||||
.setColor(0x0099FF)
|
||||
.setTitle(postTitle)
|
||||
.setURL(postLink)
|
||||
.addFields({ name: 'Source', value: postSourceLink, inline: true })
|
||||
.addFields({ name: 'Published', value: postPubDate, inline: true });
|
||||
|
||||
// TODO - If there is more than one image, create a canvas and post the created canvas
|
||||
if (postImage) {
|
||||
log.DEBUG("Image from post:", postImage);
|
||||
rssMessage.setImage(postImage);
|
||||
}
|
||||
|
||||
//Add the main content if it's present
|
||||
postContent = postContent.slice(0, 4090).trim();
|
||||
if (postContent) rssMessage.setDescription( postContent );
|
||||
|
||||
channel.send({ embeds: [rssMessage] });
|
||||
|
||||
//throw new Error("YOU SHALL NOT PASS");
|
||||
|
||||
return callback(undefined, true);
|
||||
}
|
||||
catch (err){
|
||||
log.ERROR("Error sending message: ", postTitle, postId, postContent, postPubDate, err);
|
||||
return callback(err, undefined);
|
||||
}
|
||||
}
|
||||
|
||||
exports.returnHash = (...stringsIncluded) => {
|
||||
return crypto.createHash('sha1').update(`${stringsIncluded.join("-<<??//\\\\??>>-")}`).digest("base64");
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if a key exists in an array of objects
|
||||
* @param {*} key The key to search for
|
||||
* @param {*} array The object to search for the key
|
||||
* @returns {boolean} If the key exists in the object
|
||||
*/
|
||||
exports.checkForKeyInArrayOfObjects = (key, array) => {
|
||||
return array.filter(function (o) {
|
||||
return o.hasOwnProperty(key);
|
||||
}).length > 0;
|
||||
}
|
||||
65
Server/middleware/balanceAuthorization.js
Normal file
65
Server/middleware/balanceAuthorization.js
Normal file
@@ -0,0 +1,65 @@
|
||||
// To ensure the command caller has, 1. an account and 2. has enough balance for the request
|
||||
// The pricing table needs to be in the DB so it can be updated via API
|
||||
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "balanceAuthorizations");
|
||||
|
||||
const {
|
||||
checkForAccount,
|
||||
createAccount,
|
||||
verifyBalance,
|
||||
insufficientTokensResponse,
|
||||
welcomeResponse
|
||||
} = require("../controllers/accountController");
|
||||
|
||||
/**
|
||||
* Authorize a transaction amount for an account
|
||||
*
|
||||
* @param {*} interaction
|
||||
* @param {*} command
|
||||
* @param {undefined|number} _tokens The amount of tokens to authorize, set to undefined if the value is in the interaction
|
||||
* @param {*} next
|
||||
* @returns
|
||||
*/
|
||||
exports.authorizeTokenUsage = async (interaction, command, _tokens = undefined, next) => {
|
||||
log.DEBUG("Command requires tokens? ", command.isPrivileged)
|
||||
if(!command.requiresTokens) return next(true);
|
||||
|
||||
if(!interaction.member && (!_tokens || !interaction.options.getNumber("tokens") || !command.defaultTokenUsage)) throw new Error("No member or tokens specified before attempting to authorize");
|
||||
const memberId = interaction.member.id;
|
||||
var tokensToBeUsed;
|
||||
|
||||
if (!_tokens || _tokens && isNaN(_tokens)){
|
||||
if (interaction.options.getNumber("tokens")) tokensToBeUsed = interaction.options.getNumber("tokens");
|
||||
else tokensToBeUsed = command.defaultTokenUsage;
|
||||
}
|
||||
else tokensToBeUsed = _tokens;
|
||||
|
||||
log.DEBUG(`Authorizing ${memberId} for a purchase worth ${tokensToBeUsed} tokens`)
|
||||
log.DEBUG("Checking for account associated with discord ID: ", memberId);
|
||||
await checkForAccount(memberId, async (err, results) => {
|
||||
if (err) throw err;
|
||||
|
||||
log.DEBUG("Results from checking for account: ", results);
|
||||
|
||||
// First time user is attempting transaction
|
||||
if(!results){
|
||||
log.DEBUG("No account for discord ID: ", memberId);
|
||||
await createAccount(memberId, (err, results) => {
|
||||
if (err) throw err;
|
||||
|
||||
if (results) return welcomeResponse(interaction);
|
||||
})
|
||||
} else{
|
||||
// User has an account
|
||||
log.DEBUG(`Account ID: ${results.account_id} found for discord ID: ${memberId}`);
|
||||
await verifyBalance(tokensToBeUsed, results.account_id, async (err, isVerified) => {
|
||||
if (err) throw err;
|
||||
|
||||
if(!isVerified) return insufficientTokensResponse(interaction);
|
||||
|
||||
return next(isVerified);
|
||||
})
|
||||
}
|
||||
})
|
||||
}
|
||||
23
Server/middleware/discordAuthorization.js
Normal file
23
Server/middleware/discordAuthorization.js
Normal file
@@ -0,0 +1,23 @@
|
||||
// To authorize a message sender for admin level commands
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "discordAuthorization");
|
||||
|
||||
const botAdmins = process.env.BOT_ADMINS;
|
||||
|
||||
exports.authorizeCommand = async (interaction, command, next) => {
|
||||
log.DEBUG("Command is privileged? ", command.isPrivileged)
|
||||
|
||||
// If the command is not privileged, run the command
|
||||
if (!command.isPrivileged) return next(true);
|
||||
|
||||
log.DEBUG(`${interaction.member.user} is attempting to run the privileged command '${command}'`);
|
||||
|
||||
// Check to see if the user has the role specified in the config
|
||||
if (!interaction.member.roles.cache.has(`${botAdmins}`)) {
|
||||
log.DEBUG(`Unauthorized - ${interaction.member.user} does not have the privilege to run '${command}'`);
|
||||
return await interaction.reply({ content: `Sorry ${interaction.member.user}, you are not permitted to run that command`, ephemeral: true });
|
||||
} else {
|
||||
log.DEBUG(`Authorized - ${interaction.member.user} can run '${command}'`);
|
||||
return next(true);
|
||||
}
|
||||
}
|
||||
115
Server/modules/linkCop.js
Normal file
115
Server/modules/linkCop.js
Normal file
@@ -0,0 +1,115 @@
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const log = new DebugBuilder("server", "linkCop");
|
||||
|
||||
const { linkCopInsults } = require('../extras/linkCopInsults');
|
||||
|
||||
const linkRegExp = /(?:http[s]?:\/\/)?(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)/g
|
||||
// Langvars that can be used in the insults
|
||||
// Mention the posting user
|
||||
const mtnUsrRegExp = /\{\%mtn_user\%\}/g
|
||||
// Reference the original message
|
||||
const refOgMsgRegExp = /\{\%ref_og_msg\%\}/g
|
||||
// Reference the channel the original message was posted in
|
||||
const refOgChannelRegExp = /\{\%ref_og_channel\%\}/g
|
||||
// Reference the links from the original message
|
||||
const refLinksRegExp = /\{\%ref_links\%\}/g
|
||||
|
||||
const restrictedChannelIds = [
|
||||
'757379843792044102',
|
||||
'367396189529833474',
|
||||
"918029426397184000"
|
||||
]
|
||||
|
||||
const approvedLinksChannel = "767303243285790721";
|
||||
|
||||
async function replaceLangVars(messageContent, selectedInsult) {
|
||||
selectedInsult = String(selectedInsult)
|
||||
selectedInsult = selectedInsult.replace(mtnUsrRegExp, messageContent.author);
|
||||
selectedInsult = selectedInsult.replace(refOgMsgRegExp, messageContent.content);
|
||||
selectedInsult = selectedInsult.replace(refOgChannelRegExp, `<#${messageContent.channelId}>`);
|
||||
selectedInsult = selectedInsult.replace(refLinksRegExp, Array(messageContent.links).join(", "));
|
||||
|
||||
return selectedInsult;
|
||||
}
|
||||
|
||||
exports.linkCop = async (message) => {
|
||||
if (!restrictedChannelIds.includes(message.channelId)) return;
|
||||
const client = message.client;
|
||||
log.DEBUG("Message Content: ", message.content);
|
||||
|
||||
log.VERBOSE("Interaction: ", message);
|
||||
const urls = String(message.content).matchAll(linkRegExp);
|
||||
if (!urls || urls.length === 0) return;
|
||||
log.DEBUG("Found URLs: ", urls);
|
||||
|
||||
const messageContent = {
|
||||
'author': message.author,
|
||||
'content': String(message.content),
|
||||
'channelId': message.channelId,
|
||||
'links': urls
|
||||
}
|
||||
|
||||
await message.delete();
|
||||
|
||||
var selectedInsult = linkCopInsults[Math.floor(Math.random()*linkCopInsults.length)];
|
||||
const finalMessage = await replaceLangVars(messageContent, selectedInsult);
|
||||
log.DEBUG("Final Message: ", finalMessage);
|
||||
|
||||
log.VERBOSE("Client: ", client.guilds);
|
||||
|
||||
client.channels.get(approvedLinksChannel).send(finalMessage);
|
||||
}
|
||||
|
||||
/* TODO
|
||||
- Get the message to send, getting an error because client.guilds and .channels doesn't have functions
|
||||
- Make sure the links are added to the final message
|
||||
*/
|
||||
|
||||
/*
|
||||
Message {
|
||||
channelId: '918029426397184000',
|
||||
guildId: '367396189529833472',
|
||||
id: '1101346007087845437',
|
||||
createdTimestamp: 1682651750109,
|
||||
type: 0,
|
||||
system: false,
|
||||
content: '<@943742040255115304> asd',
|
||||
author: User {
|
||||
id: '301507932678520833',
|
||||
bot: false,
|
||||
system: false,
|
||||
flags: UserFlagsBitField { bitfield: 0 },
|
||||
username: 'Logan',
|
||||
discriminator: '3331',
|
||||
avatar: null,
|
||||
banner: undefined,
|
||||
accentColor: undefined
|
||||
},
|
||||
pinned: false,
|
||||
tts: false,
|
||||
nonce: '1101346006672343040',
|
||||
embeds: [],
|
||||
components: [],
|
||||
attachments: Collection(0) [Map] {},
|
||||
stickers: Collection(0) [Map] {},
|
||||
position: null,
|
||||
editedTimestamp: null,
|
||||
reactions: ReactionManager { message: [Circular *1] },
|
||||
mentions: MessageMentions {
|
||||
everyone: false,
|
||||
users: Collection(1) [Map] { '943742040255115304' => [ClientUser] },
|
||||
roles: Collection(0) [Map] {},
|
||||
_members: null,
|
||||
_channels: null,
|
||||
_parsedUsers: null,
|
||||
crosspostedChannels: Collection(0) [Map] {},
|
||||
repliedUser: null
|
||||
},
|
||||
webhookId: null,
|
||||
groupActivityApplication: null,
|
||||
applicationId: null,
|
||||
activity: null,
|
||||
flags: MessageFlagsBitField { bitfield: 0 },
|
||||
reference: null,
|
||||
interaction: null
|
||||
} */
|
||||
189
Server/modules/mlsScraper/main.py
Normal file
189
Server/modules/mlsScraper/main.py
Normal file
@@ -0,0 +1,189 @@
|
||||
import re
|
||||
import json
|
||||
import pandas as pd
|
||||
import requests
|
||||
import os
|
||||
import random
|
||||
from fake_useragent import UserAgent
|
||||
from bs4 import BeautifulSoup
|
||||
from urllib.parse import urlparse, unquote, parse_qs
|
||||
from time import sleep
|
||||
ua = UserAgent()
|
||||
|
||||
|
||||
#simply scrape
|
||||
def scrape(url,**kwargs):
|
||||
|
||||
session=requests.Session()
|
||||
session.headers.update({
|
||||
'User-Agent': ua.random,
|
||||
"authority": "www.zillow.com",
|
||||
"accept": "*/*",
|
||||
"accept-language": "en-US,en;q=0.9",
|
||||
"cache-control": "no-cache",
|
||||
"sec-ch-ua-mobile": "?0",
|
||||
"sec-ch-ua-platform": '^\^"Windows^\^"',
|
||||
"sec-fetch-dest": "empty",
|
||||
"sec-fetch-mode": "cors",
|
||||
"sec-fetch-site": "same-origin",
|
||||
})
|
||||
|
||||
response=session.get(url,**kwargs)
|
||||
|
||||
return response
|
||||
|
||||
# Return all sections with key and attributes
|
||||
def slurp(html, tag, attributes):
|
||||
return BeautifulSoup(html, features="html.parser").findAll(tag, attributes)
|
||||
|
||||
# Returns the first number group from a given string
|
||||
def return_numbers(string):
|
||||
return int(re.findall(r'\d+', string)[0])
|
||||
|
||||
|
||||
class Listing:
|
||||
def __init__(self, address, bedrooms, bathrooms, sqft, price, link):
|
||||
self.address = address
|
||||
self.bedrooms = bedrooms
|
||||
self.bathrooms = bathrooms
|
||||
self.sqft = sqft
|
||||
self.price = price
|
||||
self.link = link
|
||||
|
||||
|
||||
class ScrapeZillowListings:
|
||||
def __init__(self, url):
|
||||
self.parsed_original_url = self.init_check_url(urlparse(url))
|
||||
self.html = scrape(url).text
|
||||
self.listings = []
|
||||
|
||||
def init_check_url(self, parsed_url):
|
||||
# Check to see if we are requesting listResults
|
||||
print(parsed_url)
|
||||
print(unquote(parsed_url.query))
|
||||
print(parse_qs(parsed_url.query)['wants'])
|
||||
for want in parse_qs(parsed_url.query)['wants']:
|
||||
print(unquote(unquote(want)))
|
||||
|
||||
return parsed_url
|
||||
|
||||
def run(self):
|
||||
self.listings.extend(self.scrape_listings(self.html))
|
||||
pages = []
|
||||
for page_nav in slurp(self.html, "nav", {"role":"navigation", "aria-label":"Pagination"}):
|
||||
page_nav = f"<html><head><head/><body>{page_nav}<body/><html/>"
|
||||
pages_list = slurp(page_nav, "li", {})
|
||||
for page in pages_list:
|
||||
if re.match("\d{1,2}", page.text) and not page.text == "1":
|
||||
parsed_url = self.setup_url(page.find('a').get('href'))
|
||||
sleep(random.randint(0,15))
|
||||
temp_html = scrape(parsed_url.geturl()).text
|
||||
self.listings.extend(self.scrape_listings(temp_html))
|
||||
|
||||
return self.listings
|
||||
|
||||
def print_listings(self):
|
||||
index = 0
|
||||
for temp_listing in self.listings:
|
||||
print("--------")
|
||||
print(f"Listing #{index}")
|
||||
print(temp_listing.address)
|
||||
print(temp_listing.price)
|
||||
print(temp_listing.bedrooms)
|
||||
print(temp_listing.bathrooms)
|
||||
print(temp_listing.sqft)
|
||||
print(temp_listing.link)
|
||||
print("--------")
|
||||
index += 1
|
||||
|
||||
def scrape_listings(self, html):
|
||||
temp_listings = []
|
||||
for listing in slurp(html, "article", {"data-test":"property-card"}):
|
||||
listing = f"<html><head><head/><body>{listing}<body/><html/>"
|
||||
|
||||
uls = slurp(listing, "li", {})
|
||||
beds = 0
|
||||
baths = 0
|
||||
sqft = 0
|
||||
for ul in uls:
|
||||
ul = ul.get_text()
|
||||
if ("bds" in str(ul)):
|
||||
beds = return_numbers(ul)
|
||||
if ("ba" in str(ul)):
|
||||
baths = return_numbers(ul)
|
||||
if ("sqft" in str(ul)):
|
||||
sqft = return_numbers(ul)
|
||||
|
||||
temp_listings.append(Listing(
|
||||
address=slurp(listing, "address", {"data-test":"property-card-addr"})[0].get_text(),
|
||||
bedrooms=beds,
|
||||
bathrooms=baths,
|
||||
sqft=sqft,
|
||||
price=slurp(listing, "span", {"data-test":"property-card-price"})[0].get_text(),
|
||||
link=slurp(listing, "a", {"data-test":"property-card-link"})[0].get('href'),
|
||||
))
|
||||
|
||||
return temp_listings
|
||||
|
||||
def setup_url(self, url):
|
||||
parsed_url = urlparse(url)
|
||||
print(parsed_url)
|
||||
if not parsed_url.netloc:
|
||||
return urlparse(f"{self.parsed_original_url.scheme}://{self.parsed_original_url.netloc}{parsed_url.path}{self.parsed_original_url.query}{self.parsed_original_url.params}")
|
||||
|
||||
#create dataframe
|
||||
def etl(response):
|
||||
|
||||
#regex to find the data
|
||||
|
||||
for listing in listings:
|
||||
print("--------")
|
||||
print(listing)
|
||||
print("--------")
|
||||
|
||||
print("FORCE STOP")
|
||||
exit()
|
||||
|
||||
#convert text to dict via json
|
||||
dicts=[json.loads('{'+i+'}') for i in num]
|
||||
|
||||
#create dataframe
|
||||
df=pd.DataFrame()
|
||||
for ind,val in enumerate(text):
|
||||
df[val]=dicts[ind].values()
|
||||
df.index=dicts[ind].keys()
|
||||
|
||||
return df
|
||||
|
||||
|
||||
def main():
|
||||
#scrapper = ScrapeZillowListings('https://www.zillow.com/westchester-county-ny/?searchQueryState=%7B%22usersSearchTerm%22%3A%22Yorktown%20Heights%2C%20NY%22%2C%22mapBounds%22%3A%7B%22north%22%3A41.69948153143324%2C%22east%22%3A-72.68804025585938%2C%22south%22%3A40.83865274682678%2C%22west%22%3A-74.29479074414063%7D%2C%22isMapVisible%22%3Atrue%2C%22filterState%22%3A%7B%22price%22%3A%7B%22max%22%3A250000%7D%2C%22ah%22%3A%7B%22value%22%3Atrue%7D%2C%22sort%22%3A%7B%22value%22%3A%22days%22%7D%2C%22land%22%3A%7B%22value%22%3Afalse%7D%2C%22cmsn%22%3A%7B%22value%22%3Afalse%7D%2C%22sche%22%3A%7B%22value%22%3Afalse%7D%2C%22schm%22%3A%7B%22value%22%3Afalse%7D%2C%22schh%22%3A%7B%22value%22%3Afalse%7D%2C%22schp%22%3A%7B%22value%22%3Afalse%7D%2C%22schr%22%3A%7B%22value%22%3Afalse%7D%2C%22schc%22%3A%7B%22value%22%3Afalse%7D%2C%22schu%22%3A%7B%22value%22%3Afalse%7D%7D%2C%22isListVisible%22%3Atrue%2C%22regionSelection%22%3A%5B%7B%22regionId%22%3A3148%2C%22regionType%22%3A4%7D%2C%7B%22regionId%22%3A2694%2C%22regionType%22%3A4%7D%5D%2C%22pagination%22%3A%7B%7D%7D')
|
||||
scrapper = ScrapeZillowListings("https://www.zillow.com/search/GetSearchPageState.htm?searchQueryState=^%^7B^%^22pagination^%^22^%^3A^%^7B^%^7D^%^2C^%^22usersSearchTerm^%^22^%^3A^%^22Yorktown^%^20Heights^%^2C^%^20NY^%^22^%^2C^%^22mapBounds^%^22^%^3A^%^7B^%^22north^%^22^%^3A42.99146217894271^%^2C^%^22east^%^22^%^3A-70.80209903627659^%^2C^%^22south^%^22^%^3A39.549453943310084^%^2C^%^22west^%^22^%^3A-77.00937442690159^%^7D^%^2C^%^22mapZoom^%^22^%^3A8^%^2C^%^22regionSelection^%^22^%^3A^%^5B^%^7B^%^22regionId^%^22^%^3A3148^%^2C^%^22regionType^%^22^%^3A4^%^7D^%^2C^%^7B^%^22regionId^%^22^%^3A2694^%^2C^%^22regionType^%^22^%^3A4^%^7D^%^5D^%^2C^%^22isMapVisible^%^22^%^3Atrue^%^2C^%^22filterState^%^22^%^3A^%^7B^%^22price^%^22^%^3A^%^7B^%^22max^%^22^%^3A250000^%^7D^%^2C^%^22isAllHomes^%^22^%^3A^%^7B^%^22value^%^22^%^3Atrue^%^7D^%^2C^%^22sortSelection^%^22^%^3A^%^7B^%^22value^%^22^%^3A^%^22days^%^22^%^7D^%^2C^%^22isLotLand^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isMiddleSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isHighSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22includeUnratedSchools^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isComingSoon^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isPublicSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isPrivateSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isElementarySchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isCharterSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^7D^%^2C^%^22isListVisible^%^22^%^3Atrue^%^7D&wants=^\{^%^22cat1^%^22:^\[^%^22mapResults^%^22^\]^\}&requestId=3")
|
||||
listings = scrapper.run()
|
||||
scrapper.print_listings()
|
||||
|
||||
#df=etl(response)
|
||||
|
||||
return
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
main()
|
||||
|
||||
|
||||
#curl "https://www.zillow.com/search/GetSearchPageState.htm?searchQueryState=^%^7B^%^22pagination^%^22^%^3A^%^7B^%^7D^%^2C^%^22usersSearchTerm^%^22^%^3A^%^22Yorktown^%^20Heights^%^2C^%^20NY^%^22^%^2C^%^22mapBounds^%^22^%^3A^%^7B^%^22north^%^22^%^3A42.99146217894271^%^2C^%^22east^%^22^%^3A-70.80209903627659^%^2C^%^22south^%^22^%^3A39.549453943310084^%^2C^%^22west^%^22^%^3A-77.00937442690159^%^7D^%^2C^%^22mapZoom^%^22^%^3A8^%^2C^%^22regionSelection^%^22^%^3A^%^5B^%^7B^%^22regionId^%^22^%^3A3148^%^2C^%^22regionType^%^22^%^3A4^%^7D^%^2C^%^7B^%^22regionId^%^22^%^3A2694^%^2C^%^22regionType^%^22^%^3A4^%^7D^%^5D^%^2C^%^22isMapVisible^%^22^%^3Atrue^%^2C^%^22filterState^%^22^%^3A^%^7B^%^22price^%^22^%^3A^%^7B^%^22max^%^22^%^3A250000^%^7D^%^2C^%^22isAllHomes^%^22^%^3A^%^7B^%^22value^%^22^%^3Atrue^%^7D^%^2C^%^22sortSelection^%^22^%^3A^%^7B^%^22value^%^22^%^3A^%^22days^%^22^%^7D^%^2C^%^22isLotLand^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isMiddleSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isHighSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22includeUnratedSchools^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isComingSoon^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isPublicSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isPrivateSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isElementarySchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^2C^%^22isCharterSchool^%^22^%^3A^%^7B^%^22value^%^22^%^3Afalse^%^7D^%^7D^%^2C^%^22isListVisible^%^22^%^3Atrue^%^7D&wants=^\{^%^22cat1^%^22:^\[^%^22mapResults^%^22^\]^\}&requestId=3",
|
||||
# "authority: www.zillow.com",
|
||||
# "accept: */*",
|
||||
# "accept-language: en-US,en;q=0.9",
|
||||
# "cache-control: no-cache",
|
||||
# "cookie: JSESSIONID=97FD1EB701E102B7353E8EA4528843CE; zguid=24^|^%^24825bd6e9-4f90-46df-a475-4a9910b5847c; zgsession=1^|a6a5b7ca-c651-45a2-93c8-c5b66fea68d3; AWSALB=oQ3DGTMPgyQOTPA6zLmQ0liqJ1oax2QoQ5rUSCsORkWP52C7k6G8H1gZnlxOtgU/zzO503UHUnQ7tUeivhOnupv7aYI6+E5LxUZl4TeE0JyhvT3pZ6LYeC9iFbTw; AWSALBCORS=oQ3DGTMPgyQOTPA6zLmQ0liqJ1oax2QoQ5rUSCsORkWP52C7k6G8H1gZnlxOtgU/zzO503UHUnQ7tUeivhOnupv7aYI6+E5LxUZl4TeE0JyhvT3pZ6LYeC9iFbTw; search=6^|1689549806090^%^7Crect^%^3D42.92311815473404^%^252C-70.80209903627659^%^252C39.62142250427077^%^252C-77.00937442690159^%^26rid^%^3D2694^%^26disp^%^3Dmap^%^26mdm^%^3Dauto^%^26p^%^3D1^%^26sort^%^3Ddays^%^26z^%^3D1^%^26listPriceActive^%^3D1^%^26type^%^3Dhouse^%^252Ccondo^%^252Capartment_duplex^%^252Cmobile^%^252Ctownhouse^%^26lt^%^3Dfsba^%^252Cfsbo^%^252Cfore^%^252Cnew^%^252Cauction^%^26price^%^3D0-250000^%^26fs^%^3D1^%^26fr^%^3D0^%^26mmm^%^3D0^%^26rs^%^3D0^%^26ah^%^3D0^%^26singlestory^%^3D0^%^26housing-connector^%^3D0^%^26abo^%^3D0^%^26garage^%^3D0^%^26pool^%^3D0^%^26ac^%^3D0^%^26waterfront^%^3D0^%^26finished^%^3D0^%^26unfinished^%^3D0^%^26cityview^%^3D0^%^26mountainview^%^3D0^%^26parkview^%^3D0^%^26waterview^%^3D0^%^26hoadata^%^3D1^%^26zillow-owned^%^3D0^%^263dhome^%^3D0^%^26featuredMultiFamilyBuilding^%^3D0^%^26commuteMode^%^3Ddriving^%^26commuteTimeOfDay^%^3Dnow^%^09^%^092694^%^09^%^09^%^09^%^09^%^09^%^09",
|
||||
# "pragma: no-cache",
|
||||
# "sec-ch-ua: ^\^"Not.A/Brand^\^";v=^\^"8^\^", ^\^"Chromium^\^";v=^\^"114^\^", ^\^"Google Chrome^\^";v=^\^"114^\^"",
|
||||
# "sec-ch-ua-mobile: ?0",
|
||||
# "sec-ch-ua-platform: ^\^"Windows^\^"",
|
||||
# "sec-fetch-dest: empty",
|
||||
# "sec-fetch-mode: cors",
|
||||
# "sec-fetch-site: same-origin",
|
||||
# "user-agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/114.0.0.0 Safari/537.36",
|
||||
# "x-kl-ajax-request: Ajax_Request",
|
||||
# --compressed
|
||||
4
Server/modules/mlsScraper/requirements.txt
Normal file
4
Server/modules/mlsScraper/requirements.txt
Normal file
@@ -0,0 +1,4 @@
|
||||
pandas
|
||||
requests
|
||||
fake-useragent
|
||||
beautifulsoup4
|
||||
2995
Server/package-lock.json
generated
2995
Server/package-lock.json
generated
File diff suppressed because it is too large
Load Diff
@@ -1,17 +1,38 @@
|
||||
{
|
||||
"name": "server",
|
||||
"version": "0.0.0",
|
||||
"private": true,
|
||||
"scripts": {
|
||||
"start": "node ./bin/www"
|
||||
},
|
||||
"name": "Emmelia",
|
||||
"version": "1.0.0",
|
||||
"description": "Discord RSS News Bot",
|
||||
"main": "index.js",
|
||||
"dependencies": {
|
||||
"cookie-parser": "~1.4.4",
|
||||
"debug": "~2.6.9",
|
||||
"ejs": "~2.6.1",
|
||||
"express": "~4.16.1",
|
||||
"http-errors": "~1.6.3",
|
||||
"morgan": "~1.9.1",
|
||||
"mysql": "^2.18.1"
|
||||
}
|
||||
"@discordjs/builders": "^1.6.3",
|
||||
"@discordjs/rest": "^1.7.1",
|
||||
"axios": "^1.4.0",
|
||||
"chatgpt": "^5.2.4",
|
||||
"cookie-parser": "^1.4.6",
|
||||
"debug": "^4.3.4",
|
||||
"discord-api-types": "^0.37.42",
|
||||
"discord.js": "^14.11.0",
|
||||
"dotenv": "^16.0.3",
|
||||
"ejs": "^3.1.9",
|
||||
"express": "^4.18.2",
|
||||
"fs": "^0.0.1-security",
|
||||
"gpt-3-encoder": "^1.1.4",
|
||||
"http-errors": "*",
|
||||
"jsdoc": "^4.0.2",
|
||||
"jsonfile": "^6.1.0",
|
||||
"morgan": "^1.10.0",
|
||||
"mysql": "^2.18.1",
|
||||
"node-html-markdown": "^1.3.0",
|
||||
"node-html-parser": "^6.1.5",
|
||||
"openai": "^3.2.1",
|
||||
"parse-files": "^0.1.1",
|
||||
"rss-parser": "^3.13.0",
|
||||
"user-agents": "^1.0.1393"
|
||||
},
|
||||
"scripts": {
|
||||
"test": "echo \"Error: no test specified\" && exit 1",
|
||||
"start": "node index.js"
|
||||
},
|
||||
"author": "Logan Cusano",
|
||||
"license": "ISC"
|
||||
}
|
||||
|
||||
@@ -1,8 +0,0 @@
|
||||
body {
|
||||
padding: 50px;
|
||||
font: 14px "Lucida Grande", Helvetica, Arial, sans-serif;
|
||||
}
|
||||
|
||||
a {
|
||||
color: #00B7FF;
|
||||
}
|
||||
@@ -1,9 +1,31 @@
|
||||
const libCore = require("../libCore");
|
||||
var express = require('express');
|
||||
var router = express.Router();
|
||||
|
||||
/* GET home page. */
|
||||
router.get('/', (req, res, next) => {
|
||||
res.render('index', { title: 'Express' });
|
||||
router.get('/', (req, res) => {
|
||||
var sources = libCore.getSources();
|
||||
//res.render('index', { "sources": sources });
|
||||
|
||||
var htmlOutput = "";
|
||||
|
||||
sources.forEach(source => {
|
||||
htmlOutput += `
|
||||
<div style='margin-bottom:15px;'>
|
||||
|
||||
<div> Title: ${source.title} </div>
|
||||
<div> Link: ${source.link} </div>
|
||||
<div> category: ${source.category} </div>
|
||||
|
||||
</div>
|
||||
<div>
|
||||
<hr />
|
||||
|
||||
</div>
|
||||
|
||||
`
|
||||
});
|
||||
res.send(htmlOutput);
|
||||
});
|
||||
|
||||
module.exports = router;
|
||||
|
||||
@@ -28,4 +28,8 @@ router.get('/nodeInfo', nodesController.getNodeInfo);
|
||||
// Client checkin with the server to update information
|
||||
router.post('/nodeCheckIn', nodesController.nodeCheckIn);
|
||||
|
||||
// TODO Need to authenticate this request
|
||||
// Request a particular client to join a particular channel listening to a particular preset
|
||||
router.post('/joinServer', nodesController.requestNodeJoinServer);
|
||||
|
||||
module.exports = router;
|
||||
|
||||
15
Server/update.sh
Normal file
15
Server/update.sh
Normal file
@@ -0,0 +1,15 @@
|
||||
#!/bin/bash
|
||||
# Stating Message
|
||||
echo "<!-- UPDATING ---!>"
|
||||
|
||||
# TODO - Add an updater for Stable Diffusion API
|
||||
|
||||
# Update the git Repo
|
||||
git fetch -a -p
|
||||
git pull
|
||||
|
||||
# Install any new libraries
|
||||
npm i
|
||||
|
||||
# Update complete message
|
||||
echo "<!--- UPDATE COMPLETE! ---!>"
|
||||
46
Server/utilities/customSlashCommandBuilder.js
Normal file
46
Server/utilities/customSlashCommandBuilder.js
Normal file
@@ -0,0 +1,46 @@
|
||||
const { SlashCommandBuilder, SlashCommandStringOption } = require('discord.js');
|
||||
const { DebugBuilder } = require("../utilities/debugBuilder");
|
||||
const { BufferToJson } = require("../utilities/utils");
|
||||
const log = new DebugBuilder("server", "customSlashCommandBuilder");
|
||||
|
||||
const { getAllNodes, getAllNodesSync } = require("../utilities/mysqlHandler");
|
||||
|
||||
exports.customSlashCommandBuilder = class customSlashCommandBuilder extends SlashCommandBuilder {
|
||||
constructor() {
|
||||
super();
|
||||
}
|
||||
|
||||
async addAllSystemPresetOptions() {
|
||||
const nodeObjects = await new Promise((recordResolve, recordReject) => {
|
||||
getAllNodes((nodeRows) => {
|
||||
recordResolve(nodeRows);
|
||||
});
|
||||
});
|
||||
log.DEBUG("Node objects: ", nodeObjects);
|
||||
var presetsAvailable = [];
|
||||
for (const nodeObject of nodeObjects) {
|
||||
log.DEBUG("Node object: ", nodeObject);
|
||||
for (const presetName in nodeObject.nearbySystems) presetsAvailable.push(nodeObject.nearbySystems[presetName]);
|
||||
}
|
||||
|
||||
log.DEBUG("All Presets available: ", presetsAvailable);
|
||||
|
||||
// Remove duplicates
|
||||
presetsAvailable = [...new Set(presetsAvailable)];
|
||||
log.DEBUG("DeDuped Presets available: ", presetsAvailable);
|
||||
|
||||
this.addStringOption(option => option.setName("preset").setRequired(true).setDescription("The channels"));
|
||||
for (const preset of presetsAvailable){
|
||||
log.DEBUG("Preset: ", preset);
|
||||
this.options[0].addChoices({
|
||||
'name': String(preset),
|
||||
'value': String(preset)
|
||||
});
|
||||
}
|
||||
log.DEBUG("Preset Options: ", this);
|
||||
|
||||
return this;
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user