Linting
All checks were successful
release-tag / release-image (push) Successful in 1m52s
Lint JavaScript/Node.js / lint-js (push) Successful in 11s
DRB Tests / drb_mocha_tests (push) Successful in 29s

This commit is contained in:
Logan Cusano
2024-08-11 15:57:46 -04:00
parent 5cd47378d6
commit 117cbea67f
37 changed files with 2273 additions and 1738 deletions

View File

@@ -1,122 +1,117 @@
import { DebugBuilder } from "../../modules/debugger.mjs";
const log = new DebugBuilder("server", "discordBot.modules.gptHandler");
import dotenv from 'dotenv';
import dotenv from "dotenv";
dotenv.config();
import { OpenAI } from 'openai';
import { EventEmitter } from 'events';
import { OpenAI } from "openai";
import { EventEmitter } from "events";
const openai = new OpenAI(process.env.OPENAI_API_KEY);
const assistant = await openai.beta.assistants.create({
name: "Emmelia",
instructions: process.env.DRB_SERVER_INITIAL_PROMPT,
model: "gpt-4o",
name: "Emmelia",
instructions: process.env.DRB_SERVER_INITIAL_PROMPT,
model: "gpt-4o",
});
class EventHandler extends EventEmitter {
constructor(client) {
super();
this.client = client;
}
constructor(client) {
super();
this.client = client;
}
async onEvent(event) {
try {
console.log(event);
// Retrieve events that are denoted with 'requires_action'
// since these will have our tool_calls
if (event.event === "thread.run.requires_action") {
await this.handleRequiresAction(
event.data,
event.data.id,
event.data.thread_id,
);
}
} catch (error) {
console.error("Error handling event:", error);
}
async onEvent(event) {
try {
console.log(event);
// Retrieve events that are denoted with 'requires_action'
// since these will have our tool_calls
if (event.event === "thread.run.requires_action") {
await this.handleRequiresAction(
event.data,
event.data.id,
event.data.thread_id,
);
}
} catch (error) {
console.error("Error handling event:", error);
}
}
async handleRequiresAction(data, runId, threadId) {
try {
const toolOutputs =
data.required_action.submit_tool_outputs.tool_calls.map((toolCall) => {
// Call the function
switch (toolCall.function.name) {
case "getCurrentTemperature": return {
tool_call_id: toolCall.id,
output: "57",
};
}
});
// Submit all the tool outputs at the same time
await this.submitToolOutputs(toolOutputs, runId, threadId);
} catch (error) {
console.error("Error processing required action:", error);
}
async handleRequiresAction(data, runId, threadId) {
try {
const toolOutputs =
data.required_action.submit_tool_outputs.tool_calls.map((toolCall) => {
// Call the function
switch (toolCall.function.name) {
case "getCurrentTemperature":
return {
tool_call_id: toolCall.id,
output: "57",
};
}
});
// Submit all the tool outputs at the same time
await this.submitToolOutputs(toolOutputs, runId, threadId);
} catch (error) {
console.error("Error processing required action:", error);
}
}
async submitToolOutputs(toolOutputs, runId, threadId) {
try {
// Use the submitToolOutputsStream helper
const stream = this.client.beta.threads.runs.submitToolOutputsStream(
threadId,
runId,
{ tool_outputs: toolOutputs },
);
for await (const event of stream) {
this.emit("event", event);
}
} catch (error) {
console.error("Error submitting tool outputs:", error);
}
async submitToolOutputs(toolOutputs, runId, threadId) {
try {
// Use the submitToolOutputsStream helper
const stream = this.client.beta.threads.runs.submitToolOutputsStream(
threadId,
runId,
{ tool_outputs: toolOutputs },
);
for await (const event of stream) {
this.emit("event", event);
}
} catch (error) {
console.error("Error submitting tool outputs:", error);
}
}
}
const eventHandler = new EventHandler(openai);
eventHandler.on("event", eventHandler.onEvent.bind(eventHandler));
export const gptHandler = async (additionalMessages) => {
const thread = await openai.beta.threads.create();
const thread = await openai.beta.threads.create();
// Add the additional messages to the conversation
for (const msgObj of additionalMessages) {
await openai.beta.threads.messages.create(
thread.id,
msgObj
);
// Add the additional messages to the conversation
for (const msgObj of additionalMessages) {
await openai.beta.threads.messages.create(thread.id, msgObj);
}
log.DEBUG("AI Conversation:", thread);
// Run the thread to get a response
try {
const stream = await openai.beta.threads.runs.stream(
thread.id,
{ assistant_id: assistant.id },
eventHandler,
);
for await (const event of stream) {
eventHandler.emit("event", event);
}
log.DEBUG("AI Conversation:", thread);
let response;
const messages = await openai.beta.threads.messages.list(thread.id);
response = messages.data[0].content[0].text.value;
// Run the thread to get a response
try {
const stream = await openai.beta.threads.runs.stream(
thread.id,
{ assistant_id: assistant.id },
eventHandler,
);
log.DEBUG("AI Response:", response);
for await (const event of stream) {
eventHandler.emit("event", event);
}
let response;
const messages = await openai.beta.threads.messages.list(
thread.id
);
response = messages.data[0].content[0].text.value;
log.DEBUG("AI Response:", response);
if (!response) {
return false;
}
return response;
} catch (error) {
console.error('Error generating response:', error);
return false;
if (!response) {
return false;
}
}
return response;
} catch (error) {
console.error("Error generating response:", error);
return false;
}
};

View File

@@ -0,0 +1,51 @@
import { getConfig } from "./configHandler.mjs";
class PresenceManager {
/**
* Creates an instance of PresenceManager.
* @param {import('discord.js').Client} client - The Discord client instance.
*/
constructor(client) {
this.client = client;
this.defaultStatus = "online";
this.defaultActivityType = "LISTENING";
this.defaultActivityName = "for your commands";
this.defaultUrl = null;
}
/**
* Set the bot's presence.
* @param {"online"|"idle"|"dnd"} status - The status of the bot (online, idle, dnd).
* @param {"PLAYING"|"STREAMING"|"LISTENING"|"WATCHING"|"COMPETING"} activityType - The type of activity.
* @param {string} activityName - The name of the activity.
* @param {string} [url=null] - The URL for STREAMING activity type (optional).
*/
setPresence(status, activityType, activityName, url = null) {
const activityOptions = {
type: activityType.toUpperCase(),
name: activityName,
};
if (activityType.toUpperCase() === "STREAMING" && url) {
activityOptions.url = url;
}
this.client.user.setPresence({
status: status,
activities: [activityOptions],
});
}
/**
* Reset the bot's presence to the default state.
*/
resetToDefault() {
const defaultPresence = getConfig("presence");
console.log("Default Presence:", defaultPresence);
// Update your bot's presence using this configuration
this.client.user.setPresence(defaultPresence);
}
}
export default PresenceManager;

View File

@@ -1,73 +1,92 @@
import { DebugBuilder } from "../../modules/debugger.mjs";
const log = new DebugBuilder("server", "discordBot.modules.registerCommands");
import { REST, Routes } from 'discord.js';
import { REST, Routes } from "discord.js";
import dotenv from 'dotenv';
dotenv.config()
import dotenv from "dotenv";
dotenv.config();
const discordToken = process.env.DISCORD_TOKEN;
export const registerActiveCommands = async (serverClient) => {
const guildIDs = serverClient.guilds.cache;
const clientId = serverClient.user.id;
const commands = await serverClient.commands.map(command => command = command.data.toJSON());
const guildIDs = serverClient.guilds.cache;
const clientId = serverClient.user.id;
const commands = await serverClient.commands.map(
(command) => (command = command.data.toJSON()),
);
// Construct and prepare an instance of the REST module
const rest = new REST({ version: '10' }).setToken(discordToken);
// Construct and prepare an instance of the REST module
const rest = new REST({ version: "10" }).setToken(discordToken);
// and deploy your commands!
guildIDs.forEach(guild => {
log.INFO("Deploying commands for: ", guild.id);
log.DEBUG("Commands", commands);
(async () => {
try {
log.DEBUG(`Started refreshing application (/) commands for guild ID: ${guild.id}.`);
// The put method is used to fully refresh all commands in the guild with the current set
const data = await rest.put(
Routes.applicationGuildCommands(clientId, guild.id),
{ body: commands },
);
// and deploy your commands!
guildIDs.forEach((guild) => {
log.INFO("Deploying commands for: ", guild.id);
log.DEBUG("Commands", commands);
(async () => {
try {
log.DEBUG(
`Started refreshing application (/) commands for guild ID: ${guild.id}.`,
);
// The put method is used to fully refresh all commands in the guild with the current set
const data = await rest.put(
Routes.applicationGuildCommands(clientId, guild.id),
{ body: commands },
);
log.DEBUG(`Successfully reloaded ${data.length} application (/) commands for guild ID: ${guild.id}.`);
} catch (error) {
// And of course, make sure you catch and log any errors!
log.ERROR("ERROR Deploying commands: ", error, "Body from error: ", commands);
}
})()
})
log.DEBUG(
`Successfully reloaded ${data.length} application (/) commands for guild ID: ${guild.id}.`,
);
} catch (error) {
// And of course, make sure you catch and log any errors!
log.ERROR(
"ERROR Deploying commands: ",
error,
"Body from error: ",
commands,
);
}
})();
});
};
/**
* Remove all commands for a given bot in a given guild
*
*
* @param {any} serverClient The discord bot client
*/
export const unregisterAllCommands = async (serverClient) => {
const guildIDs = serverClient.guilds.cache;
const clientId = serverClient.user.id;
commands = [];
const guildIDs = serverClient.guilds.cache;
const clientId = serverClient.user.id;
commands = [];
const rest = new REST({ version: '10' }).setToken(discordToken);
guildIDs.forEach(guild => {
log.INFO("Removing commands for: ", clientId, guild.id);
(async () => {
try {
log.DEBUG(`Started removal of ${commands.length} application (/) commands for guild ID: ${guild.id}.`);
// The put method is used to fully refresh all commands in the guild with the current set
const data = await rest.put(
Routes.applicationGuildCommands(clientId, guild.id),
{ body: commands },
);
const rest = new REST({ version: "10" }).setToken(discordToken);
guildIDs.forEach((guild) => {
log.INFO("Removing commands for: ", clientId, guild.id);
(async () => {
try {
log.DEBUG(
`Started removal of ${commands.length} application (/) commands for guild ID: ${guild.id}.`,
);
// The put method is used to fully refresh all commands in the guild with the current set
const data = await rest.put(
Routes.applicationGuildCommands(clientId, guild.id),
{ body: commands },
);
log.DEBUG(`Successfully removed ${data.length} application (/) commands for guild ID: ${guild.id}.`);
} catch (error) {
// And of course, make sure you catch and log any errors!
log.ERROR("ERROR removing commands: ", error, "Body from error: ", commands);
}
})()
})
}
log.DEBUG(
`Successfully removed ${data.length} application (/) commands for guild ID: ${guild.id}.`,
);
} catch (error) {
// And of course, make sure you catch and log any errors!
log.ERROR(
"ERROR removing commands: ",
error,
"Body from error: ",
commands,
);
}
})();
});
};
/**
* This named wrapper will remove all commands and then re-add the commands back, effectively refreshing them
@@ -75,11 +94,13 @@ export const unregisterAllCommands = async (serverClient) => {
* @returns {any}
*/
export const refreshActiveCommandsWrapper = async (serverClient) => {
// Remove all commands
log.INFO("Removing/Unregistering all commands from all connected servers/guilds");
await unregisterAllCommands(serverClient);
// Deploy the active commands
log.INFO("Adding commands to all connected servers/guilds");
await registerActiveCommands(serverClient);
return;
}
// Remove all commands
log.INFO(
"Removing/Unregistering all commands from all connected servers/guilds",
);
await unregisterAllCommands(serverClient);
// Deploy the active commands
log.INFO("Adding commands to all connected servers/guilds");
await registerActiveCommands(serverClient);
return;
};

View File

@@ -1,37 +1,39 @@
// Import necessary modules
import { EmbedBuilder } from 'discord.js';
import { EmbedBuilder } from "discord.js";
import { DebugBuilder } from "../../modules/debugger.mjs";
import { parse } from "node-html-parser";
import { config } from 'dotenv';
import { config } from "dotenv";
// Load environment variables
config();
const log = new DebugBuilder("server", "discordBot.modules.rssWrappers");
const imageRegex = /(http(s?):)([/|.|\w|\s|-])*((\.(?:jpg|gif|png|webm))|(\/gallery\/(?:[/|.|\w|\s|-])*))/g;
const youtubeVideoRegex = /((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube(-nocookie)?\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)/g;
const imageRegex =
/(http(s?):)([/|.|\w|\s|-])*((\.(?:jpg|gif|png|webm))|(\/gallery\/(?:[/|.|\w|\s|-])*))/g;
const youtubeVideoRegex =
/((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube(-nocookie)?\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)/g;
export class DRBEmbedBuilder extends EmbedBuilder {
constructor() {
super();
this.setTimestamp();
this.setFooter({ text: 'Brought to you by Emmelia.' });
this.setFooter({ text: "Brought to you by Emmelia." });
}
}
export const sendPost = (post, source, channel) => {
log.DEBUG("Sending post from source: ", post, source);
const postTitle = String(post.title).substring(0, 150);
const postLink = post.link;
let postContent = `*This post has no content* [Direct Link](${post.link})`;
if (post.content || post['content:encoded']) {
const content = post['content:encoded'] ?? post.content;
if (post.content || post["content:encoded"]) {
const content = post["content:encoded"] ?? post.content;
const parsedContent = parse(content);
let postText = parsedContent.text.trim();
if (postText.length >= 3800) {
postText = `${postText.slice(0, 3800).substring(0, postText.lastIndexOf(" "))} [...](${post.link})`;
} else if (postText.length === 0) {
@@ -43,16 +45,18 @@ export const sendPost = (post, source, channel) => {
const ytVideos = content.match(youtubeVideoRegex);
if (ytVideos) {
ytVideos.slice(0, 4).forEach((ytVideo) => {
if (ytVideo.includes("embed")) ytVideo = ytVideo.replace("embed/", "watch?v=");
if (ytVideo.includes("embed"))
ytVideo = ytVideo.replace("embed/", "watch?v=");
postContent += `\nEmbedded Video from Post: [YouTube](${ytVideo})`;
});
}
// Extract the first image link if available
const imageLinks = parsedContent.querySelectorAll("a")
.map(link => link.getAttribute("href"))
.filter(href => href && href.match(imageRegex));
const imageLinks = parsedContent
.querySelectorAll("a")
.map((link) => link.getAttribute("href"))
.filter((href) => href && href.match(imageRegex));
if (imageLinks.length > 0) {
post.image = imageLinks[0];
}
@@ -67,11 +71,11 @@ export const sendPost = (post, source, channel) => {
try {
const rssMessage = new DRBEmbedBuilder()
.setColor(0x0099FF)
.setColor(0x0099ff)
.setTitle(postTitle)
.setURL(postLink)
.addFields({ name: 'Source', value: postSourceLink, inline: true })
.addFields({ name: 'Published', value: postPubDate, inline: true });
.addFields({ name: "Source", value: postSourceLink, inline: true })
.addFields({ name: "Published", value: postPubDate, inline: true });
if (postImage) {
log.DEBUG("Image from post:", postImage);
@@ -87,7 +91,14 @@ export const sendPost = (post, source, channel) => {
return channelResponse;
} catch (err) {
log.ERROR("Error sending message: ", postTitle, postId, postContent, postPubDate, err);
log.ERROR(
"Error sending message: ",
postTitle,
postId,
postContent,
postPubDate,
err,
);
return err;
}
};

View File

@@ -1,39 +1,49 @@
import { DebugBuilder } from "../../modules/debugger.mjs";
const log = new DebugBuilder("server", "discordBot.modules.wrappers");
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';
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
});
}
}));
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
checkOnlineBotsInGuild(nodeIo, guildId), // Check online bots in the guild
]);
// Use the results of both promises here
@@ -41,60 +51,76 @@ export const getAvailableTokensInGuild = async (nodeIo, guildId) => {
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));
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);
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
* 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} 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
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;
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);
// 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));
log.DEBUG(
"Availble nodes:",
availableNodes.map((socket) => socket.node.name),
);
return availableNodes;
}
};
/**
* Gets the voice channel the user is currently in.
@@ -103,11 +129,14 @@ export const getAvailableNodes = async (nodeIo, guildId, system) => {
*/
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 });
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.
@@ -117,25 +146,55 @@ export const getUserVoiceChannel = (interaction) => {
* @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) => {
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);
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 });
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);
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 });
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 });
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.
@@ -143,9 +202,16 @@ export const joinNode = async (nodeIo, interaction, nodeId, system, channel) =>
* @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)
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);
@@ -153,15 +219,21 @@ export const promptNodeSelection = async (interaction, availableNodes, onNodeSel
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
ephemeral: true,
});
const collectorFilter = i => i.user.id === interaction.user.id;
const collectorFilter = (i) => i.user.id === interaction.user.id;
try {
const selectedNode = await response.awaitMessageComponent({ filter: collectorFilter, time: 60_000 });
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: [] });
await interaction.editReply({
content: "Confirmation not received within 1 minute, cancelling.",
components: [],
});
}
}
};