diff --git a/Server/clientIds.json.EXAMPLE b/Server/clientIds.json.EXAMPLE new file mode 100644 index 0000000..bf05146 --- /dev/null +++ b/Server/clientIds.json.EXAMPLE @@ -0,0 +1,6 @@ +{ + "[ID from Discord]": { + "name": "[Nickname of the Bot]", + "id": "[Client ID from Discord Dev Portal]" + } +} diff --git a/Server/commands/join.js b/Server/commands/join.js new file mode 100644 index 0000000..d5bb497 --- /dev/null +++ b/Server/commands/join.js @@ -0,0 +1,101 @@ +// Modules +const { customSlashCommandBuilder } = require('../utilities/customSlashCommandBuilder'); +const { DebugBuilder } = require("../utilities/debugBuilder"); +const { BufferToJson, getMembersInRole, getKeyByArrayValue } = require("../utilities/utils"); +const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests"); +const { readFileSync } = require('fs'); +const { getOnlineNodes, getNodeInfoFromId } = require("../utilities/mysqlHandler"); +const path = require('path'); + +// 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 + */ +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 reqOptions = new requestOptions("/bot/status", "GET", node.ip, node.port); + await new Promise(resolve => sendHttpRequest(reqOptions, "", (responseObj) => { + if (!responseObj || !responseObj.statusCode == 200) return resolve(false); + log.VERBOSE("Response Object from node ", node, responseObj); + nodesCurrentlyAvailable.push(node); + resolve(true); + })); + } + 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 Object(JSON.parse(readFileSync(path.resolve(__dirname, '../clientIds.json')))); + log.DEBUG("All clients: ", Object.keys(availableClientIds)); + log.DEBUG("Client IDs Used: ", clientIdsUsed.keys()); + for (const usedClientId of clientIdsUsed.keys()) { + log.DEBUG("Used Client ID: ", usedClientId); + if (Object.keys(availableClientIds).includes(usedClientId)) { + delete availableClientIds[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.") + + const reqOptions = new requestOptions("/bot/join", "POST", nodesCurrentlyAvailable[0].ip, nodesCurrentlyAvailable[0].port); + const selectedClientId = availableClientIds[Object.keys(availableClientIds)[0]]; + sendHttpRequest(reqOptions, JSON.stringify({ + "channelId": channelId, + "clientId": selectedClientId.id, + "presetName": presetName + }), (responseObj) => { + log.VERBOSE("Response Object from node ", nodesCurrentlyAvailable[0], responseObj); + if (!responseObj || !responseObj.statusCode == 200) return false; + nodesCurrentlyAvailable.push(nodesCurrentlyAvailable[0]); + }); +} +exports.joinServerWrapper = joinServerWrapper; + + +var presetsAvailable = []; +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'); + const channelId = interaction.member.voice.channel.id; + log.DEBUG(`Join requested by: ${interaction.user.username}, to: '${presetName}', in channel: ${channelId} / ${guildId}`); + await interaction.editReply('**Pong.**'); + + const onlineBots = await getMembersInRole(interaction); + + log.DEBUG("Online Bots: ", onlineBots); + + await joinServerWrapper(presetName, channelId, onlineBots.online); + //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()); + } + } +} \ No newline at end of file diff --git a/Server/utilities/customSlashCommandBuilder.js b/Server/utilities/customSlashCommandBuilder.js new file mode 100644 index 0000000..fb6d84a --- /dev/null +++ b/Server/utilities/customSlashCommandBuilder.js @@ -0,0 +1,86 @@ +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; + } + + + + /* + return new class extends SlashCommandStringOption { + constructor() { + super(); + getAllNodes((nodeObjects) => { + this.name = "preset" + this.required = "false" + + var presetsAvailable = []; + for (const nodeObject of nodeObjects) { + nodeObject.nearbySystems = BufferToJson(nodeObject.nearbySystems); + 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); + + var choicesList = [] + for (const preset of presetsAvailable){ + log.DEBUG("Preset: ", preset); + choicesList.push({ + name: preset, + value: preset + }) + } + + log.DEBUG("Choice List: ", choicesList); + + this.choices = JSON.stringify(choicesList); + return this; + }); + } + } + */ +} +