From 42784f185216e1c35bd03fadd24f5397e74583bc Mon Sep 17 00:00:00 2001 From: Logan Cusano Date: Sun, 18 Feb 2024 20:05:10 -0500 Subject: [PATCH] Functional joining and leaving - Needs to be tested on multiple servers - Needs to be tested with multiple nodes #1 #9 --- client/discordAudioBot/dab.mjs | 10 +++ client/modules/socketClient.mjs | 48 ++++++++----- client/modules/socketClientWrappers.mjs | 49 ++++++++++++++ server/discordBot/commands/join.mjs | 27 +++++--- server/discordBot/commands/leave.mjs | 67 +++++++++++++++++++ .../discordBot/events/interactionCreate.mjs | 2 +- server/modules/socketServerWrappers.mjs | 49 ++++++++++---- 7 files changed, 210 insertions(+), 42 deletions(-) create mode 100644 server/discordBot/commands/leave.mjs diff --git a/client/discordAudioBot/dab.mjs b/client/discordAudioBot/dab.mjs index 8afecb8..ce2781b 100644 --- a/client/discordAudioBot/dab.mjs +++ b/client/discordAudioBot/dab.mjs @@ -97,6 +97,10 @@ export async function checkIfConnectedToVC(guildId) { return connection } +export const getVoiceConnectionFromGuild = async (guildId) => { + return getVoiceConnection(guildId); +} + export async function initDiscordBotClient(token, systemName, readyCallback) { const client = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildMessages, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.MessageContent], @@ -104,10 +108,16 @@ export async function initDiscordBotClient(token, systemName, readyCallback) { client.on(Events.ClientReady, () => { console.log('discord.js client is ready!'); + + // Attach the recorder to the VC connection attachRecorder(); + + // Set the activity of the bot user client.user.setPresence({ activities: [{ name: `${systemName}`, type: ActivityType.Listening }], }); + + // readyCallback(client); }); diff --git a/client/modules/socketClient.mjs b/client/modules/socketClient.mjs index a37301b..a91e955 100644 --- a/client/modules/socketClient.mjs +++ b/client/modules/socketClient.mjs @@ -1,49 +1,61 @@ import { io } from "socket.io-client"; -import { connectToChannel, initDiscordBotClient, getVoiceChannelFromID, checkIfConnectedToVC } from '../discordAudioBot/dab.mjs'; -import { logIntoServerWrapper, sendNodeUpdateWrapper } from "./socketClientWrappers.mjs"; +import { connectToChannel, initDiscordBotClient, getVoiceChannelFromID, checkIfConnectedToVC, getVoiceConnectionFromGuild } from '../discordAudioBot/dab.mjs'; +import { logIntoServerWrapper, sendNodeUpdateWrapper, nodeCheckStatus } from "./socketClientWrappers.mjs"; +/** + * Initialize the socket connection with the server, this will handle disconnects within itself + * @param {Object} localNodeConfig The local node config object + * @returns {any} + */ export const initSocketConnection = async (localNodeConfig) => { const serverEndpoint = `http://${localNodeConfig.serverIp}:${localNodeConfig.serverPort}` || 'http://localhost:3000'; // Adjust the server endpoint const socket = io.connect(serverEndpoint); + const discordClients = {}; + socket.on('connect', async () => { console.log('Connected to the server'); await logIntoServerWrapper(socket, localNodeConfig); }); - socket.on('node-join', async (joinData) => { + socket.on('node-join', async (joinData) => { console.log("Join requested: ", joinData) // TODO - Implement logic to control OP25 for the requested channel/system // Join the requested channel with the requested ID initDiscordBotClient(joinData.clientID, joinData.system, client => { getVoiceChannelFromID(client, joinData.channelID).then(vc => { + // Add the client object to the IO instance + discordClients[vc.guild.id] = client; const connection = connectToChannel(vc); console.log("Bot Connected to VC"); - }) - }); + }) + }); }); - socket.on('node-leave', async () => { + socket.on('node-leave', async (guildId) => { console.log("Leave requested"); - const connection = await getVoiceConnection(myVoiceChannel.guild.id); - if (connection) { - console.log("There is an open VC connection, closing it now"); - connection.destroy(); + if (await checkIfConnectedToVC(guildId)) { + const connection = await getVoiceConnectionFromGuild(guildId); + if (connection) { + console.log("There is an open VC connection, closing it now"); + // Destroy the open VC connection + connection.destroy(); + + // Remove the client from the socket connection + delete discordClients[guildId]; + } } }); - socket.on('node-check-connected-status', async (guildId, socketCallback) => { - console.log("Requested status check"); - if (await checkIfConnectedToVC(guildId)) { - console.log("There is an open VC connection"); - socketCallback(true); - } else { - socketCallback(false); - } + socket.on('node-get-discord-username', async (guildId, socketCallback) => { + console.log("Requested username"); + socketCallback(discordClients[guildId].user.username); }); + socket.on('node-check-connected-status', nodeCheckStatus); + socket.on('disconnect', () => { console.log('Disconnected from the server'); }); diff --git a/client/modules/socketClientWrappers.mjs b/client/modules/socketClientWrappers.mjs index 107a9fc..62dbd5d 100644 --- a/client/modules/socketClientWrappers.mjs +++ b/client/modules/socketClientWrappers.mjs @@ -1,11 +1,60 @@ +import { checkIfConnectedToVC } from '../discordAudioBot/dab.mjs'; + +/** + * Wrapper to log into the server + * @param {any} socket The socket connection with the server + * @param {object} localNodeConfig The local node object + * @returns {any} + */ export const logIntoServerWrapper = async (socket, localNodeConfig) => { + // Log into the server socket.emit("node-login", localNodeConfig.node); + + // Send an update to the server sendNodeUpdateWrapper(socket, localNodeConfig); } +/** + * Send the server an update + * @param {any} socket The socket connection with the server + * @param {object} localNodeConfig The local node object + */ export const sendNodeUpdateWrapper = async (socket, localNodeConfig) => { socket.emit('node-update', { 'node': localNodeConfig.node, 'nearbySystems': localNodeConfig.nearbySystems }); +} + + +export const nodeJoinServer = async (joinData) => { + console.log("Join requested: ", joinData) + // TODO - Implement logic to control OP25 for the requested channel/system + + // Join the requested channel with the requested ID + initDiscordBotClient(joinData.clientID, joinData.system, client => { + getVoiceChannelFromID(client, joinData.channelID).then(vc => { + // Add the client object to the IO instance + discordClients[vc.guild.id] = client; + const connection = connectToChannel(vc); + console.log("Bot Connected to VC"); + }) + }); +} + + +/** + * Check if the bot is connected to a discord VC in the given server + * @param {string} guildId The guild id to check the connection status in + * @param {any} socketCallback The callback function to return the result to + * @returns {any} + */ +export const nodeCheckStatus = async (guildId, socketCallback) => { + console.log("Requested status check"); + if (await checkIfConnectedToVC(guildId)) { + console.log("There is an open VC connection"); + socketCallback(true); + } else { + socketCallback(false); + } } \ No newline at end of file diff --git a/server/discordBot/commands/join.mjs b/server/discordBot/commands/join.mjs index e87d7e8..2ad24e3 100644 --- a/server/discordBot/commands/join.mjs +++ b/server/discordBot/commands/join.mjs @@ -12,7 +12,16 @@ export const data = new SlashCommandBuilder() .setRequired(true) .setAutocomplete(true)); -export async function autocomplete(interaction) { +// Exporting other properties +export const example = "/join"; +export const deferInitialReply = true; + +/** + * Function to give the user auto-reply suggestions + * @param {any} nodeIo The nodeIO server for manipulation of sockets + * @param {any} interaction The interaction object + */ +export async function autocomplete(nodeIo, interaction) { const focusedValue = interaction.options.getFocused(); const choices = await getAllSystems(); const filtered = choices.filter(choice => choice.name.startsWith(focusedValue)); @@ -24,14 +33,14 @@ export async function autocomplete(interaction) { ); } -// Exporting other properties -export const example = "/join"; -export const deferInitialReply = true; - -// Exporting execute function +/** + * The function to run when the command is called by a discord user + * @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.reply({ content: 'You need to enter a voice channel before use the command', ephemeral: true }) } + if (!interaction.member.voice.channel) { return await interaction.reply({ content: `<@${interaction.member.id}>, you need to enter a voice channel before use the command`, ephemeral: true }) } // Grab the channel if the user is connected to VC const channelToJoin = interaction.member.voice.channel; @@ -74,7 +83,7 @@ export async function execute(nodeIo, interaction) { // 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("The selected system has no available nodes"); + 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 @@ -94,7 +103,7 @@ export async function execute(nodeIo, interaction) { // Reply to the user with the button prompts const response = await interaction.editReply({ - content: "Please select the Node you would like to join with this system", + content: `<@${interaction.member.id}>, Please select the Node you would like to join with this system`, components: [actionRow] }); diff --git a/server/discordBot/commands/leave.mjs b/server/discordBot/commands/leave.mjs new file mode 100644 index 0000000..24f1e77 --- /dev/null +++ b/server/discordBot/commands/leave.mjs @@ -0,0 +1,67 @@ +import { SlashCommandBuilder } from 'discord.js'; +import { checkIfNodeIsConnectedToVC, requestBotLeaveServer, getNodeDiscordUsername, getSocketIdByNuid } from '../../modules/socketServerWrappers.mjs'; + +// Exporting data property +export const data = new SlashCommandBuilder() + .setName('leave') + .setDescription('Disconnect a bot from the server') + .addStringOption(system => + system.setName('bot') + .setDescription('The bot you would like to disconnect') + .setRequired(true) + .setAutocomplete(true));; + +// Exporting other properties +export const example = "/leave *{Bot Name}*"; +export const deferInitialReply = true; + +/** + * Function to give the user auto-reply suggestions + * @param {any} nodeIo The nodeIO server for manipulation of sockets + * @param {any} interaction The interaction object + */ +export async function autocomplete(nodeIo, interaction) { + const focusedValue = interaction.options.getFocused(); + const choices = []; + + const openSockets = [...await nodeIo.allSockets()]; + await Promise.all(openSockets.map(async openSocket => { + openSocket = await nodeIo.sockets.sockets.get(openSocket); + const connected = await checkIfNodeIsConnectedToVC(nodeIo, interaction.guild.id, openSocket.node.nuid); + console.log("Connected:", connected); + if (connected) { + const username = await getNodeDiscordUsername(openSocket, interaction.guild.id); + choices.push({ + name: username, + value: openSocket.node.nuid + }); + } + })); + + const filtered = choices.filter(choice => choice.name.startsWith(focusedValue)); + + console.log(focusedValue, choices, filtered); + + await interaction.respond(filtered); +} + +/** + * The function to run when the command is called by a discord user + * @param {any} nodeIo The nodeIO server for manipulation of sockets + * @param {any} interaction The interaction object + */ +export async function execute(nodeIo, interaction) { + try { + // Get the requested bot + const selectedNode = interaction.options.getString('bot'); + const socket = await getSocketIdByNuid(nodeIo, selectedNode); + console.log("All open sockets:", socket, selectedNode); + 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.**'); + } catch (err) { + console.error(err); + // await interaction.reply(err.toString()); + } +} \ No newline at end of file diff --git a/server/discordBot/events/interactionCreate.mjs b/server/discordBot/events/interactionCreate.mjs index 25332e0..0aa7482 100644 --- a/server/discordBot/events/interactionCreate.mjs +++ b/server/discordBot/events/interactionCreate.mjs @@ -9,7 +9,7 @@ export async function execute(nodeIo, interaction) { // Execute autocomplete if the user is checking autocomplete if (interaction.isAutocomplete()) { console.log("Running autocomplete for command: ", command.data.name); - return await command.autocomplete(interaction); + return await command.autocomplete(nodeIo, interaction); } // Check if the interaction is a command diff --git a/server/modules/socketServerWrappers.mjs b/server/modules/socketServerWrappers.mjs index 16beb98..37a5ded 100644 --- a/server/modules/socketServerWrappers.mjs +++ b/server/modules/socketServerWrappers.mjs @@ -66,7 +66,6 @@ export const nodeUpdateWrapper = async (nodeData) => { * Wrapper to update the systems from the nearbySystems object passed from clients * @param {string} nuid The NUID of the node that sent the update * @param {object} nearbySystems The nearby systems object passed from the node to be updated - * @returns {any} */ export const nearbySystemsUpdateWraper = async (nuid, nearbySystems) => { console.log("System updates sent by node: ", nuid, nearbySystems); @@ -142,9 +141,12 @@ export const nearbySystemsUpdateWraper = async (nuid, nearbySystems) => { * @param {string} nuid The NUID to find within the open sockets * @returns {string|null} Will return the open socket ID or NULL */ -const getSocketIdByNuid = async (nodeIo, nuid) => { - for (const openSocket in await nodeIo.allSockets()) { - if (openSockets[openSocket] == nuid) +export const getSocketIdByNuid = async (nodeIo, nuid) => { + const openSockets = await nodeIo.allSockets(); + for (const openSocketId of openSockets) { + console.log(openSockets) + const openSocket = await nodeIo.sockets.sockets.get(openSocketId); + if (openSocket.node.nuid == nuid) return openSocket; } return null; @@ -152,9 +154,9 @@ const getSocketIdByNuid = async (nodeIo, nuid) => { /** * Get all nodes that are connected to a voice channel - * @param {any} nodeIo - * @param {any} guildId The guild ID string for the guild we are looking in - * @returns {any} + * @param {any} nodeIo The nodeIo object that contains the IO server + * @param {string} guildId The guild ID string for the guild we are looking in + * @returns {Array} The sockets connected to VC in a given server */ export const getAllSocketsConnectedToVC = async (nodeIo, guildId) => { // Get all open socket nodes @@ -167,7 +169,7 @@ export const getAllSocketsConnectedToVC = async (nodeIo, guildId) => { await new Promise((res) => { openSocket.emit('node-check-connected-status', guildId, (status) => { if (status) { - console.log("Socket is connected to VC:", openSocket.node.name); + console.log("Socket is connected to VC:", openSocket.node.name, status); socketsConnectedToVC.push(openSocket); } else { console.log("Socket is NOT connected to VC:", openSocket.node.name); @@ -183,8 +185,8 @@ export const getAllSocketsConnectedToVC = async (nodeIo, guildId) => { /** * Wrapper to check if the given NUID is connected to a VC * @param {any} nodeIo The nodeIo object that contains the IO server - * @param {any} nuid The NUID string that we would like to find in the open socket connections - * @returns {any} + * @param {string} nuid The NUID string that we would like to find in the open socket connections + * @returns {boolean} If the node is connected to VC in the given server */ export const checkIfNodeIsConnectedToVC = async (nodeIo, guildId, nuid) => { const socketsConnectedToVC = await getAllSocketsConnectedToVC(nodeIo, guildId); @@ -196,12 +198,25 @@ export const checkIfNodeIsConnectedToVC = async (nodeIo, guildId, nuid) => { return false; } +/** + * Get the discord username from a given socket + * @param {any} socket The socket object of the node to check the username of + * * @param {string} guildId The guild ID to check the username in + * @returns {string} The username of the bot in the requested server + */ +export const getNodeDiscordUsername = async (socket, guildId) => { + return await new Promise((res) => { + socket.emit('node-get-discord-username', guildId, (username) => { + res(username); + }); + }); +} + /** * Request a given socket node to join a given voice channel * @param {any} socket The socket object of the node the request should be sent to * @param {any} systemName The system preset name that we would like to listen to - * @param {any} discordChanelId The Discord channel ID to join the listening bot to - * @returns {any} + * @param {string} discordChanelId The Discord channel ID to join the listening bot to */ export const requestNodeJoinSystem = async (socket, systemName, discordChanelId) => { // Check for open client IDs @@ -214,6 +229,12 @@ export const requestNodeJoinSystem = async (socket, systemName, discordChanelId) await sendNodeCommand(socket, "node-join", joinData); } -export const requestBotLeave = async () => { - +/** + * Request a given socket node to leave VC in a given server + * @param {any} socket The socket object of the node the request should be sent to + * @param {string} guildId The guild ID to disconnect the socket node from + */ +export const requestBotLeaveServer = async (socket, guildId) => { + // Send the command to the node + await sendNodeCommand(socket, "node-leave", guildId); } \ No newline at end of file