18 Commits

Author SHA1 Message Date
Logan Cusano
b51300d878 Improved joining and leaving
All checks were successful
DRB Tests / drb_mocha_tests (push) Successful in 1m5s
release-tag / release-image (push) Successful in 2m6s
- Added wrappers
- Improved readability of command code
2024-07-14 19:26:17 -04:00
Logan Cusano
f29459aadb Added new connections command for debug
Some checks failed
DRB Tests / drb_mocha_tests (push) Failing after 35s
release-tag / release-image (push) Failing after 35s
2024-07-14 16:50:48 -04:00
Logan Cusano
2cd5eee940 Implement OpenAI Assistant API
- Updated linkCop
- Updated standard interaction handler
2024-07-14 15:47:46 -04:00
Logan Cusano
24296c2ae4 Update the prompt with the proper discord tag for the member ID
All checks were successful
release-tag / release-image (push) Successful in 3m3s
DRB Tests / drb_mocha_tests (push) Successful in 1m6s
2024-06-06 23:01:08 -04:00
Logan Cusano
db065c3ef0 Add a new event for server client joining new guild/server #1 2024-06-02 19:39:38 -04:00
Logan Cusano
697025ec1e Updated environment var names to match convention
All checks were successful
release-tag / release-image (push) Successful in 4m25s
DRB Tests / drb_mocha_tests (push) Successful in 32s
2024-06-02 19:35:01 -04:00
Logan Cusano
3350b9f191 Added new event for new member joining #18
- When a new member joins the server, GPT integration will welcome them
2024-06-02 19:34:41 -04:00
Logan Cusano
961a7cc2bd Implement early AI integration #18
All checks were successful
release-tag / release-image (push) Successful in 4m19s
DRB Tests / drb_mocha_tests (push) Successful in 1m16s
- Added new event to catch messageCreate events
- @ messages to the server will use ChatGPT to respond to the message with an indepth prompt about the server
- Implement module to interact with chatGPT repeatably
- Add linkcop with GPT integration #12
- Added environment variable for inital prompt for GPT integration
2024-06-02 19:16:01 -04:00
Logan Cusano
2c3cc18474 Update dependencies and add openai 2024-06-02 19:10:29 -04:00
Logan Cusano
424d5ae749 #16 Fix bug in rss remove
All checks were successful
release-tag / release-image (push) Successful in 4m5s
DRB Tests / drb_mocha_tests (push) Successful in 43s
- A dependency of remove was missing the log object
- Updated discord output for all RSS commands
2024-05-26 22:42:54 -04:00
Logan Cusano
5c86185ef5 Update port in dockerfile
All checks were successful
release-tag / release-image (push) Successful in 3m42s
DRB Tests / drb_mocha_tests (push) Successful in 55s
2024-05-26 21:45:08 -04:00
Logan Cusano
e6de0f4453 Undo repo change
All checks were successful
release-tag / release-image (push) Successful in 4m24s
DRB Tests / drb_mocha_tests (push) Successful in 36s
- Does not add to the repo, changes the name
2024-05-26 21:30:47 -04:00
Logan Cusano
e8cfca1d8d Update build action
All checks were successful
release-tag / release-image (push) Successful in 5m15s
DRB Tests / drb_mocha_tests (push) Successful in 1m0s
- Attempt to place the package in the repo instead of my profile
2024-05-26 21:23:19 -04:00
Logan Cusano
dce0086fdb Update build action
All checks were successful
release-tag / release-image (push) Successful in 4m52s
DRB Tests / drb_mocha_tests (push) Successful in 41s
- Update repo name with proper vars
2024-05-26 21:12:57 -04:00
Logan Cusano
ad45d8f0ea Update repo name
Some checks failed
DRB Tests / drb_mocha_tests (push) Successful in 49s
release-tag / release-image (push) Failing after 4m1s
2024-05-26 20:49:32 -04:00
Logan Cusano
2c5cf3dac0 update all local IPs to public hostname
Some checks failed
release-tag / release-image (push) Failing after 22s
DRB Tests / drb_mocha_tests (push) Successful in 32s
2024-05-26 20:47:31 -04:00
Logan Cusano
a3223b716e update server build action
Some checks failed
release-tag / release-image (push) Failing after 20s
DRB Tests / drb_mocha_tests (push) Successful in 34s
- try public hostname instead of IP for docker login (to use HTTPS)
2024-05-26 20:45:16 -04:00
Logan Cusano
7a246f9e2a Update build action to explicitly set gitea as http
Some checks failed
release-tag / release-image (push) Failing after 16s
DRB Tests / drb_mocha_tests (push) Successful in 31s
2024-05-26 20:43:19 -04:00
19 changed files with 1202 additions and 454 deletions

View File

