From b51300d878cc9a379dc1a671eaecf49f68a18a9d Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Sun, 14 Jul 2024 19:26:17 -0400 Subject: [PATCH] Improved joining and leaving - Added wrappers - Improved readability of command code --- discordBot/commands/join.mjs | 146 ++++++----------------- discordBot/commands/leave.mjs | 40 ++++--- discordBot/modules/wrappers.mjs | 203 +++++++++++++++++++++++++------- 3 files changed, 225 insertions(+), 164 deletions(-) diff --git a/discordBot/commands/join.mjs b/discordBot/commands/join.mjs index a7784eb..928b3a2 100644 --- a/discordBot/commands/join.mjs +++ b/discordBot/commands/join.mjs @@ -1,9 +1,9 @@ import { DebugBuilder } from "../../modules/debugger.mjs"; +import { SlashCommandBuilder } from 'discord.js'; +import { joinNode, getAvailableNodes, promptNodeSelection, getUserVoiceChannel } from '../modules/wrappers.mjs'; +import { getAllSystems, getSystemByName } from '../../modules/mongo-wrappers/mongoSystemsWrappers.mjs'; + const log = new DebugBuilder("server", "discordBot.command.join"); -import { SlashCommandBuilder, ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; -import { requestNodeJoinSystem, checkIfNodeIsConnectedToVC, checkIfNodeHasOpenDiscordClient, getNodeCurrentListeningSystem } from '../../modules/socketServerWrappers.mjs'; -import { getSystemsByNuid, getAllSystems, getSystemByName } from '../../modules/mongo-wrappers/mongoSystemsWrappers.mjs'; -import { getAvailableTokensInGuild } from '../modules/wrappers.mjs'; // Exporting data property export const data = new SlashCommandBuilder() @@ -13,7 +13,8 @@ export const data = new SlashCommandBuilder() system.setName('system') .setDescription('The radio system you would like to listen to') .setRequired(true) - .setAutocomplete(true)); + .setAutocomplete(true) + ); // Exporting other properties export const example = "/join"; @@ -32,125 +33,56 @@ export async function autocomplete(nodeIo, interaction) { log.DEBUG(focusedValue, choices, filtered); await interaction.respond( - filtered.map(choice => ({ name: choice.name, value: choice.name })), + filtered.map(choice => ({ name: choice.name, value: choice.name })) ); } /** - * The function to run when the command is called by a discord user + * Handle join command execution * @param {any} nodeIo The nodeIO server for manipulation of sockets * @param {any} interaction The interaction object */ export async function execute(nodeIo, interaction) { - // Check if the user is in a VC - if (!interaction.member.voice.channel) { return await interaction.editReply({ content: `<@${interaction.member.id}>, you need to enter a voice channel before you use this command`, ephemeral: true }) } - // Grab the channel if the user is connected to VC - const channelToJoin = interaction.member.voice.channel; - log.INFO(`The user '${interaction.member.id}' is in the voice channel '${channelToJoin}'`); - - // Get the selected system option from the command interaction - const selectedSystem = interaction.options.getString('system'); - try { - // Get the selected system object from the DB - const system = await getSystemByName(selectedSystem); + // Validate user is in a voice channel + const channelToJoin = getUserVoiceChannel(interaction); + if (!channelToJoin) return; - // Function wrapper to request the selected/only node to join the selected system - const joinSelectedNode = async (selectedNodeSocketId) => { - const openSocket = await nodeIo.sockets.sockets.get(selectedNodeSocketId); - // Get the open ID for this connection\ - const discordTokens = await getAvailableTokensInGuild(nodeIo, interaction.guild.id); - log.DEBUG("Available discord tokens: ", discordTokens); + // Get the selected system + const selectedSystemName = interaction.options.getString('system'); + const system = await getSystemByName(selectedSystemName); - if (discordTokens.length >= 1) { - // TODO - Implement a method to have preferred tokens (bot users) for specific systems - log.INFO("Joining selected open socket:", selectedNodeSocketId, system.name, channelToJoin.id, openSocket.node.name, discordTokens[0].token); - - // Ask the node to join the selected channel and system - await requestNodeJoinSystem(openSocket, system.name, channelToJoin.id, discordTokens[0].token); - } - else { - return await interaction.editReply({ content: `<@${interaction.member.id}>, there are no free bots. Free up or create a new bot ID (discord app) to listen to this system.`, ephemeral: true }) - } + // Check if there was a system found by the given system name + if (!system) { + await interaction.editReply({ content: `System '${selectedSystemName}' not found.`, ephemeral: true }); + return; } - // Get all open socket nodes - const openSockets = [...await nodeIo.allSockets()]; // TODO - Filter the returned nodes to only nodes that have the radio capability - log.DEBUG("All open sockets: ", openSockets); + // Get the available nodes for this system + const availableNodes = await getAvailableNodes(nodeIo, interaction.guild.id, system); - var availableNodes = []; - // Check each open socket to see if the node has the requested system - await Promise.all(openSockets.map(async openSocket => { - openSocket = await nodeIo.sockets.sockets.get(openSocket); - // Check if the node has an existing open client (meaning the radio is already being listened to) - const hasOpenClient = await checkIfNodeHasOpenDiscordClient(openSocket); - if (hasOpenClient) { - let currentSystem = await getNodeCurrentListeningSystem(openSocket); - if (currentSystem != system.name) { - log.INFO("Node is listening to a different system than requested", openSocket.node.name); - return; - } - } + // Check if there are available nodes + if (availableNodes.length === 0) { + // If not, let the user know + await interaction.editReply(`<@${interaction.member.id}>, the selected system has no available nodes`); + return; + } - // Check if the bot has an open voice connection in the requested server already - const connected = await checkIfNodeIsConnectedToVC(nodeIo, interaction.guild.id, openSocket.node.nuid); - log.INFO("Connected:", connected); - if (!connected) { - // Check if this node has the requested system, if so add it to the availble array - if (system.nodes.includes(openSocket.node.nuid)) { - availableNodes.push(openSocket); - } - } + // If there is one available node, request that node join + if (availableNodes.length === 1) { + await joinNode(nodeIo, interaction, availableNodes[0].id, system, channelToJoin); + } - })); - - log.DEBUG("Availble nodes:", availableNodes.map(socket => socket.node.name)); - - // If there are no available nodes, let the user know there are none available - if (availableNodes.length == 0) { - // There are no nodes availble for the requested system - return await interaction.editReply(`<@${interaction.member.id}>, the selected system has no available nodes`); - } else if (availableNodes.length == 1) { - // There is only one node available for the requested system - // Request the node to join - await joinSelectedNode(availableNodes[0].id); - // Let the user know - await interaction.editReply({ content: `Ok <@${interaction.member.id}>, a bot will join your channel listening to *'${system.name}'* shortly`, components: [] }); - } else if (availableNodes.length > 1) { - // There is more than one node availble for the requested system - const nodeSelectionButtons = [] - - // Create a button for each available node - for (const availableNode of availableNodes) { - nodeSelectionButtons.push(new ButtonBuilder().setCustomId(availableNode.id).setLabel(availableNode.node.name).setStyle(ButtonStyle.Primary)); - } - - const actionRow = new ActionRowBuilder().addComponents(nodeSelectionButtons); - - // Reply to the user with the button prompts - const response = await interaction.editReply({ - content: `<@${interaction.member.id}>, Please select the Node you would like to join with this system`, - components: [actionRow] + // If there are more than one available, prompt the user for their selected node + else { + await promptNodeSelection(interaction, availableNodes, async selectedNode => { + await joinNode(nodeIo, interaction, selectedNode, system, channelToJoin); }); - - // Make sure the responding selection is from the user who initiated the command - const collectorFilter = i => i.user.id === interaction.user.id; - - // Wait for the confirmation from the user on which node to join - try { - const selectedNode = await response.awaitMessageComponent({ filter: collectorFilter, time: 60_000 }); - // Run the local wrapper to listen to the selected node - await joinSelectedNode(selectedNode.customId); - // Let the user know - await selectedNodeConfirmation.update({ content: `Ok <@${interaction.member.id}>, a bot will join your channel listening to *'${system.name}'*`, components: [] }); - } catch (e) { - console.error(e); - // Timeout the prompt if the user doesn't interact with it - await interaction.editReply({ content: 'Confirmation not received within 1 minute, cancelling', components: [] }); - } } - } catch (err) { - console.error(err); - // await interaction.reply(err.toString()); + } + + catch (err) { + log.ERROR(err); + await interaction.editReply({ content: `An error occurred: ${err.message}`, ephemeral: true }); } } \ No newline at end of file diff --git a/discordBot/commands/leave.mjs b/discordBot/commands/leave.mjs index a993e13..65f0d00 100644 --- a/discordBot/commands/leave.mjs +++ b/discordBot/commands/leave.mjs @@ -1,8 +1,9 @@ import { DebugBuilder } from "../../modules/debugger.mjs"; -const log = new DebugBuilder("server", "discordBot.command.leave"); import { SlashCommandBuilder } from 'discord.js'; import { requestBotLeaveServer, getSocketIdByNuid } from '../../modules/socketServerWrappers.mjs'; -import { checkOnlineBotsInGuild } from '../modules/wrappers.mjs' +import { checkOnlineBotsInGuild } from '../modules/wrappers.mjs'; + +const log = new DebugBuilder("server", "discordBot.command.leave"); // Exporting data property export const data = new SlashCommandBuilder() @@ -12,7 +13,8 @@ export const data = new SlashCommandBuilder() system.setName('bot') .setDescription('The bot you would like to disconnect') .setRequired(true) - .setAutocomplete(true));; + .setAutocomplete(true) + ); // Exporting other properties export const example = "/leave *{Bot Name}*"; @@ -25,15 +27,22 @@ export const deferInitialReply = true; */ export async function autocomplete(nodeIo, interaction) { const focusedValue = interaction.options.getFocused(); - const choices = (await checkOnlineBotsInGuild(nodeIo, interaction.guild.id)); + const choices = await checkOnlineBotsInGuild(nodeIo, interaction.guild.id); log.DEBUG(choices); - const filtered = choices.filter(choice => choice.name.startsWith(focusedValue)).map(choice => choice = {name: choice.name, value: choice.nuid}); + const filtered = choices + .filter(choice => choice.name.startsWith(focusedValue)) + .map(choice => ({ name: choice.name, value: choice.nuid })); log.DEBUG(focusedValue, choices, filtered); - await interaction.respond(filtered); + try{ + await interaction.respond(filtered); + } + catch (e) { + log.WARN("Autocomplete interaction failure", e); + } } /** @@ -43,16 +52,19 @@ export async function autocomplete(nodeIo, interaction) { */ export async function execute(nodeIo, interaction) { try { - // Get the requested bot const selectedNode = interaction.options.getString('bot'); const socket = await getSocketIdByNuid(nodeIo, selectedNode); - log.DEBUG("All open sockets:", socket, selectedNode); + + if (!socket) { + await interaction.editReply({ content: `Bot '${selectedNode}' not found or not connected.`, ephemeral: true }); + return; + } + await requestBotLeaveServer(socket, interaction.guild.id); - //await interaction.reply(`**Online Sockets: '${sockets}'**`); - await interaction.editReply(`Ok <@${interaction.member.id}>, the bot is leaving shortly`); - //await interaction.channel.send('**Pong.**'); + + await interaction.editReply(`Ok <@${interaction.member.id}>, the bot is leaving shortly.`); } catch (err) { - console.error(err); - // await interaction.reply(err.toString()); + log.ERROR("Failed to disconnect bot:", err); + await interaction.editReply({ content: `An error occurred: ${err.message}`, ephemeral: true }); } -} \ No newline at end of file +} diff --git a/discordBot/modules/wrappers.mjs b/discordBot/modules/wrappers.mjs index 652b6b3..62cc623 100644 --- a/discordBot/modules/wrappers.mjs +++ b/discordBot/modules/wrappers.mjs @@ -1,50 +1,167 @@ import { DebugBuilder } from "../../modules/debugger.mjs"; const log = new DebugBuilder("server", "discordBot.modules.wrappers"); -import { checkIfNodeIsConnectedToVC, getNodeDiscordID, getNodeDiscordUsername } from '../../modules/socketServerWrappers.mjs'; +import { checkIfNodeIsConnectedToVC, getNodeDiscordID, getNodeDiscordUsername, checkIfNodeHasOpenDiscordClient, getNodeCurrentListeningSystem, requestNodeJoinSystem } from '../../modules/socketServerWrappers.mjs'; import { getAllDiscordIDs } from '../../modules/mongo-wrappers/mongoDiscordIDWrappers.mjs' +import { ActionRowBuilder, ButtonBuilder, ButtonStyle } from 'discord.js'; + export const checkOnlineBotsInGuild = async (nodeIo, guildId) => { - let onlineBots = []; - const openSockets = [...await nodeIo.allSockets()]; - await Promise.all(openSockets.map(async openSocket => { - openSocket = await nodeIo.sockets.sockets.get(openSocket); - const connected = await checkIfNodeIsConnectedToVC(nodeIo, guildId, openSocket.node.nuid); - log.INFO("Connected:", connected); - if (connected) { - const username = await getNodeDiscordUsername(openSocket, guildId); - const discordID = await getNodeDiscordID(openSocket); - onlineBots.push({ - name: username, - discord_id: discordID, - nuid: openSocket.node.nuid - }); - } - })); - - return onlineBots; - } - - - export const getAvailableTokensInGuild = async (nodeIo, guildId) => { - try { - // Execute both asynchronous functions concurrently - const [discordIDs, onlineBots] = await Promise.all([ - getAllDiscordIDs(), // Fetch all Discord IDs - checkOnlineBotsInGuild(nodeIo, guildId) // Check online bots in the guild - ]); - - // Use the results of both promises here - log.INFO("Available Discord IDs:", discordIDs); - log.INFO("Online bots in the guild:", onlineBots); - - // Filter any discordIDs that are not active - const availableDiscordIDs = discordIDs.filter(discordID => discordID.active == true).filter(discordID => !onlineBots.some(bot => Number(bot.discord_id) == discordID.discord_id)); - - // Return the unavailable discordIDs - return availableDiscordIDs; - } catch (error) { - console.error('Error getting available tokens in guild:', error); - throw error; + let onlineBots = []; + const openSockets = [...await nodeIo.allSockets()]; + await Promise.all(openSockets.map(async openSocket => { + openSocket = await nodeIo.sockets.sockets.get(openSocket); + const connected = await checkIfNodeIsConnectedToVC(nodeIo, guildId, openSocket.node.nuid); + log.INFO("Connected:", connected); + if (connected) { + const username = await getNodeDiscordUsername(openSocket, guildId); + const discordID = await getNodeDiscordID(openSocket); + onlineBots.push({ + name: username, + discord_id: discordID, + nuid: openSocket.node.nuid + }); } -}; \ No newline at end of file + })); + + return onlineBots; +} + + +export const getAvailableTokensInGuild = async (nodeIo, guildId) => { + try { + // Execute both asynchronous functions concurrently + const [discordIDs, onlineBots] = await Promise.all([ + getAllDiscordIDs(), // Fetch all Discord IDs + checkOnlineBotsInGuild(nodeIo, guildId) // Check online bots in the guild + ]); + + // Use the results of both promises here + log.INFO("Available Discord IDs:", discordIDs); + log.INFO("Online bots in the guild:", onlineBots); + + // Filter any discordIDs that are not active + const availableDiscordIDs = discordIDs.filter(discordID => discordID.active == true).filter(discordID => !onlineBots.some(bot => Number(bot.discord_id) == discordID.discord_id)); + + // Return the unavailable discordIDs + return availableDiscordIDs; + } catch (error) { + console.error('Error getting available tokens in guild:', error); + throw error; + } +}; + + + +/** + * Get the nodes with given system that are available to be used within a given server + * @param {any} nodeIo The nodeIO object contained in the discord server object + * @param {any} guildId The guild ID to search in + * @param {any} system The system to filter the nodes by + * @returns {any} + */ +export const getAvailableNodes = async (nodeIo, guildId, system) => { + // Get all open socket nodes + const openSockets = [...await nodeIo.allSockets()]; // TODO - Filter the returned nodes to only nodes that have the radio capability + log.DEBUG("All open sockets: ", openSockets); + + var availableNodes = []; + // Check each open socket to see if the node has the requested system + await Promise.all(openSockets.map(async openSocket => { + openSocket = await nodeIo.sockets.sockets.get(openSocket); + // Check if the node has an existing open client (meaning the radio is already being listened to) + const hasOpenClient = await checkIfNodeHasOpenDiscordClient(openSocket); + if (hasOpenClient) { + let currentSystem = await getNodeCurrentListeningSystem(openSocket); + if (currentSystem != system.name) { + log.INFO("Node is listening to a different system than requested", openSocket.node.name); + return; + } + } + + // Check if the bot has an open voice connection in the requested server already + const connected = await checkIfNodeIsConnectedToVC(nodeIo, guildId, openSocket.node.nuid); + log.INFO("Connected:", connected); + if (!connected) { + // Check if this node has the requested system, if so add it to the availble array + if (system.nodes.includes(openSocket.node.nuid)) { + availableNodes.push(openSocket); + } + } + + })); + + log.DEBUG("Availble nodes:", availableNodes.map(socket => socket.node.name)); + + return availableNodes; +} + +/** + * Gets the voice channel the user is currently in. + * @param {any} interaction - The interaction object. + * @returns {any} - The voice channel object, or null if the user is not in a voice channel. + */ +export const getUserVoiceChannel = (interaction) => { + if (!interaction.member.voice.channel) { + interaction.editReply({ content: `<@${interaction.member.id}>, you need to enter a voice channel before using this command`, ephemeral: true }); + return null; + } + return interaction.member.voice.channel; +} + +/** + * Joins a node to a specified system and voice channel. + * @param {any} nodeIo - The nodeIO server for manipulation of sockets. + * @param {any} interaction - The interaction object. + * @param {string} nodeId - The ID of the node to join. + * @param {any} system - The system object to join. + * @param {any} channel - The voice channel to join. + */ +export const joinNode = async (nodeIo, interaction, nodeId, system, channel) => { + try { + const openSocket = await nodeIo.sockets.sockets.get(nodeId); + const discordTokens = await getAvailableTokensInGuild(nodeIo, interaction.guild.id); + + if (discordTokens.length === 0) { + await interaction.editReply({ content: `<@${interaction.member.id}>, there are no free bots available.`, ephemeral: true }); + return; + } + + log.INFO("Joining node:", nodeId, system.name, channel.id, openSocket.node.name, discordTokens[0].token); + await requestNodeJoinSystem(openSocket, system.name, channel.id, discordTokens[0].token); + + await interaction.editReply({ content: `<@${interaction.member.id}>, a bot will join your channel listening to '${system.name}' shortly.`, ephemeral: true }); + } catch (err) { + log.ERROR("Failed to join node:", err); + await interaction.editReply({ content: `<@${interaction.member.id}>, an error occurred while joining the node: ${err.message}`, ephemeral: true }); + } +} + +/** + * Prompts the user to select a node from available nodes. + * @param {any} interaction - The interaction object. + * @param {Array} availableNodes - The list of available nodes. + * @param {Function} onNodeSelected - Callback function to handle the selected node. + */ +export const promptNodeSelection = async (interaction, availableNodes, onNodeSelected) => { + const nodeSelectionButtons = availableNodes.map(node => + new ButtonBuilder().setCustomId(node.id).setLabel(node.node.name).setStyle(ButtonStyle.Primary) + ); + + const actionRow = new ActionRowBuilder().addComponents(nodeSelectionButtons); + + const response = await interaction.editReply({ + content: `<@${interaction.member.id}>, please select the Node you would like to join with this system:`, + components: [actionRow], + ephemeral: true + }); + + const collectorFilter = i => i.user.id === interaction.user.id; + try { + const selectedNode = await response.awaitMessageComponent({ filter: collectorFilter, time: 60_000 }); + await onNodeSelected(selectedNode.customId); + } catch (e) { + log.ERROR("Node selection timeout:", e); + await interaction.editReply({ content: 'Confirmation not received within 1 minute, cancelling.', components: [] }); + } +} \ No newline at end of file