diff --git a/Client/app.js b/Client/app.js index 7aaed47..6be566d 100644 --- a/Client/app.js +++ b/Client/app.js @@ -5,8 +5,8 @@ var cookieParser = require('cookie-parser'); var logger = require('morgan'); var indexRouter = require('./routes/index'); -var botController = require('./routes/bot'); -var clientController = require('./routes/client'); +var botRouter = require('./routes/bot'); +var clientRouter = require('./routes/client'); var app = express(); @@ -23,10 +23,10 @@ app.use(express.static(path.join(__dirname, 'public'))); app.use('/', indexRouter); // Discord bot control route -app.use('/bot', botController); +app.use('/bot', botRouter); // Local client control route -app.use("/client", clientController); +app.use("/client", clientRouter); // catch 404 and forward to error handler app.use(function(req, res, next) { diff --git a/Client/controllers/botController.js b/Client/controllers/botController.js index e69de29..ac68a30 100644 --- a/Client/controllers/botController.js +++ b/Client/controllers/botController.js @@ -0,0 +1,86 @@ +// Debug +const { DebugBuilder } = require("../utilities/debugBuilder.js"); +const log = new DebugBuilder("client", "clientController"); +// Modules +const path = require('path'); +const fork = require('child_process').fork; +const discordBotPath = path.resolve('discord-bot/app.js'); + +let botChildProcess, radioChildProcess, 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; + } +} + +/** + * Get Status of the discord process + */ +exports.getStatus = (req, res) => { + if (!botChildProcess) return res.sendStatus(200); + botChildProcess.send(new BPOB("Status")); + tempRes = res; +} + +/** + * Start the bot and join the server and preset specified + */ +exports.joinServer = (req, res) => { + const channelID = req.body.channelID; + const presetName = req.body.presetName; + + 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); + + // 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(); + return; + case "ChgPreSet": + tempRes.sendStatus(200); + tempRes = undefined; + return; + } + }) +} + +/** + * 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; +} \ No newline at end of file diff --git a/Client/discord-bot/app.js b/Client/discord-bot/app.js index 2352948..3dcdca2 100644 --- a/Client/discord-bot/app.js +++ b/Client/discord-bot/app.js @@ -13,6 +13,23 @@ 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: [ @@ -23,9 +40,59 @@ const client = new Client({ ] }); +/** + * 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"}); }); /* @@ -43,10 +110,10 @@ client.on('interactionCreate', (interaction) => { ping(interaction); break; case "join": - join(interaction); + join({ interaction: interaction }); break; case "leave": - leave(interaction); + leave({ interaction: interaction }); break; case "status": status({ interaction: interaction }); @@ -63,6 +130,16 @@ 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(); diff --git a/Client/discord-bot/commands/join.js b/Client/discord-bot/commands/join.js index 3e50f57..dc0eded 100644 --- a/Client/discord-bot/commands/join.js +++ b/Client/discord-bot/commands/join.js @@ -1,4 +1,8 @@ -import {joinVoiceChannel} from "@discordjs/voice"; +// 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"; @@ -11,17 +15,29 @@ 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){ - const voiceChannel = interaction.options.getChannel('voicechannel'); +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: voiceChannel.id, - guildId: interaction.guildId, - adapterCreator: interaction.guild.voiceAdapterCreator, + channelId: channelID, + guildId: guildID, + adapterCreator: guildObj.voiceAdapterCreator, selfMute: false, selfDeaf: false, }); - replyToInteraction(interaction, `Ok, Joining ${voiceChannel.name}`); const audioInstance = createAudioInstance(); @@ -32,5 +48,12 @@ export default async function join(interaction){ voiceConnection.playOpusPacket(encoded); }) + // Exit the audio handler when the bot disconnects + voiceConnection.on(VoiceConnectionStatus.Destroyed, () => { + audioInstance.quit(); + }) + audioInstance.start(); + + if (guildID && callback) callback(); } \ No newline at end of file diff --git a/Client/discord-bot/commands/leave.js b/Client/discord-bot/commands/leave.js index 06bbdb8..a39c2a5 100644 --- a/Client/discord-bot/commands/leave.js +++ b/Client/discord-bot/commands/leave.js @@ -8,11 +8,24 @@ import {replyToInteraction} from "../utilities/messageHandler.js"; * 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){ - const guildId = interaction.guild.id; - const voiceConnection = getVoiceConnection(guildId); - if (!voiceConnection) return replyToInteraction(interaction, "Not in a voice channel."); +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(); - return replyToInteraction(interaction, `Goodbye`); + + response = "Goodbye" + if (interaction) return replyToInteraction(interaction, response); + else callback(response); } \ No newline at end of file diff --git a/Client/discord-bot/commands/status.js b/Client/discord-bot/commands/status.js index b5ac166..b3ad44f 100644 --- a/Client/discord-bot/commands/status.js +++ b/Client/discord-bot/commands/status.js @@ -7,15 +7,18 @@ import {getVoiceConnection} from "@discordjs/voice"; import { replyToInteraction } from '../utilities/messageHandler.js'; -export default async function status({interaction= undefined, guildID= undefined}) { +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); - log.DEBUG("guildID: ", guildID) - log.DEBUG("Voice Connection: ", voiceConnection) + const statusObj = { + "guildID": guildID, "voiceConnection": voiceConnection + } + + //log.DEBUG('Status Object: ', statusObj); // get the status and return it accordingly (message reply / module) @@ -23,6 +26,6 @@ export default async function status({interaction= undefined, guildID= undefined return replyToInteraction(interaction, "Pong! I have Aids and now you do too!"); } else { - + callback(statusObj); } } \ No newline at end of file diff --git a/Client/discord-bot/controllers/audioController.js b/Client/discord-bot/controllers/audioController.js index 7ad034e..d6ab149 100644 --- a/Client/discord-bot/controllers/audioController.js +++ b/Client/discord-bot/controllers/audioController.js @@ -35,7 +35,7 @@ export function getAudioDevices(){ return null; } }).filter(Boolean); - log.DEBUG("Device List: ", deviceList); + //log.DEBUG("Device List: ", deviceList); return deviceList; } diff --git a/Client/discord-bot/utilities/configHandler.js b/Client/discord-bot/utilities/configHandler.js index 1d67fe3..49d367f 100644 --- a/Client/discord-bot/utilities/configHandler.js +++ b/Client/discord-bot/utilities/configHandler.js @@ -1,10 +1,12 @@ -import { readFileSync } from 'fs'; // 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("./config/botConfig.json")); + return JSON.parse(readFileSync(path.resolve("discord-bot/config/botConfig.json"))); } export function getTOKEN() { @@ -17,7 +19,7 @@ export function getTOKEN() { export function getGuildID() { const parsedJSON = getConfig(); - const guildID = BigInt(parsedJSON.GuildID); + const guildID = parsedJSON.GuildID; log.DEBUG("Guild ID: ", guildID); return guildID; @@ -25,7 +27,7 @@ export function getGuildID() { export function getApplicationID() { const parsedJSON = getConfig(); - const appID = BigInt(parsedJSON.ApplicationID); + const appID = parsedJSON.ApplicationID; log.DEBUG("Application ID: ", appID); return appID; diff --git a/Client/routes/bot.js b/Client/routes/bot.js index aa8b59f..40ed5c2 100644 --- a/Client/routes/bot.js +++ b/Client/routes/bot.js @@ -1,18 +1,14 @@ var express = require('express'); +const botController = require("../controllers/botController"); var router = express.Router(); -/* GET users listing. -router.get('/', function(req, res, next) { - res.send('respond with a resource'); -}); - */ - /** GET bot status * Check to see if the bot is online and if so, if it is currently connected to anything * - * The status of the bot: 200 = online, 202 = connected, 500 + JSON = encountered error + * 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 * @returns status */ +router.get('/status', botController.getStatus); /** POST bot join channel * Join the channel specified listening to the specified freq/mode @@ -21,6 +17,7 @@ router.get('/', function(req, res, next) { * @param req.body.channelId The channel ID to join * @param req.body.presetName The name of the preset to start listening to */ +router.post('/join', botController.joinServer); /** POST bot leave channel * Will leave the channel it is currently listening to if any, otherwise it will just return that it is not connected @@ -28,6 +25,7 @@ router.get('/', function(req, res, next) { * The status of the bot: 200 = no change, 202 = changed successfully, 500 + JSON = encountered error * @returns status */ +router.post('/leave', botController.leaveServer); /** POST change bot preset * This will change the bot to the preset specified (if different from what is currently playing)