From e8d68b2da786d45706af2ec2ef531f561e4f92c8 Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Sat, 10 Jun 2023 22:16:39 -0400 Subject: [PATCH] Initial #7 & #9 - Working commands - Keeps track of open connections --- Server/commands/join.js | 28 ++-- Server/commands/leave.js | 83 ++++++++++++ Server/events/messageCreate.js | 2 +- Server/utilities/mysqlHandler.js | 222 +++++++++++++++++++++++++++---- Server/utilities/recordHelper.js | 44 +++++- Server/utilities/utils.js | 41 +++++- 6 files changed, 379 insertions(+), 41 deletions(-) create mode 100644 Server/commands/leave.js diff --git a/Server/commands/join.js b/Server/commands/join.js index b1a4841..05c7f34 100644 --- a/Server/commands/join.js +++ b/Server/commands/join.js @@ -3,7 +3,7 @@ const { customSlashCommandBuilder } = require('../utilities/customSlashCommandBu const { DebugBuilder } = require("../utilities/debugBuilder"); const { getMembersInRole, getAllClientIds } = require("../utilities/utils"); const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests"); -const { getOnlineNodes, updateNodeInfo } = require("../utilities/mysqlHandler"); +const { getOnlineNodes, updateNodeInfo, addNodeConnection } = require("../utilities/mysqlHandler"); // Global Vars const log = new DebugBuilder("server", "join"); @@ -47,37 +47,45 @@ async function joinServerWrapper(presetName, channelId, clientIdsUsed) { var selectedClientId; if (typeof clientIdsUsed === 'string') { - if (Object.keys(availableClientIds).includes(clientIdsUsed)) selectedClientId = availableClientIds[clientIdsUsed]; + 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); - if (Object.keys(availableClientIds).includes(usedClientId)) { - delete availableClientIds[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[Object.keys(availableClientIds)[0]]; + selectedClientId = availableClientIds[0]; } const selectedNode = nodesCurrentlyAvailable[0]; const reqOptions = new requestOptions("/bot/join", "POST", selectedNode.ip, selectedNode.port); - sendHttpRequest(reqOptions, JSON.stringify({ + const postObject = { "channelId": channelId, - "clientId": selectedClientId.id, + "clientId": selectedClientId.clientId, "presetName": presetName - }), async (responseObj) => { + }; + 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 selectedNode.connected = true; - const updatedNode = await updateNodeInfo(selectedNode) + 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; diff --git a/Server/commands/leave.js b/Server/commands/leave.js new file mode 100644 index 0000000..666825c --- /dev/null +++ b/Server/commands/leave.js @@ -0,0 +1,83 @@ +// 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 discord + + // Updating the node object in the DB + node.connected = false; + const updatedNode = await updateNodeInfo(node) + log.DEBUG("Updated Node: ", updatedNode); + + // 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()); + } + } +} \ No newline at end of file diff --git a/Server/events/messageCreate.js b/Server/events/messageCreate.js index 149580b..e7c6915 100644 --- a/Server/events/messageCreate.js +++ b/Server/events/messageCreate.js @@ -9,6 +9,6 @@ const log = new DebugBuilder("server", "messageCreate"); module.exports = { name: Events.MessageCreate, async execute(interaction) { - await linkCop(interaction); + //await linkCop(interaction); }, }; \ No newline at end of file diff --git a/Server/utilities/mysqlHandler.js b/Server/utilities/mysqlHandler.js index 2c49041..090ab9b 100644 --- a/Server/utilities/mysqlHandler.js +++ b/Server/utilities/mysqlHandler.js @@ -1,9 +1,9 @@ require('dotenv').config(); const mysql = require('mysql'); const utils = require('./utils'); -const { nodeObject } = require("./recordHelper"); +const { nodeObject, clientObject, connectionObject } = require("./recordHelper"); const { DebugBuilder } = require("../utilities/debugBuilder"); -const { BufferToJson } = require("../utilities/utils"); +const { BufferToJson, getClientObjectByClientID } = require("../utilities/utils"); const log = new DebugBuilder("server", "mysSQLHandler"); @@ -15,6 +15,7 @@ const connection = mysql.createPool({ }); const nodesTable = `${process.env.NODE_DB_NAME}.nodes`; +const nodeConnectionsTable = `${process.env.NODE_DB_NAME}.node_connections`; /** * Return a node object from a single SQL row @@ -31,7 +32,8 @@ function returnNodeObjectFromRow(row) { _location: row.location, _nearbySystems: BufferToJson(row.nearbySystems), _online: (row.online === 1) ? true : false, - _connected: (row.connected === 1) ? true : false + _connected: (row.connected === 1) ? true : false, + _connection: (row.connection) ? row.connection : null, }); } @@ -53,6 +55,21 @@ function returnNodeObjectFromRows(rows) { return rows; } +/** + * Returns a connection object from an SQL row + * + * @param {*} row The SQL row to convert to a connection object + * @returns {connectionObject} + */ +async function returnConnectionObjectFromRow(row) { + log.DEBUG("Connection row: ", row); + return new connectionObject({ + _connection_id: row.connection_id, + _node: await getNodeInfoFromId(row.id), + _client_object: await getClientObjectByClientID(row.discord_client_id) + }); +} + /** Get all nodes the server knows about regardless of status * @param {*} callback Callback function */ @@ -71,8 +88,7 @@ exports.getAllNodes = (callback) => { * @returns */ exports.getAllNodesSync = async () => { - const sqlQuery = `SELECT * FROM ${nodesTable}` - var returnObjects = []; + const sqlQuery = `SELECT * FROM ${nodesTable}` const rows = await runSQL(sqlQuery); console.log("Rows: ", rows); @@ -94,20 +110,26 @@ exports.getOnlineNodes = (callback) => { * @param nodeId The ID of the node * @param callback Callback function */ -exports.getNodeInfoFromId = (nodeId, callback) => { + async function getNodeInfoFromId(nodeId, callback = undefined) { const sqlQuery = `SELECT * FROM ${nodesTable} WHERE id = ${nodeId}` - runSQL(sqlQuery, (rows) => { - // Call back the first (and theoretically only) row - // Specify 0 so downstream functions don't have to worry about it - return callback(returnNodeObjectFromRow(rows[0])); - }) + + const sqlResponse = await new Promise((recordResolve, recordReject) => { + runSQL(sqlQuery, (rows) => { + recordResolve(rows); + }) + }); + + // Call back the first (and theoretically only) row + // Specify 0 so downstream functions don't have to worry about it + return (callback) ? callback(returnNodeObjectFromRow(sqlResponse[0])) : returnNodeObjectFromRow(sqlResponse[0]); } +exports.getNodeInfoFromId = getNodeInfoFromId /** Add a new node to the DB * @param nodeObject Node information object * @param callback Callback function */ -exports.addNewNode = (nodeObject, callback) => { +exports.addNewNode = async (nodeObject, callback) => { if (!nodeObject.name) throw new Error("No name provided"); const name = nodeObject.name, ip = nodeObject.ip, @@ -118,16 +140,22 @@ exports.addNewNode = (nodeObject, callback) => { connected = 0; const sqlQuery = `INSERT INTO ${nodesTable} (name, ip, port, location, nearbySystems, online, connected) VALUES ('${name}', '${ip}', ${port}, '${location}', '${nearbySystems}', ${online}, ${connected})`; - runSQL(sqlQuery, (rows) => { - return callback(returnNodeObjectFromRows(rows)); - }) + const sqlResponse = await new Promise((recordResolve, recordReject) => { + runSQL(sqlQuery, (rows) => { + recordResolve(rows); + }) + }); + + // Call back the first (and theoretically only) row + // Specify 0 so downstream functions don't have to worry about it + return (callback) ? callback(returnNodeObjectFromRow(sqlResponse)) : returnNodeObjectFromRow(sqlResponse); } /** Update the known info on a node * @param nodeObject Node information object * @param callback Callback function */ -exports.updateNodeInfo = (nodeObject, callback = undefined) => { +exports.updateNodeInfo = async (nodeObject, callback = undefined) => { if(!nodeObject.id) throw new Error("Attempted to updated node without providing ID", nodeObject); const name = nodeObject.name, ip = nodeObject.ip, @@ -175,21 +203,167 @@ exports.updateNodeInfo = (nodeObject, callback = undefined) => { sqlQuery = `${sqlQuery} WHERE id = ${nodeObject.id};` - runSQL(sqlQuery, (rows) => { - if (rows.affectedRows === 1) return (callback) ? callback(true) : true; - else return (callback) ? callback(returnNodeObjectFromRows(rows)) : returnNodeObjectFromRows(rows); - }) + const sqlResponse = await new Promise((recordResolve, recordReject) => { + runSQL(sqlQuery, (rows) => { + recordResolve(rows); + }) + }); + + if (sqlResponse.affectedRows === 1) return (callback) ? callback(true) : true; + else return (callback) ? callback(returnNodeObjectFromRows(sqlResponse)) : returnNodeObjectFromRows(sqlResponse); +} + +/** + * Add a new connection to the DB when a bot has been connected to the server + * + * @param {*} nodeObject The node object that is being used for this connection + * @param {*} clientId The client ID Object being used for this connection + * @param {*} callback [OPTIONAL] The callback function to be called with the results, will return otherwise + */ +exports.addNodeConnection = (nodeObject, clientObject, callback = undefined) => { + if (!nodeObject.id || !clientObject.clientId) throw new Error("Tried to add a connection without a client and/or node ID"); + const sqlQuery = `INSERT INTO ${nodeConnectionsTable} (id, discord_client_id) VALUES (${nodeObject.id}, '${clientObject.clientId}')`; + + const sqlResponse = new Promise((recordResolve, recordReject) => { + runSQL(sqlQuery, (rows) => { + recordResolve(rows); + }) + }); + + if (!sqlResponse) throw new Error("No result from added connection"); + return (callback) ? callback(true) : true; +} + +/** + * Check what node is connected with a given client ID object + * + * @param {*} clientId The client ID object used to search for a connected node + * @param {*} callback [OPTIONAL] The callback function to be called with the results, return will be used otherwise + */ +exports.checkNodeConnectionByClientId = async (clientId, callback = undefined) => { + if (!clientId.clientId) throw new Error("Tried to check a connection without a client ID"); + const sqlQuery = `SELECT * FROM ${nodeConnectionsTable} WHERE discord_client_id = '${clientId.clientId}'`; + + const sqlResponse = await new Promise((recordResolve, recordReject) => { + runSQL(sqlQuery, (rows) => { + recordResolve(rows); + }) + }); + + log.VERBOSE("SQL Response from checking connection: ", sqlResponse); + + if (!sqlResponse) return (callback) ? callback(undefined) : undefined; + const newNodeObject = await getNodeInfoFromId(sqlResponse[0].id); + log.DEBUG("Node Object from SQL Response: ", newNodeObject); + return (callback) ? callback(newNodeObject) : newNodeObject; +} + +/** + * Get a connection by node ID + * + * @param {*} nodeId The ID to search for a connection with + * @param {*} callback [OPTIONAL] The callback function to be called with the results, return will be used otherwise + * @returns {connectionObject} + */ +exports.getConnectionByNodeId = async (nodeId, callback = undefined) => { + const sqlQuery = `SELECT * FROM ${nodeConnectionsTable} WHERE id = '${nodeId}'`; + + const sqlResponse = await new Promise((recordResolve, recordReject) => { + runSQL(sqlQuery, (rows) => { + recordResolve(rows); + }) + }); + + log.VERBOSE("SQL Response from checking connection: ", sqlResponse); + + if (!sqlResponse) return (callback) ? callback(undefined) : undefined; + const newConnectionObject = await returnConnectionObjectFromRow(sqlResponse) + log.DEBUG("Connection Object from SQL Response: ", newConnectionObject); + return (callback) ? callback(newConnectionObject) : newConnectionObject; +} + +/** + * Remove a node connection by the node + * + * @param {*} nodeId The node ID of the node to remove connections of + * @param {*} callback [OPTIONAL] The callback function to callback with the results, return will be used otherwise + * @returns + */ +exports.removeNodeConnectionByNodeId = async (nodeId, callback = undefined) => { + const sqlQuery = `DELETE FROM ${nodeConnectionsTable} WHERE id = '${nodeId}'`; + + const sqlResponse = await new Promise((recordResolve, recordReject) => { + runSQL(sqlQuery, (rows) => { + recordResolve(rows); + }) + }); + log.VERBOSE("SQL Response from removing connection: ", sqlResponse); + + if (!sqlResponse) return (callback) ? callback(undefined) : undefined; + return (callback) ? callback(sqlResponse) : sqlResponse; +} + +/** + * Gets all connected nodes + * + * @param {*} callback [OPTIONAL] The callback function to callback with the results, return will be used otherwise + * @returns {nodeObject} + */ +exports.getConnectedNodes = async (callback = undefined) => { + const sqlQuery = `SELECT * FROM ${nodeConnectionsTable}`; + + const sqlResponse = await new Promise((recordResolve, recordReject) => { + runSQL(sqlQuery, (rows) => { + recordResolve(rows); + }) + }); + + log.VERBOSE("SQL Response from checking connection: ", sqlResponse); + + if (!sqlResponse) return (callback) ? callback(undefined) : undefined; + var nodeObjects = [] + for (const row of sqlResponse) { + const newNodeObject = await getNodeInfoFromId(row.id); + log.DEBUG("Node Object from SQL Response: ", newNodeObject); + nodeObjects.push(newNodeObject); + } + return (callback) ? callback(nodeObjects) : nodeObjects; +} + +/** + * Returns all connections + * + * @param {*} callback [OPTIONAL] The callback function to callback with the results, return will be used otherwise + * @returns {connectionObject} + */ +exports.getAllConnections = async (callback = undefined) => { + const sqlQuery = `SELECT * FROM ${nodeConnectionsTable}`; + + const sqlResponse = await new Promise((recordResolve, recordReject) => { + runSQL(sqlQuery, (rows) => { + recordResolve(rows); + }) + }); + + log.VERBOSE("SQL Response from checking connection: ", sqlResponse); + + if (!sqlResponse) return (callback) ? callback(undefined) : undefined; + var connectionObjects = [] + for (const row of sqlResponse) { + connectionObjects.push(await returnConnectionObjectFromRow(row)); + } + return (callback) ? callback(connectionObjects) : connectionObjects; } // Function to run and handle SQL errors -function runSQL(sqlQuery, callback, error = (err) => { +function runSQL(sqlQuery, callback = undefined, error = (err) => { console.log(err); throw err; }) { - return connection.query(sqlQuery, (err, rows) => { + connection.query(sqlQuery, (err, rows) => { if (err) return error(err); - //console.log('The rows are:', rows); - return callback(rows); + //console.log('The rows are:', rows); + return (callback) ? callback(rows) : rows }) } diff --git a/Server/utilities/recordHelper.js b/Server/utilities/recordHelper.js index f8050e7..b8c9135 100644 --- a/Server/utilities/recordHelper.js +++ b/Server/utilities/recordHelper.js @@ -111,9 +111,10 @@ class nodeObject { * @param {*} param0._location The physical location of the node * @param {*} param0._online True/False if the node is online or offline * @param {*} param0._connected True/False if the bot is connected to discord or not + * @param {*} param0._connection The connection Object associated with the node, null if not checked, undefined if none exists * @param {*} param0._nearbySystems An object array of nearby systems */ - constructor({ _id = null, _name = null, _ip = null, _port = null, _location = null, _nearbySystems = null, _online = null, _connected = null }) { + constructor({ _id = null, _name = null, _ip = null, _port = null, _location = null, _nearbySystems = null, _online = null, _connected = null, _connection = null }) { this.id = _id; this.name = _name; this.ip = _ip; @@ -122,7 +123,46 @@ class nodeObject { this.nearbySystems = _nearbySystems; this.online = _online; this.connected = _connected; + this.connection = _connection; } } -exports.nodeObject = nodeObject; \ No newline at end of file +exports.nodeObject = nodeObject; + +/** + * This object represents a discord bot's client information + */ +class clientObject { + /** + * + * @param {*} param0._discord_id The discord id from the node, as seen when right clicking -> copy ID + * @param {*} param0._name The name of the bot associated with the IDs + * @param {*} param0._client_id The client ID of the bot needed to connect to Discord + */ + constructor({_discord_id = null, _name = null, _client_id = null,}) { + this.discordId = _discord_id; + this.name = _name; + this.clientId = _client_id; + } +} + +exports.clientObject = clientObject; + +/** + * This object represents a discord node connection + */ +class connectionObject { + /** + * + * @param {*} param0._connection_id The connection ID associated with the connection in the database + * @param {*} param0._node The node associated with the connection + * @param {*} param0._client_object The client object associated with the connection + */ + constructor({_connection_id = null, _node = null, _client_object}) { + this.connectionId = _connection_id; + this.node = _node; + this.clientObject = _client_object; + } +} + +exports.connectionObject = connectionObject; \ No newline at end of file diff --git a/Server/utilities/utils.js b/Server/utilities/utils.js index e0d71bd..259c7cb 100644 --- a/Server/utilities/utils.js +++ b/Server/utilities/utils.js @@ -1,5 +1,6 @@ // Debug const { DebugBuilder } = require("../utilities/debugBuilder"); +const { clientObject } = require("./recordHelper"); const { readFileSync } = require('fs'); const log = new DebugBuilder("server", "utils"); const path = require('path'); @@ -32,8 +33,11 @@ exports.SanitizePresetName = (presetName) => { */ exports.getMembersInRole = async (interaction, roleName = "Bots" ) => { log.DEBUG("Fetching all members"); - await interaction.guild.members.fetch() //cache all members in the server - const role = await interaction.guild.roles.cache.find(role => role.name === roleName); //the role to check + var guild = await interaction.client.guilds.fetch({ guild: interaction.guild.id, cache: false }); //cache all members in the server + await guild.members.fetch({cache: false}); + await guild.roles.fetch({cache: false}); + log.VERBOSE("Guild: ", guild); + const role = await guild.roles.cache.find(role => role.name === roleName); //the role to check log.DEBUG("Role to check members from: ", role); log.DEBUG("Members of role: ", role.members); @@ -58,7 +62,9 @@ exports.getMembersInRole = async (interaction, roleName = "Bots" ) => { * @returns The key of the object that contains the value */ exports.getKeyByArrayValue = (object, value) => { - return Object.keys(object).find(key => object[key].includes(value)); + if (typeof value == "string") return Object.keys(object).find(key => object[key].includes(value)); + const valueKey = Object.keys(value)[0]; + return Object.keys(object).find(key => (object[key][valueKey] == value[valueKey])); } /** @@ -82,5 +88,32 @@ exports.isJsonString = (str) => { * @returns Object of Client IDs */ exports.getAllClientIds = () => { - return Object(JSON.parse(readFileSync(path.resolve(__dirname, '../clientIds.json')))); + const jsonClientIds = JSON.parse(readFileSync(path.resolve(__dirname, '../clientIds.json'))); + var clientObjects = []; + for (const jsonClientId of Object.keys(jsonClientIds)){ + clientObjects.push(new clientObject({ + _discord_id: jsonClientId, + _name: jsonClientIds[jsonClientId].name, + _client_id: jsonClientIds[jsonClientId].id + })) + } + return clientObjects; +} + +/** + * Gets a client object froma discord client ID + * + * @param {*} clientId The discord client ID to get the client object of + * @returns {clientObject|undefined} + */ +exports.getClientObjectByClientID = (clientId) => { + const clientObjects = this.getAllClientIds(); + log.DEBUG("All client IDs: ", clientObjects); + for (const clientObject of clientObjects){ + if (clientObject.clientId == clientId) { + log.DEBUG("Found client ID from given ID: ", clientObject); + return clientObject + } + } + return undefined } \ No newline at end of file