@@ -25,14 +25,14 @@ jobs:
uses: docker/setup-buildx-action@v2
with: # replace it with your local IP
config-inline: |
[registry."${{ secrets.LOCAL_GITEA_IP}}:3000"]
http = true
insecure = true
[registry."git.vpn.cusano.net"]
http = false
insecure = false
- name: Login to DockerHub
uses: docker/login-action@v2
with:
registry: ${{ secrets.LOCAL_GITEA_IP}}:3000 # replace it with your local IP
registry: git.vpn.cusano.net # replace it with your local IP
username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }}
@@ -52,5 +52,5 @@ jobs:
linux/arm64
push: true
tags: | # replace it with your local IP and tags
${{ secrets.LOCAL_GITEA_IP}}:3000/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
${{ secrets.LOCAL_GITEA_IP}}:3000/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}
git.vpn.cusano.net/${{ vars.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ steps.meta.outputs.REPO_VERSION }}
git.vpn.cusano.net/${{ vars.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}

View File

@@ -15,7 +15,7 @@ RUN npm install
COPY . .
# Expose the port on which your Node.js application will run
EXPOSE 3000
EXPOSE 3420
# Command to run the Node.js application
CMD ["node", "."]

View File

@@ -0,0 +1,53 @@
import { DebugBuilder } from "../../modules/debugger.mjs";
const log = new DebugBuilder("server", "discordBot.addons.gptInteraction");
import { gptHandler } from "../modules/gptHandler.mjs";
export const gptInteraction = async (nodeIo, message) => {
let conversation = [];
let prevMessages = await message.channel.messages.fetch({ limit: 10 });
prevMessages.reverse();
prevMessages.forEach((msg) => {
// Check if the message was sent within the last 24 hours
if (new Date().getTime() - msg.createdTimestamp > (24 * 60 * 60 * 1000)) {
return;
}
// Check if it's from a bot other than the server
if (msg.author.bot && msg.author.id !== nodeIo.serverClient.user.id) return;
const username = msg.author.username.replace(/\s+/g, '_').replace(/[^\w\s]/gi, '');
if (msg.author.id === nodeIo.serverClient.user.id) {
conversation.push({
role: 'assistant',
//name: msg.author.id,
content: msg.content,
});
return;
}
conversation.push({
role: 'user',
//name: msg.author.id,
content: msg.content.replace(`<@${nodeIo.serverClient.user.id}>`, ''),
});
});
const response = await gptHandler(conversation);
if (response) {
const responseMessage = response;
const chunkSize = 2500;
for (let i = 0; i < responseMessage.length; i += chunkSize) {
const chunk = responseMessage.substring(i, i + chunkSize);
log.DEBUG("Sending message chunk:", chunk);
await message.reply(chunk);
}
} else {
message.channel.send('Sorry, I encountered an error while processing your request.');
}
}

View File

@@ -0,0 +1,83 @@
import { DebugBuilder } from "../../modules/debugger.mjs";
const log = new DebugBuilder("server", "discordBot.addons.linkCop");
import { gptHandler } from "../modules/gptHandler.mjs";
import dotenv from 'dotenv';
dotenv.config();
const approvedLinksChannel = "767303243285790721";
const restrictedChannelIds = process.env.LINKCOP_RESTRICTED_CHANNEL_IDS.split(',');
const linkRegExp = /(?:http[s]?:\/\/)?(?:www\.)?[-a-zA-Z0-9@:%._\+~#=]{1,256}\.[a-zA-Z0-9()]{1,6}\b(?:[-a-zA-Z0-9()@:%_\+.~#?&\/=]*)/g
export const linkCop = async (nodeIo, message) => {
if (message.channel.id == approvedLinksChannel || !restrictedChannelIds.includes(message.channel.id)) return false;
const urls = String(message.content).matchAll(linkRegExp);
if (!urls || urls.length === 0) return false;
log.DEBUG("Found URLs: ", urls);
let conversation = [];
let prevMessages = await message.channel.messages.fetch({ limit: 2 });
prevMessages.reverse();
prevMessages.forEach((msg) => {
// Check if the message was sent within the last 5 minutes
if (new Date().getTime() - msg.createdTimestamp > (5 * 60 * 1000)) {
return;
}
// Check if it's from a bot other than the server
if (msg.author.bot && msg.author.id !== nodeIo.serverClient.user.id) return;
const username = msg.author.username.replace(/\s+/g, '_').replace(/[^\w\s]/gi, '');
if (msg.author.id === nodeIo.serverClient.user.id) {
conversation.push({
role: 'assistant',
//name: msg.author.id,
content: msg.content,
});
return;
}
conversation.push({
role: 'user',
//name: msg.author.id,
content: msg.content.replace(`<@${nodeIo.serverClient.user.id}>`, ''),
});
});
conversation.push({
role: 'assistant',
content: `There has been a link posted to a channel that links are not allowed in. The above messages are from the channel that links are not allowed including the message with the link. The message with the link is going to be deleted and moved to the '#links' channels. You are replying to the message with the link to let the user know.`
});
const response = await gptHandler(conversation);
if (response) {
const responseMessage = response;
const chunkSize = 2000;
for (let i = 0; i < responseMessage.length; i += chunkSize) {
const chunk = responseMessage.substring(i, i + chunkSize);
log.DEBUG("Sending message chunk:", chunk);
await message.reply(chunk);
}
const messageContent = {
'author': message.author,
'content': `<@${message.author.id}> - ${String(message.content)}`,
'channelId': message.channelId,
'links': urls
}
await message.delete();
log.DEBUG("Message content: ", messageContent);
message.client.channels.cache.get(approvedLinksChannel).send(messageContent);
}
}

View File

@@ -0,0 +1,52 @@
import { DebugBuilder } from "../../modules/debugger.mjs";
const log = new DebugBuilder("server", "discordBot.command.ping");
import { SlashCommandBuilder } from 'discord.js';
// Exporting data property that contains the command structure for discord including any params
export const data = new SlashCommandBuilder()
.setName('connections')
.setDescription('Check to see what bots are online.');
// Exporting other properties
export const example = "/connections"; // An example of how the command would be run in discord chat, this will be used for the help command
export const deferInitialReply = false; // If we the initial reply in discord should be deferred. This gives extra time to respond, however the method of replying is different.
/**
* 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 = []; // The array to be filled with the autocorrect values
const filtered = choices.filter(choice => choice.name.startsWith(focusedValue));
log.INFO(focusedValue, choices, filtered);
await interaction.respond(filtered.map(choice => ({name: choice.name, value: choice.name})));
}
*/
/**
* 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 const execute = async (nodeIo, interaction) => {
try {
const sockets = await nodeIo.allSockets();
log.DEBUG("All open sockets: ",sockets);
let socketMessage = "";
// Create the message for discord with each socket on a new line
sockets.forEach(socket => {
socketMessage += `\n${socket}`
});
await interaction.reply(`**Online Sockets: '${socketMessage}'**`);
//await interaction.reply('**Pong.**');
//await interaction.channel.send('**Pong.**');
} catch (err) {
console.error(err);
// await interaction.reply(err.toString());
}
}

View File

@@ -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 });
}
}

View File

@@ -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 });
}
}
}

View File

@@ -54,18 +54,19 @@ export const execute = async (nodeIo, interaction) => {
var category = interaction.options.getString('category');
if (!category) category = "ALL";
await interaction.reply(`Adding ${title} to the list of RSS sources, please wait...`);
await addSource(title, link, category, interaction.guildId, interaction.channelId, (err, result) => {
log.DEBUG("Result from adding entry", result);
if (result) {
interaction.reply(`Successfully added ${title} to the list of RSS sources`);
interaction.editReply(`Successfully added ${title} to the list of RSS sources`);
} else {
interaction.reply(`${title} already exists in the list of RSS sources`);
interaction.editReply(`${title} already exists in the list of RSS sources`);
}
});
} catch (err) {
log.ERROR(err)
await interaction.reply(err.toString());
await interaction.editReply(err.toString());
}
}

View File

@@ -42,17 +42,17 @@ export async function autocomplete(nodeIo, interaction) {
export const execute = async (nodeIo, interaction) => {
try {
var title = interaction.options.getString('title');
interaction.reply(`Removing ${title} from the list of RSS sources, please wait...`);
await interaction.reply(`Removing ${title} from the list of RSS sources, please wait...`);
const results = await deleteFeedByTitle(title);
if (!results) {
log.WARN(`Failed to remove source: ${title}`);
interaction.editReply(`Failed to remove source: '${title}'`);
await interaction.editReply(`Failed to remove source: '${title}'`);
return;
}
interaction.editReply(`${title} was successfully removed from the RSS sources.`)
await interaction.editReply(`${title} was successfully removed from the RSS sources.`)
} catch (err) {
log.ERROR(err)
interaction.editReply(err.toString());
await interaction.editReply(err.toString());
}
}

View File

@@ -38,6 +38,7 @@ export const execute = async (nodeIo, interaction) => {
//await interaction.reply(`**Online Sockets: '${sockets}'**`);
await interaction.reply('Triggering RSS update');
await updateFeeds(interaction.client);
await interaction.editReply('RSS Update Completed');
//await interaction.channel.send('**Pong.**');
} catch (err) {
console.error(err);

View File

@@ -79,7 +79,7 @@ export function addEnabledEventListeners(serverClient, _eventsPath = "./events")
}
// The discord client
export const serverClient = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates] });
export const serverClient = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates, GatewayIntentBits.GuildMessages, GatewayIntentBits.MessageContent, GatewayIntentBits.GuildMembers, GatewayIntentBits.GuildPresences] });
// Run when the bot is ready
serverClient.on('ready', async () => {

View File

@@ -0,0 +1,16 @@
import { DebugBuilder } from "../../modules/debugger.mjs";
const log = new DebugBuilder("server", "discordBot.events.guildCreate");
import { Events } from 'discord.js';
import { addEnabledCommands, addEnabledEventListeners } from "../discordBot.mjs";
export const name = Events.GuildMemberAdd;
export async function execute(nodeIo, guild) {
log.INFO("Bot has joined a new server", guild);
log.DEBUG("Refreshing commands enabled");
await addEnabledCommands(nodeIo.serverClient);
log.DEBUG("Refreshing events enabled");
await addEnabledEventListeners(nodeIo.serverClient);
}

View File

@@ -0,0 +1,34 @@
import { DebugBuilder } from "../../modules/debugger.mjs";
const log = new DebugBuilder("server", "discordBot.events.guildMemberAdd");
import dotenv from 'dotenv';
dotenv.config();
import { Events } from 'discord.js';
import { gptHandler } from "../modules/gptHandler.mjs";
const welcomeChannel = process.env.WELCOME_CHANNEL_ID;
export const name = Events.GuildMemberAdd;
export async function execute(nodeIo, member) {
log.INFO("New user joined the server", member);
let conversation = [];
conversation.push({
role: 'system',
content: `There has been a new user that joined. Their name is '<@${member.id}>'. Please welcome them to the server and remind them about the rules.`
})
const response = await gptHandler(conversation);
if (response) {
const responseMessage = response.choices[0].message.content;
const chunkSize = 2500;
for (let i = 0; i < responseMessage.length; i += chunkSize) {
const chunk = responseMessage.substring(i, i + chunkSize);
log.DEBUG("Sending message chunk:", chunk);
await nodeIo.serverClient.channels.cache.get(welcomeChannel).send(chunk);
}
}
}

View File

@@ -0,0 +1,29 @@
import { DebugBuilder } from "../../modules/debugger.mjs";
const log = new DebugBuilder("server", "discordBot.events.messageCreate");
import dotenv from 'dotenv';
dotenv.config();
import { Events } from 'discord.js';
import { gptInteraction } from '../addons/gptInteraction.mjs';
import { linkCop } from '../addons/linkCop.mjs';
const IGNORED_CHANNELS = process.env.IGNORED_CHANNEL_IDS.split(',');
export const name = Events.MessageCreate;
export async function execute(nodeIo, message) {
// Ignore ignored channels
if (IGNORED_CHANNELS.includes(message.channel.id)) return;
// Ignore messages from a bot
if (message.author.bot) return;
log.INFO("Message create", message);
// Check if the message mentions the bot
if (message.mentions.users.has(nodeIo.serverClient.user.id)) {
return await gptInteraction(nodeIo, message);
}
// Check if the message contains a link in a channel it shouldn't
if (await linkCop(nodeIo, message)) return;
}

View File

@@ -0,0 +1,122 @@
import { DebugBuilder } from "../../modules/debugger.mjs";
const log = new DebugBuilder("server", "discordBot.modules.gptHandler");
import dotenv from 'dotenv';
dotenv.config();
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",
});
class EventHandler extends EventEmitter {
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 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);
}
}
}
const eventHandler = new EventHandler(openai);
eventHandler.on("event", eventHandler.onEvent.bind(eventHandler));
export const gptHandler = async (additionalMessages) => {
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
);
}
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);
}
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;
}
}

View File

@@ -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
});
}
};
}));
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: [] });
}
}

829
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -11,19 +11,20 @@
"license": "ISC",
"type": "module",
"devDependencies": {
"chai": "^5.1.0",
"chai": "^5.1.1",
"mocha": "^10.4.0",
"socket.io-client": "^4.7.5"
},
"dependencies": {
"discord.js": "^14.14.1",
"dotenv": "^16.3.1",
"express": "^4.18.2",
"mongodb": "^6.3.0",
"discord.js": "^14.15.2",
"dotenv": "^16.4.5",
"express": "^4.19.2",
"mongodb": "^6.7.0",
"morgan": "^1.10.0",
"node-html-parser": "^6.1.13",
"openai": "^4.47.3",
"rss-parser": "^3.13.0",
"socket.io": "^4.7.2",
"user-agents": "^1.1.208"
"socket.io": "^4.7.5",
"user-agents": "^1.1.222"
}
}

View File

@@ -1,3 +1,5 @@
import { DebugBuilder } from "../modules/debugger.mjs";
const log = new DebugBuilder("server", "sourceManager");
import { createFeed, getFeedByLink, deleteFeedByLink } from '../modules/mongo-wrappers/mongoFeedsWrappers.mjs';
class SourceManager {