#37 Working Joining and Leaving

This commit is contained in:
Logan Cusano
2023-07-16 01:43:13 -04:00
committed by logan
parent c5f7cc1da6
commit f55361575e
9 changed files with 285 additions and 142 deletions

View File

@@ -1,98 +1,13 @@
// Modules
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const { getMembersInRole, getAllClientIds, filterAutocompleteValues, getKeyByArrayValue } = require("../utilities/utils");
const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
const { getOnlineNodes, updateNodeInfo, addNodeConnection, getConnectionByNodeId, getAllConnections } = require("../utilities/mysqlHandler");
const { filterAutocompleteValues } = require("../utilities/utils");
const { getOnlineNodes, getAllConnections } = require("../utilities/mysqlHandler");
const { joinServerWrapper } = require("../controllers/adminController");
// 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 {*} connections EITHER A collection of clients that are currently connected OR a single discord client ID (NOT dev portal ID) that should be used to join the server with
* @param {number} nodeId [OPTIONAL] The node ID to join with (will join with another node if given node is not available)
* @returns
*/
async function joinServerWrapper(presetName, channelId, connections, nodeId = 0) {
// Get nodes online
var onlineNodes = await new Promise((recordResolve, recordReject) => {
getOnlineNodes((nodeRows) => {
recordResolve(nodeRows);
});
});
// Check which nodes have the selected preset
onlineNodes = onlineNodes.filter(node => node.presets.includes(presetName));
log.DEBUG("Filtered Online Nodes: ", onlineNodes);
// Check if any nodes with this preset are available
var nodesCurrentlyAvailable = [];
for (const node of onlineNodes) {
const currentConnection = await getConnectionByNodeId(node.id);
log.DEBUG("Checking to see if there is a connection for Node: ", node, currentConnection);
if(!currentConnection) nodesCurrentlyAvailable.push(node);
}
log.DEBUG("Nodes Currently Available: ", nodesCurrentlyAvailable);
// If not, let the user know
if (!nodesCurrentlyAvailable.length > 0) return Error("All nodes with this channel are unavailable, consider swapping one of the currently joined bots.");
// If so, join with the first node
var availableClientIds = await getAllClientIds();
log.DEBUG("All clients: ", Object.keys(availableClientIds));
var selectedClientId;
if (typeof connections === 'string') {
for (const availableClientId of availableClientIds) {
if (availableClientId.discordId != connections ) selectedClientId = availableClientId;
}
}
else {
log.DEBUG("Open connections: ", connections);
for (const connection of connections) {
log.DEBUG("Used Client ID: ", connection);
availableClientIds = availableClientIds.filter(cid => cid.discordId != connection.clientObject.discordId);
}
log.DEBUG("Available Client IDs: ", availableClientIds);
if (!Object.keys(availableClientIds).length > 0) return log.ERROR("All client ID have been used, consider swapping one of the curretly joined bots or adding more Client IDs to the pool.")
selectedClientId = availableClientIds[0];
}
let selectedNode;
if (nodeId > 0) selectedNode = getKeyByArrayValue(nodesCurrentlyAvailable, nodeId);
if (!selectedNode) selectedNode = nodesCurrentlyAvailable[0];
const reqOptions = new requestOptions("/bot/join", "POST", selectedNode.ip, selectedNode.port);
const postObject = {
"channelId": channelId,
"clientId": selectedClientId.clientId,
"presetName": presetName
};
log.INFO("Post Object: ", postObject);
sendHttpRequest(reqOptions, JSON.stringify(postObject), async (responseObj) => {
log.VERBOSE("Response Object from node ", selectedNode, responseObj);
if (!responseObj || !responseObj.statusCode == 200) return false;
// Node has connected to discord
// Updating node Object in DB
const updatedNode = await updateNodeInfo(selectedNode);
log.DEBUG("Updated Node: ", updatedNode);
// Adding a new node connection
const nodeConnection = await addNodeConnection(selectedNode, selectedClientId);
log.DEBUG("Node Connection: ", nodeConnection);
});
return selectedClientId;
}
exports.joinServerWrapper = joinServerWrapper;
module.exports = {
data: new SlashCommandBuilder()
.setName('join')

View File

@@ -2,41 +2,12 @@
const { SlashCommandBuilder } = require('discord.js');
const { DebugBuilder } = require("../utilities/debugBuilder");
const { getAllClientIds, getKeyByArrayValue, filterAutocompleteValues } = require("../utilities/utils");
const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
const { checkNodeConnectionByClientId, removeNodeConnectionByNodeId, getAllConnections } = require('../utilities/mysqlHandler');
const { getAllConnections } = require('../utilities/mysqlHandler');
const { leaveServerWrapper } = require('../controllers/adminController');
// Global Vars
const log = new DebugBuilder("server", "leave");
/**
*
* @param {*} clientIdObject The client ID object for the node to leave the server. Either 'clientId'||'name' can be set.
* @returns
*/
async function leaveServerWrapper(clientIdObject) {
if (!clientIdObject.clientId || !clientIdObject.name) return log.ERROR("Tried to leave server without client ID and/or Name");
const node = await checkNodeConnectionByClientId(clientIdObject);
reqOptions = new requestOptions("/bot/leave", "POST", node.ip, node.port);
const responseObj = await new Promise((recordResolve, recordReject) => {
sendHttpRequest(reqOptions, JSON.stringify({}), async (responseObj) => {
recordResolve(responseObj);
});
});
log.VERBOSE("Response Object from node ", node, responseObj);
if (!responseObj || !responseObj.statusCode == 202) return false;
// Node has disconnected from discor
// Removing the node connection from the DB
const removedConnection = removeNodeConnectionByNodeId(node.id);
log.DEBUG("Removed Node Connection: ", removedConnection);
return;
}
exports.leaveServerWrapper = leaveServerWrapper;
module.exports = {
data: new SlashCommandBuilder()
.setName('leave')
@@ -64,7 +35,6 @@ module.exports = {
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

View File

@@ -4,12 +4,9 @@ require('dotenv').config();
const { DebugBuilder } = require("../utilities/debugBuilder.js");
const log = new DebugBuilder("server", "adminController");
// Utilities
const mysqlHandler = require("../utilities/mysqlHandler");
const utils = require("../utilities/utils");
const requests = require("../utilities/httpRequests");
const { leaveServerWrapper } = require("../commands/leave");
const { joinServerWrapper } = require("../commands/join");
const { getAllClientIds } = require("../utilities/utils");
const { getOnlineNodes, updateNodeInfo, addNodeConnection, getConnectionByNodeId, getNodeInfoFromId, checkNodeConnectionByClientId, removeNodeConnectionByNodeId } = require("../utilities/mysqlHandler");
const { requestOptions, sendHttpRequest } = require("../utilities/httpRequests");
/** Get the presets of all online nodes, can be used for functions
*
@@ -17,15 +14,15 @@ const { joinServerWrapper } = require("../commands/join");
* @returns {*} A list of the systems online
*/
async function getPresetsOfOnlineNodes(callback) {
mysqlHandler.getOnlineNodes((onlineNodes) => {
getOnlineNodes((onlineNodes) => {
return callback(onlineNodes);
});
}
async function getNodeBotStatus(nodeId, callback) {
mysqlHandler.getNodeInfoFromId(nodeId, (nodeObject) =>{
reqOptions = new requests.requestOptions("/bot/status", "GET", nodeObject.ip, nodeObject.port, undefined, 5);
requests.sendHttpRequest(reqOptions, JSON.stringify({}), (responseObject) => {
getNodeInfoFromId(nodeId, (nodeObject) =>{
reqOptions = new requestOptions("/bot/status", "GET", nodeObject.ip, nodeObject.port, undefined, 5);
sendHttpRequest(reqOptions, JSON.stringify({}), (responseObject) => {
if (responseObject === false) {
// Bot is joined
}
@@ -73,21 +70,141 @@ exports.joinPreset = async (req, res) => {
const joinedClient = await joinServerWrapper(preset, channelId, clientId, nodeId);
if (!joinedClient) return res.send(400).json("No joined client");
return res.send(200).json(joinedClient);
return res.status(200).json(joinedClient);
}
/** Request a node to join the server listening to a specific preset
*
* @param {*} req Express request parameter
* @param {*} res Express response parameter
* @var {*} req.body.clientId The ID of the client to disconnect
* @var {*} req.body.nodeId The ID of the node to disconnect
*/
exports.leaveServer = async (req, res) => {
if (!req.body.clientId) return res.status(400).json("No clientID specified");
if (!req.body.nodeId) return res.status(400).json("No node ID specified");
const clientId = req.body.clientId;
const nodeId = req.body.nodeId;
const currentConnection = await getConnectionByNodeId(nodeId);
log.DEBUG("Current Connection for node: ", currentConnection);
await leaveServerWrapper(clientId)
await leaveServerWrapper(currentConnection.clientObject)
return res.send(200);
}
return res.status(200).json(currentConnection.clientObject.name);
}
/**
* * This wrapper will check if there is an available node with the requested preset and if so checks for an available client ID to join with
*
* @param {*} presetName The preset name to listen to on the client
* @param {*} channelId The channel ID to join the bot to
* @param {*} connections EITHER A collection of clients that are currently connected OR a single discord client ID (NOT dev portal ID) that should be used to join the server with
* @param {number} nodeId [OPTIONAL] The node ID to join with (will join with another node if given node is not available)
* @returns
*/
async function joinServerWrapper(presetName, channelId, connections, nodeId = 0) {
// Get nodes online
var onlineNodes = await new Promise((recordResolve, recordReject) => {
getOnlineNodes((nodeRows) => {
recordResolve(nodeRows);
});
});
// Check which nodes have the selected preset
onlineNodes = onlineNodes.filter(node => node.presets.includes(presetName));
log.DEBUG("Filtered Online Nodes: ", onlineNodes);
// Check if any nodes with this preset are available
var nodesCurrentlyAvailable = [];
for (const node of onlineNodes) {
const currentConnection = await getConnectionByNodeId(node.id);
log.DEBUG("Checking to see if there is a connection for Node: ", node, currentConnection);
if(!currentConnection) nodesCurrentlyAvailable.push(node);
}
log.DEBUG("Nodes Currently Available: ", nodesCurrentlyAvailable);
// If not, let the user know
if (!nodesCurrentlyAvailable.length > 0) return Error("All nodes with this channel are unavailable, consider swapping one of the currently joined bots.");
// If so, join with the first node
var availableClientIds = await getAllClientIds();
log.DEBUG("All clients: ", Object.keys(availableClientIds));
var selectedClientId;
if (typeof connections === 'string') {
for (const availableClientId of availableClientIds) {
if (availableClientId.discordId != connections ) selectedClientId = availableClientId;
}
}
else {
log.DEBUG("Open connections: ", connections);
for (const connection of connections) {
log.DEBUG("Used Client ID: ", connection);
availableClientIds = availableClientIds.filter(cid => cid.discordId != connection.clientObject.discordId);
}
log.DEBUG("Available Client IDs: ", availableClientIds);
if (!Object.keys(availableClientIds).length > 0) return log.ERROR("All client ID have been used, consider swapping one of the curretly joined bots or adding more Client IDs to the pool.")
selectedClientId = availableClientIds[0];
}
let selectedNode;
if (nodeId > 0) {
for(const availableNode of nodesCurrentlyAvailable){
if (availableNode.id == nodeId) selectedNode = availableNode;
}
}
if (!selectedNode) selectedNode = nodesCurrentlyAvailable[0];
const reqOptions = new requestOptions("/bot/join", "POST", selectedNode.ip, selectedNode.port);
const postObject = {
"channelId": channelId,
"clientId": selectedClientId.clientId,
"presetName": presetName
};
log.INFO("Post Object: ", postObject);
sendHttpRequest(reqOptions, JSON.stringify(postObject), async (responseObj) => {
log.VERBOSE("Response Object from node ", selectedNode, responseObj);
if (!responseObj || !responseObj.statusCode == 200) return false;
// Node has connected to discord
// Updating node Object in DB
const updatedNode = await updateNodeInfo(selectedNode);
log.DEBUG("Updated Node: ", updatedNode);
// Adding a new node connection
const nodeConnection = await addNodeConnection(selectedNode, selectedClientId);
log.DEBUG("Node Connection: ", nodeConnection);
});
return selectedClientId;
}
exports.joinServerWrapper = joinServerWrapper;
/**
*
* @param {*} clientIdObject The client ID object for the node to leave the server. Either 'clientId'||'name' can be set.
* @returns
*/
async function leaveServerWrapper(clientIdObject) {
if (!clientIdObject.clientId || !clientIdObject.name) return log.ERROR("Tried to leave server without client ID and/or Name");
const node = await checkNodeConnectionByClientId(clientIdObject);
reqOptions = new requestOptions("/bot/leave", "POST", node.ip, node.port);
const responseObj = await new Promise((recordResolve, recordReject) => {
sendHttpRequest(reqOptions, JSON.stringify({}), async (responseObj) => {
recordResolve(responseObj);
});
});
log.VERBOSE("Response Object from node ", node, responseObj);
if (!responseObj || !responseObj.statusCode == 202) return false;
// Node has disconnected from discor
// Removing the node connection from the DB
const removedConnection = removeNodeConnectionByNodeId(node.id);
log.DEBUG("Removed Node Connection: ", removedConnection);
return;
}
exports.leaveServerWrapper = leaveServerWrapper;

View File

@@ -192,7 +192,8 @@ exports.requestNodeCheckIn = async (req, res) => {
if (!req.params.nodeId) return res.status(400).json("No Node ID supplied in request");
const node = await getNodeInfoFromId(req.params.nodeId);
if (!node) return res.status(400).json("No Node with the ID given");
checkInWithNode(node);
await checkInWithNode(node);
res.sendStatus(200);
}
/**

View File

@@ -38,6 +38,48 @@ function addFrequencyInput(system){
document.getElementById(`frequencyRow_${system.replaceAll(" ", "_")}`).appendChild(colParent);
}
function createToast(notificationMessage){
const toastTitle = document.createElement('strong');
toastTitle.classList.add('me-auto');
toastTitle.appendChild(document.createTextNode("Server Notification"));
const toastTime = document.createElement('small');
toastTime.appendChild(document.createTextNode(Date.now().toLocaleString()));
const toastClose = document.createElement('button');
toastClose.type = 'button';
toastClose.classList.add('btn-close');
toastClose.ariaLabel = 'Close';
toastClose.setAttribute('data-bs-dismiss', 'toast');
const toastHeader = document.createElement('div');
toastHeader.classList.add('toast-header');
toastHeader.appendChild(toastTitle);
toastHeader.appendChild(toastTime);
toastHeader.appendChild(toastClose);
const toastMessage = document.createElement('p');
toastMessage.classList.add("px-2");
toastMessage.appendChild(document.createTextNode(notificationMessage));
const toastBody = document.createElement('div');
toastBody.classList.add('toast-body');
toastBody.appendChild(toastMessage);
const wrapperDiv = document.createElement('div');
wrapperDiv.classList.add('toast');
wrapperDiv.role ='alert';
wrapperDiv.ariaLive = 'assertive';
wrapperDiv.ariaAtomic = true;
wrapperDiv.appendChild(toastHeader);
wrapperDiv.appendChild(toastMessage);
document.getElementById("toastZone").appendChild(wrapperDiv);
$('.toast').toast('show');
}
function checkInByNodeId(nodeId){
const Http = new XMLHttpRequest();
const url='/nodes/'+nodeId;
@@ -46,5 +88,56 @@ function checkInByNodeId(nodeId){
Http.onreadystatechange = (e) => {
console.log(Http.responseText)
createToast(Http.responseText);
}
}
function joinServer(){
const preset = document.getElementById("selectRadioPreset").value;
const nodeId = document.getElementById("nodeId").value;
const clientId = document.getElementById("inputDiscordClientId").value;
const channelId = document.getElementById("inputDiscordChannelId").value;
const reqBody = {
'preset': preset,
'nodeId': nodeId,
'clientId': clientId,
'channelId': channelId
};
console.log(reqBody);
const Http = new XMLHttpRequest();
const url='/admin/join';
Http.open("POST", url);
Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody));
Http.onloadend = (e) => {
const responseObject = JSON.parse(Http.responseText)
console.log(Http.status);
console.log(responseObject);
createToast(`${responseObject.name} will join shortly`);
$("#joinModal").modal('toggle');
}
}
function leaveServer(){
const nodeId = document.getElementById("nodeId").value;
const reqBody = {
'nodeId': nodeId
};
const Http = new XMLHttpRequest();
const url='/admin/leave';
Http.open("POST", url);
Http.setRequestHeader("Content-Type", "application/json");
Http.send(JSON.stringify(reqBody));
Http.onloadend = (e) => {
const responseObject = JSON.parse(Http.responseText)
console.log(Http.status);
console.log(responseObject);
createToast(`${responseObject} is leaving`);
}
}

View File

@@ -118,7 +118,8 @@ class nodeObject {
this.ip = _ip;
this.port = _port;
this.location = _location;
this.nearbySystems = _nearbySystems;
this.nearbySystems = _nearbySystems;
if (this.nearbySystems) this.presets = Object.keys(_nearbySystems);
this.online = _online;
}
}

View File

@@ -9,7 +9,7 @@
</p>
</div>
<div class="card-body">
<form>
<form>
<div class="row gx-3 mb-3">
<div class="">
@@ -26,9 +26,9 @@
<% } %>
<hr>
<!-- Join Server button-->
<a type="button" class="btn btn-info <% if(!node.online) { %>disabled<% } %>" href="/join/<%=node.id%>">Join Server</a>
<a type="button" class="btn btn-info <% if(!node.online) { %>disabled<% } %>" data-bs-toggle="modal" data-bs-target="#joinModal" href="#">Join Server</a>
<!-- Leave Server button -->
<a type="button" class="btn btn-danger <% if(!node.online) { %>disabled<% } %>" href="/leave/<%=node.id%>">Leave Server</a>
<a type="button" class="btn btn-danger <% if(!node.online) { %>disabled<% } %>" href="#" onclick="leaveServer()">Leave Server</a>
<!-- Checkin with client button -->
<a type="button" class="btn btn-secondary" href="#" onclick="checkInByNodeId('<%=node.id%>')">Check-in with Node</a>
</div>
@@ -149,6 +149,7 @@
</div>
</div>
</div>
<%- include('partials/joinModal.ejs', {'nearbySystems': node.nearbySystems}) %>
<%- include('partials/bodyEnd.ejs') %>
<script src="/res/js/node.js"></script>
<%- include('partials/htmlFooter.ejs') %>

View File

@@ -2,5 +2,6 @@
<html lang="en" data-bs-theme="auto">
<%- include('head.ejs') %>
<body>
<div class="toast-container mx-2" id="toastZone"></div>
<%- include('navbar.ejs') %>
<%- include('sidebar.ejs') %>

View File

@@ -0,0 +1,44 @@
<div class="modal fade" id="joinModal" tabindex="-1" aria-labelledby="joinModal" aria-hidden="true">
<div class="modal-dialog modal-dialog-centered">
<div class="modal-content">
<div class="modal-header">
<h1 class="modal-title fs-5" id="joinModal">Join Node <%=node.id%> to a Discord Server</h1>
<button type="button" class="btn-close" data-bs-dismiss="modal" aria-label="Close"></button>
</div>
<div class="modal-body">
<div class="container">
<div class="card mb-4">
<div class="card-body">
<form>
<div class="row gx-3 mb-3">
<div class="col-md-12">
<label class="small mb-1" for="inputDiscordClientId">Discord Client ID:</label>
<input class="form-control" id="inputDiscordClientId" type="text" value="" required></input>
</div>
</div>
<div class="row gx-3 mb-3">
<div class="col-md-6">
<label class="small mb-1" for="inputDiscordChannelId">Discord Channel ID:</label>
<input class="form-control" id="inputDiscordChannelId" type="text" value="" required></input>
</div>
<div class="col-md-6">
<label class="small mb-1" for="selectRadioPreset">Selected Preset:</label>
<select class="custom-select" id="selectRadioPreset">
<% for(const system in nearbySystems) { %>
<option value="<%=system%>"><%=system%></option>
<% } %>
</select>
</div>
</div>
</form>
</div>
</div>
</div>
</div>
<div class="modal-footer">
<button type="button" class="btn btn-secondary" data-bs-dismiss="modal">Cancel</button>
<button type="button" class="btn btn-primary" onclick="joinServer()">Join</button>
</div>
</div>
</div>
</div>