1 Commits

Author SHA1 Message Date
Logan Cusano
57aaf04119 Initial migration to jasmine
- Still bugs in tests from migration
2024-05-26 20:41:03 -04:00
49 changed files with 2341 additions and 4832 deletions

View File

@@ -1,42 +0,0 @@
name: Update Wiki from JSDoc
on:
push:
branches:
- main
# schedule:
# - cron: '0 0 * * 1' # Every Monday at midnight (UTC)
jobs:
update-wiki:
runs-on: ubuntu-latest
steps:
- name: Checkout the code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22'
- name: Install dependencies
run: npm install
- name: Generate JSDoc
run: npx jsdoc -c jsdoc.json -d docs
- name: Checkout the wiki repository
uses: actions/checkout@v4
with:
repository: logan/drb-server.wiki # Replace with your wiki repository
path: wiki
- name: Update wiki
run: |
cp -r docs/* wiki/
cd wiki
git config user.name "gitea-actions"
git config user.email "gitea-actions@cusano.net"
git add .
git commit -m "Update wiki from JSDoc"
git push

View File

@@ -1,28 +0,0 @@
name: Lint JavaScript/Node.js
on:
push:
branches:
- main
pull_request:
branches:
- "*"
jobs:
lint-js:
runs-on: ubuntu-latest
steps:
- name: Checkout Code
uses: actions/checkout@v4
- name: Set up Node.js
uses: actions/setup-node@v4
with:
node-version: '22' # Use your preferred Node.js version
- name: Install Dependencies
run: npm install
- name: Lint JavaScript/Node.js
run: npm run lint

View File

@@ -16,23 +16,23 @@ jobs:
RUNNER_TOOL_CACHE: /toolcache RUNNER_TOOL_CACHE: /toolcache
steps: steps:
- name: Checkout - name: Checkout
uses: actions/checkout@v4 uses: actions/checkout@v3
- name: Set up QEMU - name: Set up QEMU
uses: docker/setup-qemu-action@v3 uses: docker/setup-qemu-action@v2
- name: Set up Docker BuildX - name: Set up Docker BuildX
uses: docker/setup-buildx-action@v3 uses: docker/setup-buildx-action@v2
with: # replace it with your local IP with: # replace it with your local IP
config-inline: | config-inline: |
[registry."git.vpn.cusano.net"] [registry."${{ secrets.LOCAL_GITEA_IP}}:3000"]
http = false http = true
insecure = false insecure = true
- name: Login to DockerHub - name: Login to DockerHub
uses: docker/login-action@v3 uses: docker/login-action@v2
with: with:
registry: git.vpn.cusano.net # replace it with your local IP registry: ${{ secrets.LOCAL_GITEA_IP}}:3000 # replace it with your local IP
username: ${{ secrets.DOCKER_USERNAME }} username: ${{ secrets.DOCKER_USERNAME }}
password: ${{ secrets.DOCKER_PASSWORD }} password: ${{ secrets.DOCKER_PASSWORD }}
@@ -52,5 +52,5 @@ jobs:
linux/arm64 linux/arm64
push: true push: true
tags: | # replace it with your local IP and tags tags: | # replace it with your local IP and tags
git.vpn.cusano.net/${{ vars.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 }}:${{ steps.meta.outputs.REPO_VERSION }}
git.vpn.cusano.net/${{ vars.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }} ${{ secrets.LOCAL_GITEA_IP}}:3000/${{ env.DOCKER_ORG }}/${{ steps.meta.outputs.REPO_NAME }}:${{ env.DOCKER_LATEST }}

View File

@@ -3,7 +3,7 @@ name: DRB Tests
on: on:
pull_request: pull_request:
branches: branches:
- "*" - main
push: push:
branches: branches:
- main - main

View File

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

View File

@@ -4,19 +4,16 @@ const log = new DebugBuilder("server", "server");
// Function called by the main application to initialize the addon // Function called by the main application to initialize the addon
export function initialize(nodeIo, config) { export function initialize(nodeIo, config) {
log.INFO(`Initializing ${config.name}`); log.INFO(`Initializing ${config.name}`);
// Call other functions within the addon module // Call other functions within the addon module
registerSocketEvents(nodeIo, config); registerSocketEvents(nodeIo, config);
// Call additional initialization functions if needed // Call additional initialization functions if needed
} }
// Function to register Socket.IO event handlers // Function to register Socket.IO event handlers
function registerSocketEvents(nodeIo, config) { function registerSocketEvents(nodeIo, config) {
nodeIo.on(config.options.eventName, (data) => { nodeIo.on(config.options.eventName, (data) => {
log.DEBUG( log.DEBUG(`Received event "${config.options.eventName}" from client:`, data);
`Received event "${config.options.eventName}" from client:`, });
data,
);
});
} }

View File

@@ -1,57 +0,0 @@
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

@@ -1,105 +0,0 @@
import { DebugBuilder } from "../../modules/debugger.mjs";
const log = new DebugBuilder("server", "discordBot.addons.linkCop");
import { gptHandler } from "../modules/gptHandler.mjs";
import dotenv from "dotenv";
import { getGuildConfig } from "../../modules/mongo-wrappers/mongoConfigWrappers.mjs";
dotenv.config();
const linkRegExp = /http[s]?:\/\/\S+/g;
export const linkCop = async (nodeIo, message) => {
// Set the channel IDs based on the guild the message was sent in
const approvedLinksChannel =
(await getGuildConfig(message.guild.id, "approvedLinksChannel")) ||
"767303243285790721";
const restrictedChannelIds = await getGuildConfig(
message.guild.id,
"restrictedChannelIds",
);
// Check if the message was sent in an restricted channel
if (
message.channel.id == approvedLinksChannel ||
!Array.isArray(restrictedChannelIds) ||
Array.isArray(restrictedChannelIds) ||
!restrictedChannelIds.includes(message.channel.id)
) {
return false;
}
// Check if there are URLs in the sent message
const urls = [...message.content.matchAll(linkRegExp)];
log.DEBUG("Parsed URLs from message:", urls);
if (!urls || urls.length === 0) return false;
log.INFO("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

@@ -1,52 +0,0 @@
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,29 +1,19 @@
import { DebugBuilder } from "../../modules/debugger.mjs"; 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"); 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 // Exporting data property
export const data = new SlashCommandBuilder() export const data = new SlashCommandBuilder()
.setName("join") .setName('join')
.setDescription("Listen to the selected radio system in your channel") .setDescription('Listen to the selected radio system in your channel')
.addStringOption((system) => .addStringOption(system =>
system system.setName('system')
.setName("system") .setDescription('The radio system you would like to listen to')
.setDescription("The radio system you would like to listen to")
.setRequired(true) .setRequired(true)
.setAutocomplete(true), .setAutocomplete(true));
);
// Exporting other properties // Exporting other properties
export const example = "/join"; export const example = "/join";
@@ -37,93 +27,130 @@ export const deferInitialReply = true;
export async function autocomplete(nodeIo, interaction) { export async function autocomplete(nodeIo, interaction) {
const focusedValue = interaction.options.getFocused(); const focusedValue = interaction.options.getFocused();
const choices = await getAllSystems(); const choices = await getAllSystems();
const filtered = choices.filter((choice) => const filtered = choices.filter(choice => choice.name.startsWith(focusedValue));
choice.name.startsWith(focusedValue),
);
log.DEBUG(focusedValue, choices, filtered); log.DEBUG(focusedValue, choices, filtered);
try { await interaction.respond(
await interaction.respond( filtered.map(choice => ({ name: choice.name, value: choice.name })),
filtered.map((choice) => ({ name: choice.name, value: choice.name })), );
);
} catch (e) {
log.WARN("Autocomplete interaction failure", e);
}
} }
/** /**
* Handle join command execution * 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} nodeIo The nodeIO server for manipulation of sockets
* @param {any} interaction The interaction object * @param {any} interaction The interaction object
*/ */
export async function execute(nodeIo, interaction) { 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 { try {
// Validate user is in a voice channel // Get the selected system object from the DB
const channelToJoin = getUserVoiceChannel(interaction); const system = await getSystemByName(selectedSystem);
if (!channelToJoin) return;
// Get the selected system // Function wrapper to request the selected/only node to join the selected system
const selectedSystemName = interaction.options.getString("system"); const joinSelectedNode = async (selectedNodeSocketId) => {
const system = await getSystemByName(selectedSystemName); 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);
// Check if there was a system found by the given system name if (discordTokens.length >= 1) {
if (!system) { // TODO - Implement a method to have preferred tokens (bot users) for specific systems
await interaction.editReply({ log.INFO("Joining selected open socket:", selectedNodeSocketId, system.name, channelToJoin.id, openSocket.node.name, discordTokens[0].token);
content: `System '${selectedSystemName}' not found.`,
ephemeral: true, // 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 })
}
}
// 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, 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);
}
}
}));
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]
}); });
return;
}
// Get the available nodes for this system // Make sure the responding selection is from the user who initiated the command
const availableNodes = await getAvailableNodes( const collectorFilter = i => i.user.id === interaction.user.id;
nodeIo,
interaction.guild.id,
system,
);
// Check if there are available nodes // Wait for the confirmation from the user on which node to join
if (availableNodes.length === 0) { try {
// If not, let the user know const selectedNode = await response.awaitMessageComponent({ filter: collectorFilter, time: 60_000 });
await interaction.editReply( // Run the local wrapper to listen to the selected node
`<@${interaction.member.id}>, the selected system has no available nodes`, await joinSelectedNode(selectedNode.customId);
); // Let the user know
return; await selectedNodeConfirmation.update({ content: `Ok <@${interaction.member.id}>, a bot will join your channel listening to *'${system.name}'*`, components: [] });
} } catch (e) {
console.error(e);
// If there is one available node, request that node join // Timeout the prompt if the user doesn't interact with it
if (availableNodes.length === 1) { await interaction.editReply({ content: 'Confirmation not received within 1 minute, cancelling', components: [] });
await joinNode( }
nodeIo,
interaction,
availableNodes[0].id,
system,
channelToJoin,
);
}
// 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,
);
},
);
} }
} catch (err) { } catch (err) {
log.ERROR(err); console.error(err);
await interaction.editReply({ // await interaction.reply(err.toString());
content: `An error occurred: ${err.message}`,
ephemeral: true,
});
} }
} }

View File

@@ -1,24 +1,18 @@
import { DebugBuilder } from "../../modules/debugger.mjs"; import { DebugBuilder } from "../../modules/debugger.mjs";
import { SlashCommandBuilder } from "discord.js";
import {
requestBotLeaveServer,
getSocketIdByNuid,
} from "../../modules/socketServerWrappers.mjs";
import { checkOnlineBotsInGuild } from "../modules/wrappers.mjs";
const log = new DebugBuilder("server", "discordBot.command.leave"); 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'
// Exporting data property // Exporting data property
export const data = new SlashCommandBuilder() export const data = new SlashCommandBuilder()
.setName("leave") .setName('leave')
.setDescription("Disconnect a bot from the server") .setDescription('Disconnect a bot from the server')
.addStringOption((system) => .addStringOption(system =>
system system.setName('bot')
.setName("bot") .setDescription('The bot you would like to disconnect')
.setDescription("The bot you would like to disconnect")
.setRequired(true) .setRequired(true)
.setAutocomplete(true), .setAutocomplete(true));;
);
// Exporting other properties // Exporting other properties
export const example = "/leave *{Bot Name}*"; export const example = "/leave *{Bot Name}*";
@@ -31,21 +25,15 @@ export const deferInitialReply = true;
*/ */
export async function autocomplete(nodeIo, interaction) { export async function autocomplete(nodeIo, interaction) {
const focusedValue = interaction.options.getFocused(); const focusedValue = interaction.options.getFocused();
const choices = await checkOnlineBotsInGuild(nodeIo, interaction.guild.id); const choices = (await checkOnlineBotsInGuild(nodeIo, interaction.guild.id));
log.DEBUG(choices); log.DEBUG(choices);
const filtered = choices const filtered = choices.filter(choice => choice.name.startsWith(focusedValue)).map(choice => choice = {name: choice.name, value: choice.nuid});
.filter((choice) => choice.name.startsWith(focusedValue))
.map((choice) => ({ name: choice.name, value: choice.nuid }));
log.DEBUG(focusedValue, choices, filtered); log.DEBUG(focusedValue, choices, filtered);
try { await interaction.respond(filtered);
await interaction.respond(filtered);
} catch (e) {
log.WARN("Autocomplete interaction failure", e);
}
} }
/** /**
@@ -55,27 +43,16 @@ export async function autocomplete(nodeIo, interaction) {
*/ */
export async function execute(nodeIo, interaction) { export async function execute(nodeIo, interaction) {
try { try {
const selectedNode = interaction.options.getString("bot"); // Get the requested bot
const selectedNode = interaction.options.getString('bot');
const socket = await getSocketIdByNuid(nodeIo, selectedNode); 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 requestBotLeaveServer(socket, interaction.guild.id);
//await interaction.reply(`**Online Sockets: '${sockets}'**`);
await interaction.editReply( await interaction.editReply(`Ok <@${interaction.member.id}>, the bot is leaving shortly`);
`Ok <@${interaction.member.id}>, the bot is leaving shortly.`, //await interaction.channel.send('**Pong.**');
);
} catch (err) { } catch (err) {
log.ERROR("Failed to disconnect bot:", err); console.error(err);
await interaction.editReply({ // await interaction.reply(err.toString());
content: `An error occurred: ${err.message}`,
ephemeral: true,
});
} }
} }

View File

@@ -1,11 +1,11 @@
import { DebugBuilder } from "../../modules/debugger.mjs"; import { DebugBuilder } from "../../modules/debugger.mjs";
const log = new DebugBuilder("server", "discordBot.command.ping"); const log = new DebugBuilder("server", "discordBot.command.ping");
import { SlashCommandBuilder } from "discord.js"; import { SlashCommandBuilder } from 'discord.js';
// Exporting data property that contains the command structure for discord including any params // Exporting data property that contains the command structure for discord including any params
export const data = new SlashCommandBuilder() export const data = new SlashCommandBuilder()
.setName("ping") .setName('ping')
.setDescription("Replies with your input!"); .setDescription('Replies with your input!');
// Exporting other properties // Exporting other properties
export const example = "/ping"; // An example of how the command would be run in discord chat, this will be used for the help command export const example = "/ping"; // An example of how the command would be run in discord chat, this will be used for the help command
@@ -33,13 +33,13 @@ export async function autocomplete(nodeIo, interaction) {
*/ */
export const execute = async (nodeIo, interaction) => { export const execute = async (nodeIo, interaction) => {
try { try {
const sockets = await nodeIo.allSockets(); const sockets = await nodeIo.allSockets();
log.DEBUG("All open sockets: ", sockets); log.DEBUG("All open sockets: ",sockets);
//await interaction.reply(`**Online Sockets: '${sockets}'**`); //await interaction.reply(`**Online Sockets: '${sockets}'**`);
await interaction.reply("**Pong.**"); await interaction.reply('**Pong.**');
//await interaction.channel.send('**Pong.**'); //await interaction.channel.send('**Pong.**');
} catch (err) { } catch (err) {
console.error(err); console.error(err);
// await interaction.reply(err.toString()); // await interaction.reply(err.toString());
} }
}; }

View File

@@ -1,34 +1,28 @@
import { SlashCommandBuilder } from "discord.js";
import { SlashCommandBuilder } from 'discord.js';
import { DebugBuilder } from "../../modules/debugger.mjs"; import { DebugBuilder } from "../../modules/debugger.mjs";
import { addSource } from "../../rss-manager/sourceManager.mjs"; import { addSource } from '../../rss-manager/sourceManager.mjs'
const log = new DebugBuilder("server", "discordBot.command.rssAdd"); const log = new DebugBuilder("server", "discordBot.command.rssAdd");
// Exporting data property that contains the command structure for discord including any params // Exporting data property that contains the command structure for discord including any params
export const data = new SlashCommandBuilder() export const data = new SlashCommandBuilder()
.setName("rss-add") .setName('rss-add')
.setDescription("Add RSS Source") .setDescription('Add RSS Source')
.addStringOption((option) => .addStringOption(option =>
option option.setName('title')
.setName("title") .setDescription('The title of the RSS feed')
.setDescription("The title of the RSS feed") .setRequired(true))
.setRequired(true), .addStringOption(option =>
) option.setName('link')
.addStringOption((option) => .setDescription('The link to the RSS feed')
option .setRequired(true))
.setName("link") .addStringOption(option =>
.setDescription("The link to the RSS feed") option.setName('category')
.setRequired(true),
)
.addStringOption((option) =>
option
.setName("category")
.setDescription('The category for the RSS feed *("ALL" by default")*') .setDescription('The category for the RSS feed *("ALL" by default")*')
.setRequired(false), .setRequired(false))
);
// Exporting other properties // Exporting other properties
export const example = export const example = "/rss-add [title] [https://domain.com/feed.xml] [category]"; // An example of how the command would be run in discord chat, this will be used for the help command
"/rss-add [title] [https://domain.com/feed.xml] [category]"; // 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. 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.
/** /**
@@ -55,37 +49,23 @@ export async function autocomplete(nodeIo, interaction) {
*/ */
export const execute = async (nodeIo, interaction) => { export const execute = async (nodeIo, interaction) => {
try { try {
var title = interaction.options.getString("title"); var title = interaction.options.getString('title');
var link = interaction.options.getString("link"); var link = interaction.options.getString('link');
var category = interaction.options.getString("category"); var category = interaction.options.getString('category');
if (!category) category = "ALL"; if (!category) category = "ALL";
await interaction.reply(
`Adding ${title} to the list of RSS sources, please wait...`,
);
await addSource( await addSource(title, link, category, interaction.guildId, interaction.channelId, (err, result) => {
title, log.DEBUG("Result from adding entry", result);
link,
category,
interaction.guildId,
interaction.channelId,
(err, result) => {
log.DEBUG("Result from adding entry", result);
if (result) { if (result) {
interaction.editReply( interaction.reply(`Successfully added ${title} to the list of RSS sources`);
`Successfully added ${title} to the list of RSS sources`, } else {
); interaction.reply(`${title} already exists in the list of RSS sources`);
} else { }
interaction.editReply( });
`${title} already exists in the list of RSS sources`,
);
}
},
);
} catch (err) { } catch (err) {
log.ERROR(err); log.ERROR(err)
await interaction.editReply(err.toString()); await interaction.reply(err.toString());
} }
}; }

View File

@@ -1,22 +1,19 @@
import { SlashCommandBuilder } from "discord.js";
import { SlashCommandBuilder } from 'discord.js';
import { DebugBuilder } from "../../modules/debugger.mjs"; import { DebugBuilder } from "../../modules/debugger.mjs";
import { import { removeSource } from '../../rss-manager/sourceManager.mjs'
getAllFeeds, import { getAllFeeds, deleteFeedByTitle } from '../../modules/mongo-wrappers/mongoFeedsWrappers.mjs'
deleteFeedByTitle,
} from "../../modules/mongo-wrappers/mongoFeedsWrappers.mjs";
const log = new DebugBuilder("server", "discordBot.command.rssRemove"); const log = new DebugBuilder("server", "discordBot.command.rssRemove");
// Exporting data property that contains the command structure for discord including any params // Exporting data property that contains the command structure for discord including any params
export const data = new SlashCommandBuilder() export const data = new SlashCommandBuilder()
.setName("rss-remove") .setName('rss-remove')
.setDescription("Add RSS Source") .setDescription('Add RSS Source')
.addStringOption((option) => .addStringOption(option =>
option option.setName('title')
.setName("title") .setDescription('The title of the RSS feed')
.setDescription("The title of the RSS feed")
.setRequired(true) .setRequired(true)
.setAutocomplete(true), .setAutocomplete(true))
);
// Exporting other properties // Exporting other properties
export const example = "/rss-remove [title]"; // An example of how the command would be run in discord chat, this will be used for the help command export const example = "/rss-remove [title]"; // An example of how the command would be run in discord chat, this will be used for the help command
@@ -30,15 +27,11 @@ export const deferInitialReply = false; // If we the initial reply in discord sh
export async function autocomplete(nodeIo, interaction) { export async function autocomplete(nodeIo, interaction) {
const focusedValue = interaction.options.getFocused(); const focusedValue = interaction.options.getFocused();
const choices = (await getAllFeeds()) ?? []; const choices = await getAllFeeds() ?? [];
log.INFO("RSS Remove Choices:", choices); log.INFO("RSS Remove Choices:", choices);
const filtered = choices.filter((choice) => const filtered = choices.filter(choice => choice.title.startsWith(focusedValue));
choice.title.startsWith(focusedValue),
);
log.DEBUG(focusedValue, choices, filtered); log.DEBUG(focusedValue, choices, filtered);
await interaction.respond( await interaction.respond(filtered.map(choice => ({ name: choice.title, value: choice.title })));
filtered.map((choice) => ({ name: choice.title, value: choice.title })),
);
} }
/** /**
@@ -48,22 +41,18 @@ export async function autocomplete(nodeIo, interaction) {
*/ */
export const execute = async (nodeIo, interaction) => { export const execute = async (nodeIo, interaction) => {
try { try {
var title = interaction.options.getString("title"); var title = interaction.options.getString('title');
await interaction.reply( interaction.reply(`Removing ${title} from the list of RSS sources, please wait...`);
`Removing ${title} from the list of RSS sources, please wait...`,
);
const results = await deleteFeedByTitle(title); const results = await deleteFeedByTitle(title);
if (!results) { if (!results) {
log.WARN(`Failed to remove source: ${title}`); log.WARN(`Failed to remove source: ${title}`);
await interaction.editReply(`Failed to remove source: '${title}'`); interaction.editReply(`Failed to remove source: '${title}'`);
return; return;
} }
await interaction.editReply( interaction.editReply(`${title} was successfully removed from the RSS sources.`)
`${title} was successfully removed from the RSS sources.`,
);
} catch (err) { } catch (err) {
log.ERROR(err); log.ERROR(err)
await interaction.editReply(err.toString()); interaction.editReply(err.toString());
} }
}; }

View File

@@ -1,12 +1,12 @@
import { DebugBuilder } from "../../modules/debugger.mjs"; import { DebugBuilder } from "../../modules/debugger.mjs";
const log = new DebugBuilder("server", "discordBot.command.rssTrigger"); const log = new DebugBuilder("server", "discordBot.command.rssTrigger");
import { SlashCommandBuilder } from "discord.js"; import { SlashCommandBuilder } from 'discord.js';
import { updateFeeds } from "../../rss-manager/feedHandler.mjs"; import { updateFeeds } from '../../rss-manager/feedHandler.mjs'
// Exporting data property that contains the command structure for discord including any params // Exporting data property that contains the command structure for discord including any params
export const data = new SlashCommandBuilder() export const data = new SlashCommandBuilder()
.setName("rss-trigger") .setName('rss-trigger')
.setDescription("Manually triggers an RSS feed update"); .setDescription('Manually triggers an RSS feed update');
// Exporting other properties // Exporting other properties
export const example = "/rss-trigger"; // An example of how the command would be run in discord chat, this will be used for the help command export const example = "/rss-trigger"; // An example of how the command would be run in discord chat, this will be used for the help command
@@ -33,15 +33,14 @@ export async function autocomplete(nodeIo, interaction) {
* @param {any} interaction The interaction object * @param {any} interaction The interaction object
*/ */
export const execute = async (nodeIo, interaction) => { export const execute = async (nodeIo, interaction) => {
try { try {
//const sockets = await nodeIo.allSockets(); //const sockets = await nodeIo.allSockets();
//await interaction.reply(`**Online Sockets: '${sockets}'**`); //await interaction.reply(`**Online Sockets: '${sockets}'**`);
await interaction.reply("Triggering RSS update"); await interaction.reply('Triggering RSS update');
await updateFeeds(interaction.client); await updateFeeds(interaction.client);
await interaction.editReply("RSS Update Completed"); //await interaction.channel.send('**Pong.**');
//await interaction.channel.send('**Pong.**'); } catch (err) {
} catch (err) { console.error(err);
console.error(err); // await interaction.reply(err.toString());
// await interaction.reply(err.toString()); }
} }
};

View File

@@ -1,12 +1,12 @@
import { DebugBuilder } from "../../modules/debugger.mjs"; import { DebugBuilder } from "../../modules/debugger.mjs";
const log = new DebugBuilder("server", "discordBot.command.update"); const log = new DebugBuilder("server", "discordBot.command.update");
import { SlashCommandBuilder } from "discord.js"; import { SlashCommandBuilder } from 'discord.js';
import { requestNodeUpdate } from "../../modules/socketServerWrappers.mjs"; import { requestNodeUpdate } from '../../modules/socketServerWrappers.mjs';
// Exporting data property that contains the command structure for discord including any params // Exporting data property that contains the command structure for discord including any params
export const data = new SlashCommandBuilder() export const data = new SlashCommandBuilder()
.setName("update") .setName('update')
.setDescription("Updates all nodes currently logged on"); .setDescription('Updates all nodes currently logged on');
// Exporting other properties // Exporting other properties
export const example = "/update"; // An example of how the command would be run in discord chat, this will be used for the help command export const example = "/update"; // An example of how the command would be run in discord chat, this will be used for the help command
@@ -19,21 +19,19 @@ export const deferInitialReply = false; // If we the initial reply in discord sh
*/ */
export const execute = async (nodeIo, interaction) => { export const execute = async (nodeIo, interaction) => {
try { try {
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); log.DEBUG("All open sockets: ", openSockets);
// Check each open socket to see if the node has the requested system // Check each open socket to see if the node has the requested system
await Promise.all( await Promise.all(openSockets.map(openSocket => {
openSockets.map((openSocket) => { openSocket = nodeIo.sockets.sockets.get(openSocket);
openSocket = nodeIo.sockets.sockets.get(openSocket); requestNodeUpdate(openSocket);
requestNodeUpdate(openSocket); }));
}),
);
//await interaction.reply(`**Online Sockets: '${sockets}'**`); //await interaction.reply(`**Online Sockets: '${sockets}'**`);
await interaction.reply("All nodes have been requested to update"); await interaction.reply('All nodes have been requested to update');
//await interaction.channel.send('**Pong.**'); //await interaction.channel.send('**Pong.**');
} catch (err) { } catch (err) {
console.error(err); console.error(err);
// await interaction.reply(err.toString()); // await interaction.reply(err.toString());
} }
}; }

View File

@@ -1,20 +1,20 @@
import { DebugBuilder } from "../modules/debugger.mjs"; import { DebugBuilder } from "../modules/debugger.mjs";
import { Client, GatewayIntentBits, Collection } from "discord.js"; import { Client, GatewayIntentBits, Collection } from 'discord.js';
import { registerActiveCommands } from "./modules/registerCommands.mjs"; import { registerActiveCommands, unregisterAllCommands } from './modules/registerCommands.mjs'
import { RSSController } from "../rss-manager/rssController.mjs"; import { RSSController } from '../rss-manager/rssController.mjs'
import { join, dirname } from "path"; import { join, dirname } from 'path';
import { readdirSync } from "fs"; import { readdirSync } from 'fs';
import { fileURLToPath } from "url"; import { fileURLToPath } from 'url';
import PresenceManager from "./modules/presenceManager.mjs";
import dotenv from "dotenv"; import dotenv from 'dotenv';
dotenv.config(); dotenv.config()
const log = new DebugBuilder("server", "discordBot"); const log = new DebugBuilder("server", "discordBot");
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = dirname(__filename); const __dirname = dirname(__filename);
/** /**
* Add the enabled commands to the bot to be used by users in discord * Add the enabled commands to the bot to be used by users in discord
* (commands that end in '.mjs' will be enabled, to disable just remove the extension or replace with '.mjs.disabled') * (commands that end in '.mjs' will be enabled, to disable just remove the extension or replace with '.mjs.disabled')
@@ -22,41 +22,36 @@ const __dirname = dirname(__filename);
* @param {any} _commandsPath="./commands" * @param {any} _commandsPath="./commands"
* @returns {any} * @returns {any}
*/ */
export const addEnabledCommands = async ( export const addEnabledCommands = async (serverClient, _commandsPath = "./commands") => {
serverClient, // Setup commands for the Discord bot
_commandsPath = "./commands", serverClient.commands = new Collection();
) => { const commandsPath = join(__dirname, _commandsPath);
// Setup commands for the Discord bot const commandFiles = readdirSync(commandsPath).filter(file => file.endsWith('.mjs'));
serverClient.commands = new Collection();
const commandsPath = join(__dirname, _commandsPath);
const commandFiles = readdirSync(commandsPath).filter((file) =>
file.endsWith(".mjs"),
);
for (const file of commandFiles) { for (const file of commandFiles) {
const filePath = await join(commandsPath, file); const filePath = await join(commandsPath, file);
log.INFO(`Adding enabled command: ${filePath}`); log.INFO(`Adding enabled command: ${filePath}`);
await import(`file://${filePath}`).then((command) => { await import(`file://${filePath}`).then(command => {
if (command.data instanceof Promise) { if (command.data instanceof Promise) {
command.data.then(async (builder) => { command.data.then(async (builder) => {
command.data = builder; command.data = builder;
log.DEBUG("Importing command: ", command.data.name, command); log.DEBUG("Importing command: ", command.data.name, command);
// Set a new item in the Collection // Set a new item in the Collection
// With the key as the command name and the value as the exported module // With the key as the command name and the value as the exported module
serverClient.commands.set(command.data.name, command); serverClient.commands.set(command.data.name, command);
}); });
} else { } else {
log.DEBUG("Importing command: ", command.data.name, command); log.DEBUG("Importing command: ", command.data.name, command);
// Set a new item in the Collection // Set a new item in the Collection
// With the key as the command name and the value as the exported module // With the key as the command name and the value as the exported module
serverClient.commands.set(command.data.name, command); serverClient.commands.set(command.data.name, command);
} }
}); })
} }
// Register the commands currently in use by the bot // Register the commands currently in use by the bot
await registerActiveCommands(serverClient); await registerActiveCommands(serverClient);
}; }
/** /**
* Add the enabled event listeners to the bot * Add the enabled event listeners to the bot
@@ -65,64 +60,42 @@ export const addEnabledCommands = async (
* @param {any} _eventsPath="./events" * @param {any} _eventsPath="./events"
* @returns {any} * @returns {any}
*/ */
export function addEnabledEventListeners( export function addEnabledEventListeners(serverClient, _eventsPath = "./events") {
serverClient, const eventsPath = join(__dirname, _eventsPath);
_eventsPath = "./events", const eventFiles = readdirSync(eventsPath).filter(file => file.endsWith('.mjs'));
) {
const eventsPath = join(__dirname, _eventsPath);
const eventFiles = readdirSync(eventsPath).filter((file) =>
file.endsWith(".mjs"),
);
for (const file of eventFiles) { for (const file of eventFiles) {
const filePath = join(eventsPath, file); const filePath = join(eventsPath, file);
log.INFO(`Adding enabled event listener: ${filePath}`); log.INFO(`Adding enabled event listener: ${filePath}`);
import(`file://${filePath}`).then((event) => { import(`file://${filePath}`).then(event => {
log.DEBUG("Adding event: ", event); log.DEBUG("Adding event: ", event);
if (event.once) { if (event.once) {
serverClient.once(event.name, (...args) => serverClient.once(event.name, (...args) => event.execute(serverClient.nodeIo, ...args));
event.execute(serverClient.nodeIo, ...args), } else {
); serverClient.on(event.name, (...args) => event.execute(serverClient.nodeIo, ...args));
} else { }
serverClient.on(event.name, (...args) => })
event.execute(serverClient.nodeIo, ...args), }
);
}
});
}
} }
// The discord client // The discord client
export const serverClient = new Client({ export const serverClient = new Client({ intents: [GatewayIntentBits.Guilds, GatewayIntentBits.GuildVoiceStates] });
intents: [
GatewayIntentBits.Guilds,
GatewayIntentBits.GuildVoiceStates,
GatewayIntentBits.GuildMessages,
GatewayIntentBits.MessageContent,
GatewayIntentBits.GuildMembers,
GatewayIntentBits.GuildPresences,
],
});
// Run when the bot is ready // Run when the bot is ready
serverClient.on("ready", async () => { serverClient.on('ready', async () => {
log.INFO(`Logged in as ${serverClient.user.tag}!`); log.INFO(`Logged in as ${serverClient.user.tag}!`);
// Set the presence to default // Add and register commands
const pm = new PresenceManager(serverClient); await addEnabledCommands(serverClient);
await pm.resetToDefault();
// Add and register commands // Config the discord bot with events
await addEnabledCommands(serverClient); await addEnabledEventListeners(serverClient);
// Config the discord bot with events // Start the RSS Controller
await addEnabledEventListeners(serverClient); serverClient.RSSController = await new RSSController(serverClient);
serverClient.RSSController.start();
// Start the RSS Controller log.INFO("RSS Controller:", serverClient.RSSController);
serverClient.RSSController = await new RSSController(serverClient);
serverClient.RSSController.start();
log.INFO("RSS Controller:", serverClient.RSSController);
}); });
// Startup the discord bot // Startup the discord bot

View File

@@ -1,19 +0,0 @@
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

@@ -1,36 +0,0 @@
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";
import { getGuildConfig } from "../../modules/mongo-wrappers/mongoConfigWrappers.mjs";
export const name = Events.GuildMemberAdd;
export async function execute(nodeIo, member) {
const welcomeChannel = await getGuildConfig(
message.guild.id,
"welcomeChannelId",
);
log.INFO("New user joined the server", member);
let conversation = [];
conversation.push({
role: "assistant",
content: `A new user has joined the server. 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

@@ -1,7 +1,6 @@
import { DebugBuilder } from "../../modules/debugger.mjs"; import { DebugBuilder } from "../../modules/debugger.mjs";
const log = new DebugBuilder("server", "discordBot.events.interactionCreate"); const log = new DebugBuilder("server", "discordBot.events.interactionCreate");
import { Events } from "discord.js"; import { Events } from 'discord.js';
import PresenceManager from "../modules/presenceManager.mjs";
export const name = Events.InteractionCreate; export const name = Events.InteractionCreate;
@@ -9,10 +8,6 @@ export async function execute(nodeIo, interaction) {
const command = interaction.client.commands.get(interaction.commandName); const command = interaction.client.commands.get(interaction.commandName);
log.INFO("Interaction created for command: ", command); log.INFO("Interaction created for command: ", command);
// Set the presence for handling interaction
const interactionPm = new PresenceManager(interaction.client);
await interactionPm.setPresence("online", "PLAYING", "handling interaction");
// Execute autocomplete if the user is checking autocomplete // Execute autocomplete if the user is checking autocomplete
if (interaction.isAutocomplete()) { if (interaction.isAutocomplete()) {
log.INFO("Running autocomplete for command: ", command.data.name); log.INFO("Running autocomplete for command: ", command.data.name);
@@ -27,9 +22,7 @@ export async function execute(nodeIo, interaction) {
return; return;
} }
log.INFO( log.INFO(`${interaction.member.user} is running '${interaction.commandName}'`);
`${interaction.member.user} is running '${interaction.commandName}'`,
);
// Defer the initial reply if the command has the parameter set // Defer the initial reply if the command has the parameter set
if (command.deferInitialReply) { if (command.deferInitialReply) {
@@ -38,7 +31,4 @@ export async function execute(nodeIo, interaction) {
// Execute the command // Execute the command
command.execute(nodeIo, interaction); command.execute(nodeIo, interaction);
}
// Reset the presence
await interactionPm.resetToDefault();
}

View File

@@ -1,55 +0,0 @@
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";
import PresenceManager from "../modules/presenceManager.mjs";
import { getGuildConfig } from "../../modules/mongo-wrappers/mongoConfigWrappers.mjs";
export const name = Events.MessageCreate;
export async function execute(nodeIo, message) {
// Get the ignored channels from the server config
const IGNORED_CHANNELS = await getGuildConfig(
message.guild.id,
"ignoredChannels",
);
// Ignore ignored channels
if (
!Array.isArray(IGNORED_CHANNELS) ||
(Array.isArray(IGNORED_CHANNELS) &&
IGNORED_CHANNELS.includes(message.channel.id))
) {
return;
}
// Ignore messages from a bot
if (message.author.bot) {
return;
}
log.INFO("Message create", message);
// Set presence for reading message
const messagePm = new PresenceManager(message.client);
await messagePm.setPresence("online", "WATCHING", "latest messages");
// Check if the message mentions the bot
if (message.mentions.users.has(nodeIo.serverClient.user.id)) {
const interaction = await gptInteraction(nodeIo, message);
// Reset the presence
await messagePm.resetToDefault();
return interaction;
}
// Check if the message contains a link in a channel it shouldn't
await linkCop(nodeIo, message);
// Reset the presence
await messagePm.resetToDefault();
}

View File

@@ -1,117 +0,0 @@
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,112 +0,0 @@
import {
getConfig,
setConfig,
} from "../../modules/mongo-wrappers/mongoConfigWrappers.mjs";
import { ActivityType, PresenceUpdateStatus } from "discord.js";
class PresenceManager {
/**
* Creates an instance of PresenceManager.
* @param {import('discord.js').Client} client - The Discord client instance.
*/
constructor(client) {
this.client = client;
}
/**
* 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).
*/
async setPresence(status, activityType, activityName, url = null) {
const activityOptions = {
type: this.convertActivityType(activityType),
name: activityName,
};
if (activityType.toUpperCase() === "STREAMING" && url) {
activityOptions.url = url;
}
await this.client.user.setPresence({
status: this.convertStatus(status),
activities: [activityOptions],
});
}
/**
* Reset the bot's presence to the default state.
*/
async resetToDefault() {
let defaultPresence = await getConfig("presence");
if (!defaultPresence) {
defaultPresence = {
status: "idle",
activities: [
{
name: "your commands",
type: "LISTENING",
},
],
};
await setConfig("presence", defaultPresence);
}
console.log("Default Presence:", defaultPresence);
// Update your bot's presence using this configuration
await this.setPresence(
defaultPresence.status,
defaultPresence.activities[0].type,
defaultPresence.activities[0].name,
);
}
/**
* Convert a string activity type to the corresponding ActivityType enum.
* @param {string} activityType - The activity type string.
* @returns {ActivityType} - The corresponding ActivityType enum.
*/
convertActivityType(activityType) {
switch (activityType.toUpperCase()) {
case "PLAYING":
return ActivityType.Playing;
case "STREAMING":
return ActivityType.Streaming;
case "LISTENING":
return ActivityType.Listening;
case "WATCHING":
return ActivityType.Watching;
case "COMPETING":
return ActivityType.Competing;
default:
throw new Error("Invalid activity type");
}
}
/**
* Convert a string status to the corresponding PresenceUpdateStatus enum.
* @param {string} status - The status string.
* @returns {PresenceUpdateStatus} - The corresponding PresenceUpdateStatus enum.
*/
convertStatus(status) {
switch (status.toLowerCase()) {
case "online":
return PresenceUpdateStatus.Online;
case "idle":
return PresenceUpdateStatus.Idle;
case "dnd":
return PresenceUpdateStatus.DoNotDisturb;
case "invisible":
return PresenceUpdateStatus.Invisible;
default:
throw new Error("Invalid status");
}
}
}
export default PresenceManager;

View File

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

View File

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

View File

@@ -1,27 +1,15 @@
import { DebugBuilder } from "../../modules/debugger.mjs"; import { DebugBuilder } from "../../modules/debugger.mjs";
const log = new DebugBuilder("server", "discordBot.modules.wrappers"); const log = new DebugBuilder("server", "discordBot.modules.wrappers");
import { import { checkIfNodeIsConnectedToVC, getNodeDiscordID, getNodeDiscordUsername } from '../../modules/socketServerWrappers.mjs';
checkIfNodeIsConnectedToVC, import { getAllDiscordIDs } from '../../modules/mongo-wrappers/mongoDiscordIDWrappers.mjs'
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) => { export const checkOnlineBotsInGuild = async (nodeIo, guildId) => {
let onlineBots = []; let onlineBots = [];
const openSockets = [...(await nodeIo.allSockets())]; const openSockets = [...await nodeIo.allSockets()];
await Promise.all( await Promise.all(openSockets.map(async openSocket => {
openSockets.map(async (openSocket) => {
openSocket = await nodeIo.sockets.sockets.get(openSocket); openSocket = await nodeIo.sockets.sockets.get(openSocket);
const connected = await checkIfNodeIsConnectedToVC( const connected = await checkIfNodeIsConnectedToVC(nodeIo, guildId, openSocket.node.nuid);
nodeIo,
guildId,
openSocket.node.nuid,
);
log.INFO("Connected:", connected); log.INFO("Connected:", connected);
if (connected) { if (connected) {
const username = await getNodeDiscordUsername(openSocket, guildId); const username = await getNodeDiscordUsername(openSocket, guildId);
@@ -29,211 +17,34 @@ export const checkOnlineBotsInGuild = async (nodeIo, guildId) => {
onlineBots.push({ onlineBots.push({
name: username, name: username,
discord_id: discordID, discord_id: discordID,
nuid: openSocket.node.nuid, nuid: openSocket.node.nuid
}); });
} }
}), }));
);
return onlineBots;
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 = []; export const getAvailableTokensInGuild = async (nodeIo, guildId) => {
// Check each open socket to see if the node has the requested system try {
await Promise.all( // Execute both asynchronous functions concurrently
openSockets.map(async (openSocket) => { const [discordIDs, onlineBots] = await Promise.all([
openSocket = await nodeIo.sockets.sockets.get(openSocket); getAllDiscordIDs(), // Fetch all Discord IDs
// Check if the node has an existing open client (meaning the radio is already being listened to) checkOnlineBotsInGuild(nodeIo, guildId) // Check online bots in the guild
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 // Use the results of both promises here
const connected = await checkIfNodeIsConnectedToVC( log.INFO("Available Discord IDs:", discordIDs);
nodeIo, log.INFO("Online bots in the guild:", onlineBots);
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( // Filter any discordIDs that are not active
"Availble nodes:", const availableDiscordIDs = discordIDs.filter(discordID => discordID.active == true).filter(discordID => !onlineBots.some(bot => Number(bot.discord_id) == discordID.discord_id));
availableNodes.map((socket) => socket.node.name),
);
return availableNodes; // Return the unavailable discordIDs
}; return availableDiscordIDs;
} catch (error) {
/** console.error('Error getting available tokens in guild:', error);
* Gets the voice channel the user is currently in. throw error;
* @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: [],
});
}
};

View File

@@ -1,47 +0,0 @@
import path from "path";
import { fileURLToPath } from "url";
import { FlatCompat } from "@eslint/eslintrc";
import mjs from "@eslint/js";
import prettierConfig from "eslint-config-prettier";
import eslintPluginPrettierRecommended from "eslint-plugin-prettier/recommended";
import unusedImports from "eslint-plugin-unused-imports";
const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename);
const compat = new FlatCompat({
baseDirectory: __dirname,
recommendedConfig: mjs.configs.recommended,
});
export default [
// Apply ESLint recommended settings first
...compat.extends().map((config) => ({
...config,
files: ["**/*.mjs", "**/*.js", "**/*.cjs"],
})),
// Custom rules and plugin configuration
{
plugins: {
"unused-imports": unusedImports,
},
rules: {
// Custom rules here
"no-console": "warn",
"no-unused-vars": "off", // or "@typescript-eslint/no-unused-vars": "off",
"unused-imports/no-unused-imports": "error",
"unused-imports/no-unused-vars": [
"warn",
{
vars: "all",
varsIgnorePattern: "^_",
args: "after-used",
argsIgnorePattern: "^_",
},
],
"prettier/prettier": "warn", // Integrate prettier
},
},
prettierConfig, // Turns off all ESLint rules that have the potential to interfere with Prettier rules.
eslintPluginPrettierRecommended,
];

View File

@@ -1,14 +0,0 @@
{
"source": {
"include": ["addons", "discordBot", "modules", "rss-manager", "test"],
"exclude": ["node_modules"],
"includePattern": ".+\\.[mc]?js(doc|x)?$",
"excludePattern": "(^|\\/|\\\\)_"
},
"opts": {
"destination": "./docs",
"recurse": true,
"template": "default"
}
}

View File

@@ -20,13 +20,6 @@ run:
-e SERVER_PORT=${SERVER_PORT} \ -e SERVER_PORT=${SERVER_PORT} \
-e MONGO_URL=${MONGO_URL} \ -e MONGO_URL=${MONGO_URL} \
-e DISCORD_TOKEN=${DISCORD_TOKEN} \ -e DISCORD_TOKEN=${DISCORD_TOKEN} \
-e RSS_REFRESH_INTERVAL=${RSS_REFRESH_INTERVAL} \
-e WELCOME_CHANNEL_ID=${WELCOME_CHANNEL_ID} \
-e IGNORED_CHANNEL_IDS=${IGNORED_CHANNEL_IDS} \
-e LINKCOP_RESTRICTED_CHANNEL_IDS=${LINKCOP_RESTRICTED_CHANNEL_IDS} \
-e DRB_SERVER_INITIAL_PROMPT=${DRB_SERVER_INITIAL_PROMPT} \
-e OPENAI_API_KEY=${OPENAI_API_KEY} \
-e LOG_LOCATION="./logs/server.log" \
-p ${SERVER_PORT}:${SERVER_PORT} \ -p ${SERVER_PORT}:${SERVER_PORT} \
--name=drb \ --name=drb \
$(DOCKER_IMAGE_NAME) $(DOCKER_IMAGE_NAME)

View File

@@ -1,45 +1,33 @@
import { DebugBuilder } from "../modules/debugger.mjs"; import { DebugBuilder } from "../modules/debugger.mjs";
const log = new DebugBuilder("server", "addonManager"); const log = new DebugBuilder("server", "addonManager");
import { fileURLToPath } from "url"; import { fileURLToPath } from 'url';
import fs from "fs"; import fs from 'fs';
import path from "path"; import path from 'path';
// Function to load addons from the addons directory // Function to load addons from the addons directory
export const loadAddons = async (nodeIo) => { export const loadAddons = async (nodeIo) => {
const __filename = fileURLToPath(import.meta.url); const __filename = fileURLToPath(import.meta.url);
const __dirname = path.dirname(__filename); const __dirname = path.dirname(__filename);
const addonsDir = path.join(__dirname, "../addons"); const addonsDir = path.join(__dirname, '../addons');
// Read the directory containing addon modules // Read the directory containing addon modules
const addonDirectories = await fs.readdirSync(addonsDir, { const addonDirectories = await fs.readdirSync(addonsDir, { withFileTypes: true });
withFileTypes: true,
});
addonDirectories.forEach((addonDir) => { addonDirectories.forEach(addonDir => {
if (addonDir.isDirectory()) { if (addonDir.isDirectory()) {
const addonConfigPath = path.join( const addonConfigPath = path.join(addonsDir, addonDir.name, 'config.json');
addonsDir, if (fs.existsSync(addonConfigPath)) {
addonDir.name, const addonConfig = JSON.parse(fs.readFileSync(addonConfigPath, 'utf-8'));
"config.json", if (addonConfig.enabled) {
); const addonIndexPath = path.join(addonsDir, addonDir.name, 'index.js');
if (fs.existsSync(addonConfigPath)) { import(`file://${addonIndexPath}`).then(addonModule => {
const addonConfig = JSON.parse( log.DEBUG("Loading addon: ", addonModule);
fs.readFileSync(addonConfigPath, "utf-8"), addonModule.initialize(nodeIo, addonConfig);
); log.DEBUG(`Addon ${addonConfig.name} loaded.`);
if (addonConfig.enabled) { });
const addonIndexPath = path.join( }
addonsDir, }
addonDir.name,
"index.js",
);
import(`file://${addonIndexPath}`).then((addonModule) => {
log.DEBUG("Loading addon: ", addonModule);
addonModule.initialize(nodeIo, addonConfig);
log.DEBUG(`Addon ${addonConfig.name} loaded.`);
});
} }
} });
} }
});
};

View File

@@ -1,10 +1,10 @@
// Import necessary modules // Import necessary modules
import debug from "debug"; import debug from 'debug';
import { config } from "dotenv"; import { config } from 'dotenv';
config(); config();
import { promises as fs } from "fs"; import { promises as fs } from 'fs';
import { join, dirname } from "path"; import { join, dirname } from 'path';
import { inspect } from "util"; import { inspect } from 'util';
/** /**
* Write a given message to the log file * Write a given message to the log file
@@ -12,27 +12,24 @@ import { inspect } from "util";
* @param {string} appName The app name that created the log entry * @param {string} appName The app name that created the log entry
*/ */
const writeToLog = async (logMessage, appName) => { const writeToLog = async (logMessage, appName) => {
const logLocation = join(process.env.LOG_LOCATION ?? `./logs/${appName}.log`); const logLocation = join(process.env.LOG_LOCATION ?? `./logs/${appName}.log`);
// Ensure the log directory exists // Ensure the log directory exists
try { try {
await fs.mkdir(dirname(logLocation), { recursive: true }); await fs.mkdir(dirname(logLocation), { recursive: true });
} catch (err) { } catch (err) {
console.error(err); console.error(err);
} }
// Ensure the message is a string // Ensure the message is a string
logMessage = `${String(logMessage)}\n`; logMessage = `${String(logMessage)}\n`;
// Write to the file // Write to the file
try { try {
await fs.writeFile(logLocation, logMessage, { await fs.writeFile(logLocation, logMessage, { encoding: 'utf-8', flag: 'a+' });
encoding: "utf-8", } catch (err) {
flag: "a+", console.error(err);
}); }
} catch (err) {
console.error(err);
}
}; };
/** /**
@@ -42,37 +39,33 @@ const writeToLog = async (logMessage, appName) => {
* @param {string} fileName The name of the file calling the builder to be used in the 'fileName' portion of the namespace * @param {string} fileName The name of the file calling the builder to be used in the 'fileName' portion of the namespace
*/ */
export class DebugBuilder { export class DebugBuilder {
constructor(appName, fileName) { constructor(appName, fileName) {
const buildLogger = const buildLogger = (level) => (...messageParts) => {
(level) => const logger = debug(`${appName}:${fileName}:${level}`);
(...messageParts) => { logger(messageParts);
const logger = debug(`${appName}:${fileName}:${level}`);
logger(messageParts);
const timeStamp = new Date().toLocaleString("en-US", { const timeStamp = new Date().toLocaleString('en-US', { timeZone: 'America/New_York' });
timeZone: "America/New_York", const message = `${timeStamp} - ${appName}:${fileName}:${level}\t-\t${messageParts.map(part => inspect(part)).join(' ')}`;
});
const message = `${timeStamp} - ${appName}:${fileName}:${level}\t-\t${messageParts.map((part) => inspect(part)).join(" ")}`;
// Write to console // Write to console
console.log(message); console.log(message);
// Write to logfile
writeToLog(message, appName);
};
// Write to logfile this.INFO = buildLogger('INFO');
writeToLog(message, appName); this.DEBUG = buildLogger('DEBUG');
}; this.VERBOSE = buildLogger('VERBOSE');
this.WARN = buildLogger('WARNING');
this.ERROR = (...messageParts) => {
buildLogger('ERROR')(...messageParts);
this.INFO = buildLogger("INFO"); if (process.env.EXIT_ON_ERROR && process.env.EXIT_ON_ERROR > 0) {
this.DEBUG = buildLogger("DEBUG"); writeToLog("!--- EXITING ---!", appName);
this.VERBOSE = buildLogger("VERBOSE"); const exitDelay = parseInt(process.env.EXIT_ON_ERROR_DELAY, 10) || 0;
this.WARN = buildLogger("WARNING"); setTimeout(() => process.exit(1), exitDelay);
this.ERROR = (...messageParts) => { }
buildLogger("ERROR")(...messageParts); };
}
if (process.env.EXIT_ON_ERROR && process.env.EXIT_ON_ERROR > 0) {
writeToLog("!--- EXITING ---!", appName);
const exitDelay = parseInt(process.env.EXIT_ON_ERROR_DELAY, 10) || 0;
setTimeout(() => process.exit(1), exitDelay);
}
};
}
} }

View File

@@ -1,113 +0,0 @@
import {
getDocumentByField,
deleteDocumentByField,
getDocumentByFields,
upsertDocumentByField,
deleteDocumentByFields,
upsertDocumentByFields,
} from "./mongoHandler.mjs"; // Import your MongoDB handlers
import { DebugBuilder } from "../debugger.mjs";
const log = new DebugBuilder("server", "mongoConfigWrappers");
const collectionName = "configurations";
// Function to get a configuration by key
export const getConfig = async (key) => {
try {
const config = await getDocumentByField(collectionName, "key", key);
log.DEBUG(`Configuration for key "${key}" retrieved:`, config);
return config ? config[key] : null; // Return null if no configuration is found
} catch (error) {
log.ERROR("Error retrieving configuration:", error);
throw error;
}
};
// Function to set a configuration by key
export const setConfig = async (key, value) => {
// Set the config object
value = { key: value };
try {
const result = await upsertDocumentByField(
collectionName,
"key",
key,
value,
);
log.DEBUG(`Configuration for key "${key}" set:`, value, result);
return result > 0 ? key : null; // Return key if updated successfully, otherwise null
} catch (error) {
log.ERROR("Error setting configuration:", error);
throw error;
}
};
// Function to delete a configuration by key (optional)
export const deleteConfig = async (key) => {
try {
const result = await deleteDocumentByField(collectionName, "key", key);
log.DEBUG(`Configuration for key "${key}" deleted:`, result);
return result; // Return the count of deleted documents
} catch (error) {
log.ERROR("Error deleting configuration:", error);
throw error;
}
};
// Function to get a configuration by key for a specific guild
export const getGuildConfig = async (guildId, key) => {
try {
const config = await getDocumentByFields(
collectionName,
["guild", Number(guildId)],
["key", key],
);
log.DEBUG(
`Guild ${guildId} configuration for key "${key}" retrieved:`,
config,
);
return config ? config[key] : null; // Return null if no configuration is found
} catch (error) {
log.ERROR("Error retrieving guild configuration:", error);
throw error;
}
};
// Function to set a configuration by key for a specific guild
export const setGuildConfig = async (guildId, key, value) => {
// Set the config object
value = { key: value };
try {
const result = await upsertDocumentByFields(
collectionName,
value,
["guild", Number(guildId)],
["key", key],
);
log.DEBUG(`Guild ${guildId} configuration for key "${key}" set:`, value);
return result > 0 ? key : null; // Return key if updated successfully, otherwise null
} catch (error) {
log.ERROR("Error setting guild configuration:", error);
throw error;
}
};
// Function to delete a configuration by key for a specific guild (optional)
export const deleteGuildConfig = async (guildId, key) => {
try {
const result = await deleteDocumentByFields(
collectionName,
["guild", Number(guildId)],
["key", key],
);
log.DEBUG(
`Guild ${guildId} configuration for key "${key}" deleted:`,
result,
);
return result; // Return the count of deleted documents
} catch (error) {
log.ERROR("Error deleting guild configuration:", error);
throw error;
}
};

View File

@@ -1,12 +1,8 @@
import { DebugBuilder } from "../../modules/debugger.mjs"; import { DebugBuilder } from "../../modules/debugger.mjs";
const log = new DebugBuilder("server", "mongoDiscordIDWrappers"); const log = new DebugBuilder("server", "mongoDiscordIDWrappers");
import { import { insertDocument, getDocuments, connectToDatabase } from "./mongoHandler.mjs";
insertDocument,
getDocuments,
connectToDatabase,
} from "./mongoHandler.mjs";
const collectionName = "discord-ids"; const collectionName = 'discord-ids';
// Wrapper for inserting a Discord ID // Wrapper for inserting a Discord ID
export const createDiscordID = async (discordID) => { export const createDiscordID = async (discordID) => {
@@ -14,7 +10,7 @@ export const createDiscordID = async (discordID) => {
const insertedId = await insertDocument(collectionName, discordID); const insertedId = await insertDocument(collectionName, discordID);
return insertedId; return insertedId;
} catch (error) { } catch (error) {
log.ERROR("Error creating Discord ID:", error); log.ERROR('Error creating Discord ID:', error);
throw error; throw error;
} }
}; };
@@ -25,7 +21,7 @@ export const getAllDiscordIDs = async () => {
const discordIDs = await getDocuments(collectionName); const discordIDs = await getDocuments(collectionName);
return discordIDs; return discordIDs;
} catch (error) { } catch (error) {
log.ERROR("Error getting all Discord IDs:", error); log.ERROR('Error getting all Discord IDs:', error);
throw error; throw error;
} }
}; };
@@ -33,14 +29,17 @@ export const getAllDiscordIDs = async () => {
// Wrapper for retrieving a Discord ID by name or discord_id // Wrapper for retrieving a Discord ID by name or discord_id
export const getDiscordID = async (identifier) => { export const getDiscordID = async (identifier) => {
const db = await connectToDatabase(); const db = await connectToDatabase();
try { try {
const collection = db.db().collection(collectionName); const collection = db.db().collection(collectionName);
const discordID = await collection.findOne({ const discordID = await collection.findOne({
$or: [{ name: identifier }, { discord_id: identifier }], $or: [
{ name: identifier },
{ discord_id: identifier }
]
}); });
return discordID; return discordID;
} catch (error) { } catch (error) {
log.ERROR("Error getting Discord ID:", error); log.ERROR('Error getting Discord ID:', error);
throw error; throw error;
} finally { } finally {
// Close the connection // Close the connection
@@ -53,16 +52,16 @@ export const updateDiscordID = async (identifier, updatedFields) => {
const db = await connectToDatabase(); const db = await connectToDatabase();
try { try {
const collection = db.db().collection(collectionName); const collection = db.db().collection(collectionName);
const result = await collection.updateOne( const result = await collection.updateOne({
{ $or: [
$or: [{ name: identifier }, { discord_id: identifier }], { name: identifier },
}, { discord_id: identifier }
{ $set: updatedFields }, ]
); }, { $set: updatedFields });
log.INFO("Discord ID updated:", result.modifiedCount); log.INFO('Discord ID updated:', result.modifiedCount);
return result.modifiedCount; return result.modifiedCount;
} catch (error) { } catch (error) {
log.ERROR("Error updating Discord ID:", error); log.ERROR('Error updating Discord ID:', error);
throw error; throw error;
} finally { } finally {
// Close the connection // Close the connection
@@ -73,18 +72,21 @@ export const updateDiscordID = async (identifier, updatedFields) => {
// Wrapper for deleting a Discord ID by name or discord_id // Wrapper for deleting a Discord ID by name or discord_id
export const deleteDiscordID = async (identifier) => { export const deleteDiscordID = async (identifier) => {
const db = await connectToDatabase(); const db = await connectToDatabase();
try { try {
const collection = db.db().collection(collectionName); const collection = db.db().collection(collectionName);
const result = await collection.deleteOne({ const result = await collection.deleteOne({
$or: [{ name: identifier }, { discord_id: identifier }], $or: [
{ name: identifier },
{ discord_id: identifier }
]
}); });
log.INFO("Discord ID deleted:", result.deletedCount); log.INFO('Discord ID deleted:', result.deletedCount);
return result.deletedCount; return result.deletedCount;
} catch (error) { } catch (error) {
log.ERROR("Error deleting Discord ID:", error); log.ERROR('Error deleting Discord ID:', error);
throw error; throw error;
} finally { } finally {
// Close the connection // Close the connection
await db.close(); await db.close();
} }
}; };

View File

@@ -1,124 +1,112 @@
import { DebugBuilder } from "../../modules/debugger.mjs"; import { DebugBuilder } from "../../modules/debugger.mjs";
const log = new DebugBuilder("server", "mongoFeedsWrappers"); const log = new DebugBuilder("server", "mongoFeedsWrappers");
import { import {
insertDocument, insertDocument,
getDocuments, getDocuments,
getDocumentByField, getDocumentByField,
updateDocumentByField, updateDocumentByField,
deleteDocumentByField, deleteDocumentByField,
} from "./mongoHandler.mjs"; } from "./mongoHandler.mjs";
const feedCollectionName = 'feeds';
const postCollectionName = 'posts';
// Wrapper for inserting a feed
export const createFeed = async (feed) => {
try {
const insertedId = await insertDocument(feedCollectionName, feed);
return insertedId;
} catch (error) {
log.ERROR('Error creating feed:', error);
throw error;
}
};
// Wrapper for retrieving all feeds
export const getAllFeeds = async () => {
try {
const feeds = await getDocuments(feedCollectionName);
return feeds;
} catch (error) {
log.ERROR('Error getting all feeds:', error);
throw error;
}
};
// Wrapper for retrieving a feed by link
export const getFeedByLink = async (link) => {
try {
const feed = await getDocumentByField(feedCollectionName, 'link', link);
return feed;
} catch (error) {
log.ERROR('Error getting feed by link:', error);
throw error;
}
};
const feedCollectionName = "feeds"; // Wrapper for retrieving a feed by the title
const postCollectionName = "posts"; export const getFeedByTitle = async (title) => {
try {
const feed = await getDocumentByField(feedCollectionName, 'title', title);
return feed;
} catch (error) {
log.ERROR('Error getting feed by link:', error);
throw error;
}
};
// Wrapper for updating a feed by link
export const updateFeedByLink = async (link, updatedFields) => {
try {
const modifiedCount = await updateDocumentByField(feedCollectionName, 'link', link, updatedFields);
return modifiedCount;
} catch (error) {
log.ERROR('Error updating feed by link:', error);
throw error;
}
};
// Wrapper for deleting a feed by link
export const deleteFeedByLink = async (link) => {
try {
const deletedCount = await deleteDocumentByField(feedCollectionName, 'link', link);
return deletedCount;
} catch (error) {
log.ERROR('Error deleting feed by link:', error);
throw error;
}
};
// Wrapper for inserting a feed // Wrapper for deleting a feed by title
export const createFeed = async (feed) => { export const deleteFeedByTitle = async (title) => {
try { try {
const insertedId = await insertDocument(feedCollectionName, feed); const deletedCount = await deleteDocumentByField(feedCollectionName, 'title', title);
return insertedId; return deletedCount;
} catch (error) { } catch (error) {
log.ERROR("Error creating feed:", error); log.ERROR('Error deleting feed by link:', error);
throw error; throw error;
} }
}; };
// Wrapper for retrieving all feeds // Wrapper for inserting a post
export const getAllFeeds = async () => { export const createPost = async (post) => {
try { try {
const feeds = await getDocuments(feedCollectionName); const insertedId = await insertDocument(postCollectionName, post);
return feeds; return insertedId;
} catch (error) { } catch (error) {
log.ERROR("Error getting all feeds:", error); log.ERROR('Error creating post:', error);
throw error; throw error;
} }
}; };
// Wrapper for retrieving a feed by link // Wrapper for retrieving a post by postId
export const getFeedByLink = async (link) => { export const getPostByPostId = async (postId) => {
try { try {
const feed = await getDocumentByField(feedCollectionName, "link", link); const post = await getDocumentByField(postCollectionName, 'postId', postId);
return feed; return post;
} catch (error) { } catch (error) {
log.ERROR("Error getting feed by link:", error); log.ERROR('Error getting post by postId:', error);
throw error; throw error;
} }
}; };
// Wrapper for retrieving a feed by the title
export const getFeedByTitle = async (title) => {
try {
const feed = await getDocumentByField(feedCollectionName, "title", title);
return feed;
} catch (error) {
log.ERROR("Error getting feed by link:", error);
throw error;
}
};
// Wrapper for updating a feed by link
export const updateFeedByLink = async (link, updatedFields) => {
try {
const modifiedCount = await updateDocumentByField(
feedCollectionName,
"link",
link,
updatedFields,
);
return modifiedCount;
} catch (error) {
log.ERROR("Error updating feed by link:", error);
throw error;
}
};
// Wrapper for deleting a feed by link
export const deleteFeedByLink = async (link) => {
try {
const deletedCount = await deleteDocumentByField(
feedCollectionName,
"link",
link,
);
return deletedCount;
} catch (error) {
log.ERROR("Error deleting feed by link:", error);
throw error;
}
};
// Wrapper for deleting a feed by title
export const deleteFeedByTitle = async (title) => {
try {
const deletedCount = await deleteDocumentByField(
feedCollectionName,
"title",
title,
);
return deletedCount;
} catch (error) {
log.ERROR("Error deleting feed by link:", error);
throw error;
}
};
// Wrapper for inserting a post
export const createPost = async (post) => {
try {
const insertedId = await insertDocument(postCollectionName, post);
return insertedId;
} catch (error) {
log.ERROR("Error creating post:", error);
throw error;
}
};
// Wrapper for retrieving a post by postId
export const getPostByPostId = async (postId) => {
try {
const post = await getDocumentByField(postCollectionName, "postId", postId);
return post;
} catch (error) {
log.ERROR("Error getting post by postId:", error);
throw error;
}
};

View File

@@ -1,10 +1,10 @@
// Import necessary modules // Import necessary modules
import { MongoClient } from "mongodb"; import { MongoClient } from 'mongodb';
import { DebugBuilder } from "../debugger.mjs"; import { DebugBuilder } from '../debugger.mjs';
const log = new DebugBuilder("server", "mongoHandler"); const log = new DebugBuilder("server", 'mongoHandler');
import dotenv from "dotenv"; import dotenv from 'dotenv';
dotenv.config(); dotenv.config()
// MongoDB connection URI // MongoDB connection URI
const uri = process.env.MONGO_URL; const uri = process.env.MONGO_URL;
@@ -15,7 +15,7 @@ export const connectToDatabase = async () => {
const client = await MongoClient.connect(uri); const client = await MongoClient.connect(uri);
return client; return client;
} catch (error) { } catch (error) {
console.error("Error connecting to the database:", error); console.error('Error connecting to the database:', error);
throw error; throw error;
} }
}; };
@@ -24,13 +24,13 @@ export const connectToDatabase = async () => {
export const insertDocument = async (collectionName, document) => { export const insertDocument = async (collectionName, document) => {
const db = await connectToDatabase(); const db = await connectToDatabase();
log.DEBUG("Inserting document:", collectionName, document); log.DEBUG("Inserting document:", collectionName, document);
try { try {
const collection = db.db().collection(collectionName); const collection = db.db().collection(collectionName);
const result = await collection.insertOne(document); const result = await collection.insertOne(document);
log.DEBUG("Document inserted:", result.insertedId); log.DEBUG('Document inserted:', result.insertedId);
return result.insertedId; return result.insertedId;
} catch (error) { } catch (error) {
console.error("Error inserting document:", error); console.error('Error inserting document:', error);
throw error; throw error;
} finally { } finally {
// Close the connection // Close the connection
@@ -42,13 +42,13 @@ export const insertDocument = async (collectionName, document) => {
export const getDocuments = async (collectionName) => { export const getDocuments = async (collectionName) => {
log.DEBUG("Getting all documents:", collectionName); log.DEBUG("Getting all documents:", collectionName);
const db = await connectToDatabase(); const db = await connectToDatabase();
try { try {
const collection = db.db().collection(collectionName); const collection = db.db().collection(collectionName);
const documents = await collection.find({}).toArray(); const documents = await collection.find({}).toArray();
log.DEBUG("Documents retrieved:", documents); log.DEBUG('Documents retrieved:', documents);
return documents; return documents;
} catch (error) { } catch (error) {
console.error("Error retrieving documents:", error); console.error('Error retrieving documents:', error);
throw error; throw error;
} finally { } finally {
// Close the connection // Close the connection
@@ -59,29 +59,13 @@ export const getDocuments = async (collectionName) => {
// Function to retrieve a document by a specific field // Function to retrieve a document by a specific field
export const getDocumentByField = async (collectionName, field, value) => { export const getDocumentByField = async (collectionName, field, value) => {
log.DEBUG("Getting document by field:", collectionName, field, value); log.DEBUG("Getting document by field:", collectionName, field, value);
return await getDocumentByFields(collectionName, [field, value]);
};
// Function to retrieve a document by multiple fields
export const getDocumentByFields = async (
collectionName,
...fieldValuePairs
) => {
log.DEBUG("Getting document by fields:", collectionName, fieldValuePairs);
const db = await connectToDatabase(); const db = await connectToDatabase();
try { try {
const collection = db.db().collection(collectionName); const collection = db.db().collection(collectionName);
const document = await collection.findOne({ [field]: value });
// Convert the fieldValuePairs array into an object
const query = fieldValuePairs.reduce((acc, [field, value]) => {
acc[field] = value;
return acc;
}, {});
const document = await collection.findOne(query);
return document; return document;
} catch (error) { } catch (error) {
console.error("Error retrieving document:", error); console.error('Error retrieving document:', error);
throw error; throw error;
} finally { } finally {
await db.close(); await db.close();
@@ -89,102 +73,16 @@ export const getDocumentByFields = async (
}; };
// Function to update a document by a specific field // Function to update a document by a specific field
export const upsertDocumentByField = async ( export const updateDocumentByField = async (collectionName, field, value, updatedFields) => {
collectionName, log.DEBUG("Update document by field:", collectionName, field, value, updatedFields);
field,
value,
updatedFields,
) => {
log.DEBUG(
"Upsert document by field:",
collectionName,
field,
value,
updatedFields,
);
return await updateDocumentByFields(
collectionName,
updatedFields,
{ upsert: true },
[field, value],
);
};
// Function to update a document by a specific field
export const upsertDocumentByFields = async (
collectionName,
updatedFields,
...fieldValuePairs
) => {
log.DEBUG(
"Upsert document by fields:",
collectionName,
updatedFields,
fieldValuePairs,
);
return await updateDocumentByFields(
collectionName,
updatedFields,
{ upsert: true },
fieldValuePairs,
);
};
// Function to update a document by a specific field
export const updateDocumentByField = async (
collectionName,
field,
value,
updatedFields,
options = null,
) => {
log.DEBUG(
"Update document by field:",
collectionName,
field,
value,
updatedFields,
options,
);
return await updateDocumentByFields(collectionName, updatedFields, options, [
field,
value,
]);
};
// Function to update a document by multiple fields
export const updateDocumentByFields = async (
collectionName,
updatedFields,
options,
...fieldValuePairs
) => {
log.DEBUG(
"Update document by fields:",
collectionName,
updatedFields,
options,
fieldValuePairs,
);
const db = await connectToDatabase(); const db = await connectToDatabase();
try { try {
const collection = db.db().collection(collectionName); const collection = db.db().collection(collectionName);
const result = await collection.updateOne({ [field]: value }, { $set: updatedFields });
// Convert the fieldValuePairs array into an object log.DEBUG('Document updated:', result.modifiedCount);
const query = fieldValuePairs.reduce((acc, [field, value]) => {
acc[field] = value;
return acc;
}, {});
const result = await collection.updateOne(
query,
{ $set: updatedFields },
options,
);
log.DEBUG("Document updated:", result.modifiedCount);
return result.modifiedCount; return result.modifiedCount;
} catch (error) { } catch (error) {
console.error("Error updating document:", error); console.error('Error updating document:', error);
throw error; throw error;
} finally { } finally {
await db.close(); await db.close();
@@ -194,32 +92,16 @@ export const updateDocumentByFields = async (
// Function to delete a document by a specific field // Function to delete a document by a specific field
export const deleteDocumentByField = async (collectionName, field, value) => { export const deleteDocumentByField = async (collectionName, field, value) => {
log.DEBUG("Delete document by field:", collectionName, field, value); log.DEBUG("Delete document by field:", collectionName, field, value);
return await deleteDocumentByFields(collectionName, [field, value]);
};
// Function to delete a document by multiple fields
export const deleteDocumentByFields = async (
collectionName,
...fieldValuePairs
) => {
log.DEBUG("Delete document by fields:", collectionName, fieldValuePairs);
const db = await connectToDatabase(); const db = await connectToDatabase();
try { try {
const collection = db.db().collection(collectionName); const collection = db.db().collection(collectionName);
const result = await collection.deleteOne({ [field]: value });
// Convert the fieldValuePairs array into an object log.DEBUG('Document deleted:', result.deletedCount);
const query = fieldValuePairs.reduce((acc, [field, value]) => {
acc[field] = value;
return acc;
}, {});
const result = await collection.deleteOne(query);
log.DEBUG("Document deleted:", result.deletedCount);
return result.deletedCount; return result.deletedCount;
} catch (error) { } catch (error) {
console.error("Error deleting document:", error); console.error('Error deleting document:', error);
throw error; throw error;
} finally { } finally {
await db.close(); await db.close();
} }
}; };

View File

@@ -1,12 +1,8 @@
import { DebugBuilder } from "../../modules/debugger.mjs"; import { DebugBuilder } from "../../modules/debugger.mjs";
const log = new DebugBuilder("server", "mongoNodesWrappers"); const log = new DebugBuilder("server", "mongoNodesWrappers");
import { import { insertDocument, getDocuments, connectToDatabase } from "./mongoHandler.mjs";
insertDocument,
getDocuments,
connectToDatabase,
} from "./mongoHandler.mjs";
const collectionName = "nodes"; const collectionName = 'nodes';
// Wrapper for inserting a node // Wrapper for inserting a node
export const createNode = async (node) => { export const createNode = async (node) => {
@@ -14,7 +10,7 @@ export const createNode = async (node) => {
const insertedId = await insertDocument(collectionName, node); const insertedId = await insertDocument(collectionName, node);
return insertedId; return insertedId;
} catch (error) { } catch (error) {
log.ERROR("Error creating node:", error); log.ERROR('Error creating node:', error);
throw error; throw error;
} }
}; };
@@ -25,7 +21,7 @@ export const getAllNodes = async () => {
const nodes = await getDocuments(collectionName); const nodes = await getDocuments(collectionName);
return nodes; return nodes;
} catch (error) { } catch (error) {
log.ERROR("Error getting all nodes:", error); log.ERROR('Error getting all nodes:', error);
throw error; throw error;
} }
}; };
@@ -33,12 +29,12 @@ export const getAllNodes = async () => {
// Wrapper for retrieving a node by NUID // Wrapper for retrieving a node by NUID
export const getNodeByNuid = async (nuid) => { export const getNodeByNuid = async (nuid) => {
const db = await connectToDatabase(); const db = await connectToDatabase();
try { try {
const collection = db.db().collection(collectionName); const collection = db.db().collection(collectionName);
const node = await collection.findOne({ nuid }); const node = await collection.findOne({ nuid });
return node; return node;
} catch (error) { } catch (error) {
log.ERROR("Error getting node by NUID:", error); log.ERROR('Error getting node by NUID:', error);
throw error; throw error;
} finally { } finally {
// Close the connection // Close the connection
@@ -51,14 +47,11 @@ export const updateNodeByNuid = async (nuid, updatedFields) => {
const db = await connectToDatabase(); const db = await connectToDatabase();
try { try {
const collection = db.db().collection(collectionName); const collection = db.db().collection(collectionName);
const result = await collection.updateOne( const result = await collection.updateOne({ nuid }, { $set: updatedFields });
{ nuid }, log.INFO('Node updated:', result.modifiedCount);
{ $set: updatedFields },
);
log.INFO("Node updated:", result.modifiedCount);
return result.modifiedCount; return result.modifiedCount;
} catch (error) { } catch (error) {
log.ERROR("Error updating node by NUID:", error); log.ERROR('Error updating node by NUID:', error);
throw error; throw error;
} finally { } finally {
// Close the connection // Close the connection
@@ -69,16 +62,16 @@ export const updateNodeByNuid = async (nuid, updatedFields) => {
// Wrapper for deleting a node by NUID // Wrapper for deleting a node by NUID
export const deleteNodeByNuid = async (nuid) => { export const deleteNodeByNuid = async (nuid) => {
const db = await connectToDatabase(); const db = await connectToDatabase();
try { try {
const collection = db.db().collection(collectionName); const collection = db.db().collection(collectionName);
const result = await collection.deleteOne({ nuid }); const result = await collection.deleteOne({ nuid });
log.INFO("Node deleted:", result.deletedCount); log.INFO('Node deleted:', result.deletedCount);
return result.deletedCount; return result.deletedCount;
} catch (error) { } catch (error) {
log.ERROR("Error deleting node by NUID:", error); log.ERROR('Error deleting node by NUID:', error);
throw error; throw error;
} finally { } finally {
// Close the connection // Close the connection
await db.close(); await db.close();
} }
}; };

View File

@@ -1,119 +1,113 @@
import { DebugBuilder } from "../../modules/debugger.mjs"; import { DebugBuilder } from "../../modules/debugger.mjs";
const log = new DebugBuilder("server", "mongoSystemsWrappers"); const log = new DebugBuilder("server", "mongoSystemsWrappers");
import { import { insertDocument, getDocuments, connectToDatabase } from "./mongoHandler.mjs";
insertDocument,
getDocuments,
connectToDatabase,
} from "./mongoHandler.mjs";
const collectionName = "radio-systems"; const collectionName = 'radio-systems';
// Local wrapper to remove any local files from radio systems // Local wrapper to remove any local files from radio systems
const removeLocalFilesFromsystem = async (system) => { const removeLocalFilesFromsystem = async (system) => {
if (system.trunkFile) delete system.trunkFile; if (system.trunkFile) delete system.trunkFile;
if (system.whitelistFile) delete system.whitelistFile; if (system.whitelistFile) delete system.whitelistFile;
}; }
// Wrapper for inserting a system // Wrapper for inserting a system
export const createSystem = async (name, system, nuid) => { export const createSystem = async (name, system, nuid) => {
try { try {
// Remove any local files // Remove any local files
await removeLocalFilesFromsystem(system); await removeLocalFilesFromsystem(system);
// Add the NUID of the node that created this system // Add the NUID of the node that created this system
system.nodes = [nuid]; system.nodes = [nuid];
// Add the name of the system // Add the name of the system
system.name = name; system.name = name
const insertedId = await insertDocument(collectionName, system); const insertedId = await insertDocument(collectionName, system);
return insertedId; return insertedId;
} catch (error) { } catch (error) {
log.ERROR("Error creating system:", error); log.ERROR('Error creating system:', error);
throw error; throw error;
} }
}; };
// Wrapper for retrieving all systems // Wrapper for retrieving all systems
export const getAllSystems = async () => { export const getAllSystems = async () => {
try { try {
const systems = await getDocuments(collectionName); const systems = await getDocuments(collectionName);
return systems; return systems;
} catch (error) { } catch (error) {
log.ERROR("Error getting all systems:", error); log.ERROR('Error getting all systems:', error);
throw error; throw error;
} }
}; };
// Wrapper for retrieving a system by name // Wrapper for retrieving a system by name
export const getSystemByName = async (name) => { export const getSystemByName = async (name) => {
const db = await connectToDatabase(); const db = await connectToDatabase();
try { try {
const collection = db.db().collection(collectionName); const collection = db.db().collection(collectionName);
const system = await collection.findOne({ name }); const system = await collection.findOne({ name });
return system; return system;
} catch (error) { } catch (error) {
log.ERROR("Error getting system by name:", error); log.ERROR('Error getting system by name:', error);
throw error; throw error;
} finally { } finally {
// Close the connection // Close the connection
await db.close(); await db.close();
} }
}; };
// Wrapper to get all systems from a given node // Wrapper to get all systems from a given node
export const getSystemsByNuid = async (nuid) => { export const getSystemsByNuid = async (nuid) => {
const db = await connectToDatabase(); const db = await connectToDatabase();
try { try {
const collection = db.db().collection(collectionName); const collection = db.db().collection(collectionName);
// Query for documents where the 'nodes' array contains the given nodeID // Query for documents where the 'nodes' array contains the given nodeID
const query = { nodes: nuid }; const query = { nodes: nuid };
const systems = await collection.find(query).toArray(); const systems = await collection.find(query).toArray();
return systems; return systems;
} catch (error) { } catch (error) {
log.ERROR("Error finding entries:", error); log.ERROR('Error finding entries:', error);
throw error; throw error;
} finally { } finally {
// Close the connection // Close the connection
await db.close(); await db.close();
} }
}; };
// Wrapper for updating a system by name // Wrapper for updating a system by name
export const updateSystemByName = async (name, updatedSystem) => { export const updateSystemByName = async (name, updatedSystem) => {
// Remove any local files // Remove any local files
await removeLocalFilesFromsystem(updatedSystem); await removeLocalFilesFromsystem(updatedSystem);
const db = await connectToDatabase(); const db = await connectToDatabase();
try { try {
const collection = db.db().collection(collectionName); const collection = db.db().collection(collectionName);
const result = await collection.updateOne( const result = await collection.updateOne({ name }, { $set: updatedSystem });
{ name }, log.INFO('System updated:', result.modifiedCount);
{ $set: updatedSystem }, return result.modifiedCount;
); } catch (error) {
log.INFO("System updated:", result.modifiedCount); log.ERROR('Error updating system by name:', error);
return result.modifiedCount; throw error;
} catch (error) { } finally {
log.ERROR("Error updating system by name:", error); // Close the connection
throw error; await db.close();
} finally { }
// Close the connection
await db.close();
}
}; };
// Wrapper for deleting a system by name // Wrapper for deleting a system by name
export const deleteSystemByName = async (name) => { export const deleteSystemByName = async (name) => {
const db = await connectToDatabase(); const db = await connectToDatabase();
try { try {
const collection = db.db().collection(collectionName); const collection = db.db().collection(collectionName);
const result = await collection.deleteOne({ name }); const result = await collection.deleteOne({ name });
log.INFO("System deleted:", result.deletedCount); log.INFO('System deleted:', result.deletedCount);
return result.deletedCount; return result.deletedCount;
} catch (error) { } catch (error) {
log.ERROR("Error deleting system by name:", error); log.ERROR('Error deleting system by name:', error);
throw error; throw error;
} finally { } finally {
// Close the connection // Close the connection
await db.close(); await db.close();
} }
}; };

View File

@@ -1,47 +1,41 @@
import { DebugBuilder } from "../modules/debugger.mjs"; import { DebugBuilder } from "../modules/debugger.mjs";
const log = new DebugBuilder("server", "socketServer"); const log = new DebugBuilder("server", "socketServer");
import express from "express"; import express from 'express';
import { createServer } from "node:http"; import { createServer } from 'node:http';
import { Server } from "socket.io"; import { Server } from 'socket.io';
import morgan from "morgan"; import morgan from 'morgan';
import { import { nodeLoginWrapper, nodeUpdateWrapper, nodeDisconnectWrapper, nearbySystemsUpdateWraper } from "./socketServerWrappers.mjs";
nodeLoginWrapper,
nodeUpdateWrapper,
nodeDisconnectWrapper,
nearbySystemsUpdateWraper,
} from "./socketServerWrappers.mjs";
export const app = express(); export const app = express();
export const server = createServer(app); export const server = createServer(app);
export const nodeIo = new Server(server); export const nodeIo = new Server(server);
app.use(morgan("tiny")); app.use(morgan('tiny'));
app.get("/", (req, res) => { app.get('/', (req, res) => {
res.send("<h1>Hello world</h1>"); res.send('<h1>Hello world</h1>');
}); });
nodeIo.on("connection", (socket) => { nodeIo.on('connection', (socket) => {
log.INFO("a user connected", socket.id); log.INFO('a user connected', socket.id);
socket.on("node-login", async (data) => { socket.on('node-login', async (data) => {
await nodeLoginWrapper(data, socket); await nodeLoginWrapper(data, socket);
await socket.emit("node-login-successful"); await socket.emit('node-login-successful');
}); })
socket.on("node-update", async (data) => { socket.on('node-update', async (data) => {
let tempPromises = []; let tempPromises = [];
tempPromises.push(nodeUpdateWrapper(data.node)); tempPromises.push(nodeUpdateWrapper(data.node));
tempPromises.push( tempPromises.push(nearbySystemsUpdateWraper(data.node.nuid, data.nearbySystems));
nearbySystemsUpdateWraper(data.node.nuid, data.nearbySystems),
);
await Promise.all(tempPromises); await Promise.all(tempPromises);
await socket.emit("node-update-successful"); await socket.emit('node-update-successful')
}); })
socket.on("disconnect", () => { socket.on('disconnect', () => {
nodeDisconnectWrapper(socket.id); nodeDisconnectWrapper(socket.id);
}); });
});
});

View File

@@ -1,17 +1,7 @@
import { DebugBuilder } from "../modules/debugger.mjs"; import { DebugBuilder } from "../modules/debugger.mjs";
const log = new DebugBuilder("server", "socketServerWrappers"); const log = new DebugBuilder("server", "socketServerWrappers");
import { import { createNode, getNodeByNuid, updateNodeByNuid } from "./mongo-wrappers/mongoNodesWrappers.mjs"
createNode, import { createSystem, getSystemByName, updateSystemByName, getSystemsByNuid, deleteSystemByName } from "./mongo-wrappers/mongoSystemsWrappers.mjs"
getNodeByNuid,
updateNodeByNuid,
} from "./mongo-wrappers/mongoNodesWrappers.mjs";
import {
createSystem,
getSystemByName,
updateSystemByName,
getSystemsByNuid,
deleteSystemByName,
} from "./mongo-wrappers/mongoSystemsWrappers.mjs";
/** /**
* Description * Description
@@ -21,11 +11,11 @@ import {
* @returns {any} * @returns {any}
*/ */
const sendNodeCommand = async (socket, command, data) => { const sendNodeCommand = async (socket, command, data) => {
// TODO - Check to see if the command exists // TODO - Check to see if the command exists
// TODO - Check to see if the socket is alive? // TODO - Check to see if the socket is alive?
// TODO - Validate the given data // TODO - Validate the given data
socket.emit(command, data); socket.emit(command, data);
}; }
/** /**
* Log the node into the network * Log the node into the network
@@ -34,25 +24,25 @@ const sendNodeCommand = async (socket, command, data) => {
* @returns {any} * @returns {any}
*/ */
export const nodeLoginWrapper = async (data, socket) => { export const nodeLoginWrapper = async (data, socket) => {
log.INFO(`Login requested from node: ${data.nuid}`, data); log.INFO(`Login requested from node: ${data.nuid}`, data);
// Check to see if node exists // Check to see if node exists
var node = await getNodeByNuid(data.nuid); var node = await getNodeByNuid(data.nuid);
if (!node) { if (!node) {
const insertedId = await createNode(data); const insertedId = await createNode(data);
log.DEBUG("Added new node to the database:", insertedId); log.DEBUG("Added new node to the database:", insertedId);
} else { } else {
// Check for updates // Check for updates
const updatedNode = await updateNodeByNuid(data.nuid, data); const updatedNode = await updateNodeByNuid(data.nuid, data)
log.DEBUG("Updated node:", updatedNode); log.DEBUG("Updated node:", updatedNode);
} }
node = await getNodeByNuid(data.nuid); node = await getNodeByNuid(data.nuid);
// Add the socket/node connection // Add the socket/node connection
socket.node = node; socket.node = node;
return; return;
}; }
/** /**
* Disconnect the client from the server * Disconnect the client from the server
@@ -60,9 +50,9 @@ export const nodeLoginWrapper = async (data, socket) => {
* @returns {any} * @returns {any}
*/ */
export const nodeDisconnectWrapper = async (socketId) => { export const nodeDisconnectWrapper = async (socketId) => {
// TODO - Let any server know that a bot has disconnected if the bot was joined to vc? might not be worth cpu lol // TODO - Let any server know that a bot has disconnected if the bot was joined to vc? might not be worth cpu lol
return; return;
}; }
/** /**
* Update node data in the database * Update node data in the database
@@ -70,10 +60,10 @@ export const nodeDisconnectWrapper = async (socketId) => {
* @returns {any} * @returns {any}
*/ */
export const nodeUpdateWrapper = async (nodeData) => { export const nodeUpdateWrapper = async (nodeData) => {
log.DEBUG("Data update sent by node: ", nodeData); log.DEBUG("Data update sent by node: ", nodeData);
const updateResults = await updateNodeByNuid(nodeData.nuid, nodeData); const updateResults = await updateNodeByNuid(nodeData.nuid, nodeData);
return; return;
}; }
/** /**
* Wrapper to update the systems from the nearbySystems object passed from clients * Wrapper to update the systems from the nearbySystems object passed from clients
@@ -81,103 +71,89 @@ export const nodeUpdateWrapper = async (nodeData) => {
* @param {object} nearbySystems The nearby systems object passed from the node to be updated * @param {object} nearbySystems The nearby systems object passed from the node to be updated
*/ */
export const nearbySystemsUpdateWraper = async (nuid, nearbySystems) => { export const nearbySystemsUpdateWraper = async (nuid, nearbySystems) => {
log.DEBUG("System updates sent by node: ", nuid, nearbySystems); log.DEBUG("System updates sent by node: ", nuid, nearbySystems);
// Check to see if the node removed any systems // Check to see if the node removed any systems
const existingSystems = await getSystemsByNuid(nuid); const existingSystems = await getSystemsByNuid(nuid);
log.DEBUG("Existing systems:", existingSystems); log.DEBUG("Existing systems:", existingSystems);
if (existingSystems !== nearbySystems) { if (existingSystems !== nearbySystems) {
for (const existingSystem of existingSystems) { for (const existingSystem of existingSystems) {
if (existingSystem.name in nearbySystems) { if (existingSystem.name in nearbySystems) {
// Skip this system if it's in the given systems update // Skip this system if it's in the given systems update
continue; continue;
} }
log.DEBUG("System exists that was not given by node", existingSystem); log.DEBUG("System exists that was not given by node", existingSystem);
// Check if this node was the only node on this system // Check if this node was the only node on this system
if (existingSystem.nodes.filter((node) => node !== nuid).length === 0) { if (existingSystem.nodes.filter(node => node !== nuid).length === 0) {
// Remove the system if so // Remove the system if so
log.INFO( log.INFO("Given node was the only node on this system, removing the system...");
"Given node was the only node on this system, removing the system...", await deleteSystemByName(existingSystem.name);
); } else {
await deleteSystemByName(existingSystem.name); // Remove the node from the array if there are other nodes with this system
} else { log.INFO("Other nodes found on this system, removing the given NUID");
// Remove the node from the array if there are other nodes with this system existingSystem.nodes = existingSystem.nodes.filter(node => node !== nuid);
log.INFO("Other nodes found on this system, removing the given NUID"); log.DEBUG(existingSystem);
existingSystem.nodes = existingSystem.nodes.filter( await updateSystemByName(existingSystem.name, existingSystem);
(node) => node !== nuid, }
);
log.DEBUG(existingSystem);
await updateSystemByName(existingSystem.name, existingSystem);
}
}
}
// Add and update the given systems
for (const nearbySystem in nearbySystems) {
// Check if the system exists already on another node
const existingSystem = await getSystemByName(nearbySystem);
if (existingSystem) {
// Verify the frequencies match (to make sure the name isn't just the same)
if (
JSON.stringify(existingSystem.frequencies) ===
JSON.stringify(nearbySystems[nearbySystem].frequencies)
) {
// The systems are the same
// Check if the current node is listed in the nodes, if not add it
if (!existingSystem.nodes.includes(nuid)) {
existingSystem.nodes.push(nuid);
// Update the system with the added node
const updateResults = await updateSystemByName(
nearbySystem,
existingSystem,
);
if (updateResults) log.INFO("System updated", nearbySystem);
} }
} else {
// The systems are not the same
// TODO - Implement logic to handle if system names match, but they are for different frequencies or have additional freqs
// Check if the current node is listed in the nodes, if not add it
if (!existingSystem.nodes.includes(nuid)) {
existingSystem.nodes.push(nuid);
nearbySystems[nearbySystem].nodes = existingSystem.nodes;
}
// Update the system with the added node
const updateResults = await updateSystemByName(
nearbySystem,
nearbySystems[nearbySystem],
);
if (updateResults) log.INFO("System updated", nearbySystem);
}
} else {
// Create a new system
const newSystem = await createSystem(
nearbySystem,
nearbySystems[nearbySystem],
nuid,
);
log.INFO("New system created", nearbySystem, newSystem);
} }
}
return; // Add and update the given systems
}; for (const nearbySystem in nearbySystems) {
// Check if the system exists already on another node
const existingSystem = await getSystemByName(nearbySystem);
if (existingSystem) {
// Verify the frequencies match (to make sure the name isn't just the same)
if (JSON.stringify(existingSystem.frequencies) === JSON.stringify(nearbySystems[nearbySystem].frequencies)) {
// The systems are the same
// Check if the current node is listed in the nodes, if not add it
if (!existingSystem.nodes.includes(nuid)) {
existingSystem.nodes.push(nuid);
// Update the system with the added node
const updateResults = await updateSystemByName(nearbySystem, existingSystem);
if (updateResults) log.INFO("System updated", nearbySystem);
}
} else {
// The systems are not the same
// TODO - Implement logic to handle if system names match, but they are for different frequencies or have additional freqs
// Check if the current node is listed in the nodes, if not add it
if (!existingSystem.nodes.includes(nuid)) {
existingSystem.nodes.push(nuid);
nearbySystems[nearbySystem].nodes = existingSystem.nodes;
}
// Update the system with the added node
const updateResults = await updateSystemByName(nearbySystem, nearbySystems[nearbySystem]);
if (updateResults) log.INFO("System updated", nearbySystem);
}
}
else {
// Create a new system
const newSystem = await createSystem(nearbySystem, nearbySystems[nearbySystem], nuid);
log.INFO("New system created", nearbySystem, newSystem);
}
}
return;
}
/** /**
* Get the open socket connection ID for a node from the NUID * Get the open socket connection ID for a node from the NUID
* @param {string} nuid The NUID to find within the open sockets * @param {string} nuid The NUID to find within the open sockets
* @returns {string|null} Will return the open socket ID or NULL * @returns {string|null} Will return the open socket ID or NULL
*/ */
export const getSocketIdByNuid = async (nodeIo, nuid) => { export const getSocketIdByNuid = async (nodeIo, nuid) => {
const openSockets = await nodeIo.allSockets(); const openSockets = await nodeIo.allSockets();
for (const openSocketId of openSockets) { for (const openSocketId of openSockets) {
log.DEBUG(openSockets); log.DEBUG(openSockets)
const openSocket = await nodeIo.sockets.sockets.get(openSocketId); const openSocket = await nodeIo.sockets.sockets.get(openSocketId);
if (openSocket.node.nuid == nuid) return openSocket; if (openSocket.node.nuid == nuid)
} return openSocket;
return null; }
}; return null;
}
/** /**
* Get all nodes that are connected to a voice channel * Get all nodes that are connected to a voice channel
@@ -186,34 +162,29 @@ export const getSocketIdByNuid = async (nodeIo, nuid) => {
* @returns {Array} The sockets connected to VC in a given server * @returns {Array} The sockets connected to VC in a given server
*/ */
export const getAllSocketsConnectedToVC = async (nodeIo, guildId) => { export const getAllSocketsConnectedToVC = async (nodeIo, guildId) => {
// Get all open socket nodes // Get all open socket nodes
// TODO - require a server guild to filter the results, ie this would be able to check what server the VCs the nodes are connected are in // TODO - require a server guild to filter the results, ie this would be able to check what server the VCs the nodes are connected are in
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
// Check each open socket to see if the node has the requested system // Check each open socket to see if the node has the requested system
const socketsConnectedToVC = []; const socketsConnectedToVC = []
await Promise.all( await Promise.all(openSockets.map(async openSocket => {
openSockets.map(async (openSocket) => { openSocket = await nodeIo.sockets.sockets.get(openSocket);
openSocket = await nodeIo.sockets.sockets.get(openSocket); await new Promise((res) => {
await new Promise((res) => { openSocket.emit('node-check-connected-status', guildId, (status) => {
openSocket.emit("node-check-connected-status", guildId, (status) => { if (status) {
if (status) { log.INFO("Socket is connected to VC:", openSocket.node.name, status);
log.INFO( socketsConnectedToVC.push(openSocket);
"Socket is connected to VC:", } else {
openSocket.node.name, log.INFO("Socket is NOT connected to VC:", openSocket.node.name);
status, }
); res();
socketsConnectedToVC.push(openSocket); })
} else {
log.INFO("Socket is NOT connected to VC:", openSocket.node.name);
}
res();
}); });
}); }));
}),
); return socketsConnectedToVC;
}
return socketsConnectedToVC;
};
/** /**
* Check if the given node has an open discord client * Check if the given node has an open discord client
@@ -221,65 +192,43 @@ export const getAllSocketsConnectedToVC = async (nodeIo, guildId) => {
* @returns {boolean} If the given node has an open discord client or not * @returns {boolean} If the given node has an open discord client or not
*/ */
export const checkIfNodeHasOpenDiscordClient = async (openSocket) => { export const checkIfNodeHasOpenDiscordClient = async (openSocket) => {
// Check the open socket to see if the node has an open discord client // Check the open socket to see if the node has an open discord client
let hasOpenDiscordClient = false; let hasOpenDiscordClient = false;
await new Promise((res) => { await new Promise((res) => {
log.INFO( openSocket.emit('node-check-discord-open-client', (status) => {
"Checking if socket has an open connection:", if (status) {
openSocket.node.name, log.INFO("Socket has an open discord client:", openSocket.node.name, status);
); hasOpenDiscordClient = true;
openSocket.emit("node-check-discord-open-client", (status) => { } else {
if (status) { log.INFO("Socket does NOT have an open discord client:", openSocket.node.name);
log.INFO( }
"Socket has an open discord client:", res();
openSocket.node.name, })
status,
);
hasOpenDiscordClient = true;
} else {
log.INFO(
"Socket does NOT have an open discord client:",
openSocket.node.name,
);
}
res();
}); });
});
return hasOpenDiscordClient; return hasOpenDiscordClient;
}; }
export const getNodeCurrentListeningSystem = async (openSocket) => { export const getNodeCurrentListeningSystem = async (openSocket) => {
const hasOpenClient = checkIfNodeHasOpenDiscordClient(openSocket); const hasOpenClient = checkIfNodeHasOpenDiscordClient(openSocket);
if (!hasOpenClient) return undefined; if (!hasOpenClient) return undefined;
// check what system the socket is listening to // check what system the socket is listening to
let currentSystem = undefined; let currentSystem = undefined;
await new Promise((res) => { await new Promise((res) => {
log.INFO( openSocket.emit('node-check-current-system', (system) => {
"Checking system node is currently listening to:", if (system) {
openSocket.node.name, log.INFO("Socket is listening to system:", openSocket.node.name, system);
); currentSystem = system;
openSocket.emit("node-check-current-system", (system) => { } else {
if (system) { log.INFO("Socket is not currently listening to a system:", openSocket.node.name);
log.INFO( }
"Socket is listening to system:", res();
openSocket.node.name, })
system,
);
currentSystem = system;
} else {
log.INFO(
"Socket is not currently listening to a system:",
openSocket.node.name,
);
}
res();
}); });
});
return currentSystem; return currentSystem;
}; }
/** /**
* Wrapper to check if the given NUID is connected to a VC * Wrapper to check if the given NUID is connected to a VC
@@ -288,17 +237,14 @@ export const getNodeCurrentListeningSystem = async (openSocket) => {
* @returns {boolean} If the node is connected to VC in the given server * @returns {boolean} If the node is connected to VC in the given server
*/ */
export const checkIfNodeIsConnectedToVC = async (nodeIo, guildId, nuid) => { export const checkIfNodeIsConnectedToVC = async (nodeIo, guildId, nuid) => {
const socketsConnectedToVC = await getAllSocketsConnectedToVC( const socketsConnectedToVC = await getAllSocketsConnectedToVC(nodeIo, guildId);
nodeIo, for (const socket of socketsConnectedToVC) {
guildId, if (socket.node.nuid === nuid) {
); return true;
for (const socket of socketsConnectedToVC) { }
if (socket.node.nuid === nuid) {
return true;
} }
} return false;
return false; }
};
/** /**
* Get the discord username from a given socket * Get the discord username from a given socket
@@ -307,12 +253,12 @@ export const checkIfNodeIsConnectedToVC = async (nodeIo, guildId, nuid) => {
* @returns {string} The username of the bot in the requested server * @returns {string} The username of the bot in the requested server
*/ */
export const getNodeDiscordUsername = async (socket, guildId) => { export const getNodeDiscordUsername = async (socket, guildId) => {
return await new Promise((res) => { return await new Promise((res) => {
socket.emit("node-get-discord-username", guildId, (username) => { socket.emit('node-get-discord-username', guildId, (username) => {
res(username); res(username);
});
}); });
}); }
};
/** /**
* Get the discord ID from a given socket * Get the discord ID from a given socket
@@ -320,12 +266,12 @@ export const getNodeDiscordUsername = async (socket, guildId) => {
* @returns {string} The ID of the bot * @returns {string} The ID of the bot
*/ */
export const getNodeDiscordID = async (socket) => { export const getNodeDiscordID = async (socket) => {
return await new Promise((res) => { return await new Promise((res) => {
socket.emit("node-get-discord-id", (discordID) => { socket.emit('node-get-discord-id', (discordID) => {
res(discordID); res(discordID);
});
}); });
}); }
};
/** /**
* Request a given socket node to join a given voice channel * Request a given socket node to join a given voice channel
@@ -333,21 +279,16 @@ export const getNodeDiscordID = async (socket) => {
* @param {any} systemName The system preset name that we would like to listen to * @param {any} systemName The system preset name that we would like to listen to
* @param {string} discordChanelId The Discord channel ID to join the listening bot to * @param {string} discordChanelId The Discord channel ID to join the listening bot to
*/ */
export const requestNodeJoinSystem = async ( export const requestNodeJoinSystem = async (socket, systemName, discordChanelId, discordToken = "MTE5NjAwNTM2ODYzNjExMjk3Nw.GuCMXg.24iNNofNNumq46FIj68zMe9RmQgugAgfrvelEA") => {
socket, // Join the system
systemName, const joinData = {
discordChanelId, 'clientID': discordToken,
discordToken = "MTE5NjAwNTM2ODYzNjExMjk3Nw.GuCMXg.24iNNofNNumq46FIj68zMe9RmQgugAgfrvelEA", 'channelID': discordChanelId,
) => { 'system': systemName
// Join the system }
const joinData = { // Send the command to the node
clientID: discordToken, await sendNodeCommand(socket, "node-join", joinData);
channelID: discordChanelId, }
system: systemName,
};
// Send the command to the node
await sendNodeCommand(socket, "node-join", joinData);
};
/** /**
* Request a given socket node to leave VC in a given server * Request a given socket node to leave VC in a given server
@@ -355,20 +296,21 @@ export const requestNodeJoinSystem = async (
* @param {string} guildId The guild ID to disconnect the socket node from * @param {string} guildId The guild ID to disconnect the socket node from
*/ */
export const requestBotLeaveServer = async (socket, guildId) => { export const requestBotLeaveServer = async (socket, guildId) => {
// Send the command to the node // Send the command to the node
await sendNodeCommand(socket, "node-leave", guildId); await sendNodeCommand(socket, "node-leave", guildId);
}; }
/** /**
* Requset a given socket node to update themselves * Requset a given socket node to update themselves
* @param {any} socket The socket object of the node to request to update * @param {any} socket The socket object of the node to request to update
*/ */
export const requestNodeUpdate = async (socket) => { export const requestNodeUpdate = async (socket) => {
await sendNodeCommand(socket, "node-update", (status) => { await sendNodeCommand(socket, 'node-update', (status) => {
if (status) { if (status) {
log.INFO("Node is out of date, updating now", socket.node.name); log.INFO("Node is out of date, updating now", socket.node.name);
} else { } else {
log.INFO("Node is up to date", socket.node.name); log.INFO("Node is up to date", socket.node.name);
} }
}); });
}; }

2066
package-lock.json generated

File diff suppressed because it is too large Load Diff

View File

@@ -4,35 +4,26 @@
"description": "", "description": "",
"main": "server.js", "main": "server.js",
"scripts": { "scripts": {
"lint": "eslint .", "test": "jasmine",
"lint:fix": "eslint --fix .",
"test": "mocha --timeout 5000",
"start": "node server.js" "start": "node server.js"
}, },
"author": "Logan Cusano", "author": "Logan Cusano",
"license": "ISC", "license": "ISC",
"type": "module", "type": "module",
"devDependencies": { "devDependencies": {
"chai": "^5.1.1", "chai": "^5.1.0",
"eslint": "^9.9.0",
"eslint-config-prettier": "^9.1.0",
"eslint-plugin-prettier": "^5.2.1",
"eslint-plugin-unused-imports": "^4.1.3",
"jsdoc": "^4.0.3",
"mocha": "^10.4.0", "mocha": "^10.4.0",
"prettier": "^3.3.3",
"socket.io-client": "^4.7.5" "socket.io-client": "^4.7.5"
}, },
"dependencies": { "dependencies": {
"discord.js": "^14.15.2", "discord.js": "^14.14.1",
"dotenv": "^16.4.5", "dotenv": "^16.3.1",
"express": "^4.19.2", "express": "^4.18.2",
"mongodb": "^6.7.0", "mongodb": "^6.3.0",
"morgan": "^1.10.0", "morgan": "^1.10.0",
"node-html-parser": "^6.1.13", "node-html-parser": "^6.1.13",
"openai": "^4.47.3",
"rss-parser": "^3.13.0", "rss-parser": "^3.13.0",
"socket.io": "^4.7.5", "socket.io": "^4.7.2",
"user-agents": "^1.1.222" "user-agents": "^1.1.208"
} }
} }

View File

@@ -1,37 +1,28 @@
import { import { getAllFeeds, deleteFeedByLink, createPost, getPostByPostId } from '../modules/mongo-wrappers/mongoFeedsWrappers.mjs';
getAllFeeds, import crypto from 'crypto';
deleteFeedByLink, import { sendPost } from '../discordBot/modules/rssWrappers.mjs';
createPost,
getPostByPostId,
} from "../modules/mongo-wrappers/mongoFeedsWrappers.mjs";
import crypto from "crypto";
import { sendPost } from "../discordBot/modules/rssWrappers.mjs";
import { DebugBuilder } from "../modules/debugger.mjs"; import { DebugBuilder } from "../modules/debugger.mjs";
import { removeSource } from "./sourceManager.mjs"; import { removeSource } from './sourceManager.mjs'
import UserAgent from "user-agents"; import UserAgent from "user-agents";
import Parser from "rss-parser"; import Parser from 'rss-parser';
import PresenceManager from "../discordBot/modules/presenceManager.mjs";
import dotenv from "dotenv"; import dotenv from 'dotenv';
dotenv.config(); dotenv.config()
// Initialize the User-Agent string // Initialize the User-Agent string
process.env.USER_AGENT_STRING = new UserAgent({ platform: "Win32" }).toString(); process.env.USER_AGENT_STRING = new UserAgent({ platform: 'Win32' }).toString();
const parser = new Parser({ const parser = new Parser({
headers: { headers: {
"User-Agent": process.env.USER_AGENT_STRING, 'User-Agent': process.env.USER_AGENT_STRING,
Accept: "application/rss+xml,application/xhtml+xml,application/xml", "Accept": "application/rss+xml,application/xhtml+xml,application/xml"
}, }
}); });
const log = new DebugBuilder("server", "feedHandler"); const log = new DebugBuilder("server", "feedHandler");
export const returnHash = (...stringsIncluded) => { export const returnHash = (...stringsIncluded) => {
return crypto return crypto.createHash('sha1').update(stringsIncluded.join("-<<??//\\\\??>>-")).digest("base64");
.createHash("sha1")
.update(stringsIncluded.join("-<<??//\\\\??>>-"))
.digest("base64");
}; };
/** /**
@@ -42,63 +33,47 @@ export const returnHash = (...stringsIncluded) => {
export const updateFeeds = async (client) => { export const updateFeeds = async (client) => {
if (!client) throw new Error("Client object not passed"); if (!client) throw new Error("Client object not passed");
// Setup presence manager
const feedPm = new PresenceManager(client);
await feedPm.setPresence("online", "WATCHING", "for RSS feed updates");
try { try {
const records = await getAllFeeds(); const records = await getAllFeeds();
const sourcePromiseArray = records.map(async (source) => { const sourcePromiseArray = records.map(async (source) => {
log.DEBUG("Processing source:", source.title); log.DEBUG('Processing source:', source.title);
try { try {
const parsedFeed = await parser.parseURL(source.link); const parsedFeed = await parser.parseURL(source.link);
if (parsedFeed?.items) { if (parsedFeed?.items) {
await Promise.all( await Promise.all(parsedFeed.items.reverse().map(async (post) => {
parsedFeed.items.reverse().map(async (post) => { log.DEBUG("Processing post:", post.title);
log.DEBUG("Processing post:", post.title);
if (!post.title || !post.link) if (!post.title || !post.link) throw new Error("Missing title or link in the post");
throw new Error("Missing title or link in the post"); if (!post.content && !post['content:encoded']) log.WARN("No content for post:", post.title);
if (!post.content && !post["content:encoded"])
log.WARN("No content for post:", post.title);
post.postId = post.postId = post.postId ?? post.guid ?? post.id ?? returnHash(post.title, post.link, post.pubDate ?? Date.now());
post.postId ??
post.guid ??
post.id ??
returnHash(post.title, post.link, post.pubDate ?? Date.now());
const existingRecord = await getPostByPostId(post.postId); const existingRecord = await getPostByPostId(post.postId);
if (!existingRecord) { if (!existingRecord) {
const channel = client.channels.cache.get(source.channel_id); const channel = client.channels.cache.get(source.channel_id);
const sendResults = await sendPost(post, source, channel); const sendResults = await sendPost(post, source, channel);
if (!sendResults) throw new Error("Failed to send post"); if (!sendResults) throw new Error("Failed to send post");
log.DEBUG( log.DEBUG("Saving post to database:", post.title, source.channel_id);
"Saving post to database:",
post.title,
source.channel_id,
);
const postToSave = { const postToSave = {
title: post.title, title: post.title,
link: post.link, link: post.link,
pubDate: post.pubDate, pubDate: post.pubDate,
author: post.author, author: post.author,
contentSnippet: post.contentSnippet, contentSnippet: post.contentSnippet,
id: post.id, id: post.id,
isoDate: post.isoDate, isoDate: post.isoDate,
postId: post.postId, postId: post.postId
}; };
await createPost(postToSave); await createPost(postToSave);
log.DEBUG("Post saved:", postToSave); log.DEBUG("Post saved:", postToSave);
} }
}), }));
);
} else { } else {
await deleteFeedByLink(source.link); await deleteFeedByLink(source.link);
} }
@@ -111,9 +86,8 @@ export const updateFeeds = async (client) => {
await Promise.all(sourcePromiseArray); await Promise.all(sourcePromiseArray);
log.DEBUG("All sources processed"); log.DEBUG("All sources processed");
await feedPm.resetToDefault();
} catch (error) { } catch (error) {
log.ERROR("Error updating feeds:", error); log.ERROR("Error updating feeds:", error);
throw error; throw error;
} }
}; };

View File

@@ -2,7 +2,7 @@
import { DebugBuilder } from "../modules/debugger.mjs"; import { DebugBuilder } from "../modules/debugger.mjs";
import { updateFeeds } from "./feedHandler.mjs"; import { updateFeeds } from "./feedHandler.mjs";
import dotenv from "dotenv"; import dotenv from 'dotenv';
dotenv.config(); dotenv.config();
const log = new DebugBuilder("server", "rssController"); const log = new DebugBuilder("server", "rssController");
@@ -10,39 +10,40 @@ const log = new DebugBuilder("server", "rssController");
const refreshInterval = parseInt(process.env.RSS_REFRESH_INTERVAL) || 300000; const refreshInterval = parseInt(process.env.RSS_REFRESH_INTERVAL) || 300000;
export class RSSController { export class RSSController {
constructor(client) { constructor(client) {
this.client = client; this.client = client;
this.intervalId = null; this.intervalId = null;
}
async start() {
try {
log.INFO("Starting RSS Controller");
// Get initial feeds before starting the interval loop
await this.collectLatestPosts();
// Start the interval loop for updating feeds
this.intervalId = setInterval(async () => {
await this.collectLatestPosts();
}, refreshInterval);
} catch (error) {
log.ERROR(`Failed to start RSS Controller: ${error.message}`);
} }
}
async stop() { async start() {
if (this.intervalId) { try {
clearInterval(this.intervalId); log.INFO("Starting RSS Controller");
log.INFO("RSS Controller stopped"); // Get initial feeds before starting the interval loop
} await this.collectLatestPosts();
}
async collectLatestPosts() { // Start the interval loop for updating feeds
try { this.intervalId = setInterval(async () => {
log.INFO("Updating sources"); await this.collectLatestPosts();
await updateFeeds(this.client); }, refreshInterval);
} catch (error) {
log.ERROR(`Error updating feeds: ${error.message}`); } catch (error) {
log.ERROR(`Failed to start RSS Controller: ${error.message}`);
}
}
async stop() {
if (this.intervalId) {
clearInterval(this.intervalId);
log.INFO("RSS Controller stopped");
}
}
async collectLatestPosts() {
try {
log.INFO("Updating sources");
await updateFeeds(this.client);
} catch (error) {
log.ERROR(`Error updating feeds: ${error.message}`);
}
} }
}
} }

View File

@@ -1,92 +1,75 @@
import { DebugBuilder } from "../modules/debugger.mjs"; import { createFeed, getFeedByLink, deleteFeedByLink } from '../modules/mongo-wrappers/mongoFeedsWrappers.mjs';
const log = new DebugBuilder("server", "sourceManager");
import {
createFeed,
getFeedByLink,
deleteFeedByLink,
} from "../modules/mongo-wrappers/mongoFeedsWrappers.mjs";
class SourceManager { class SourceManager {
constructor(sourceFailureLimit) { constructor(sourceFailureLimit) {
this.sourceFailureLimit = sourceFailureLimit; this.sourceFailureLimit = sourceFailureLimit;
this.runningSourcesToRemove = {}; this.runningSourcesToRemove = {};
} }
async removeSource(sourceURL) {
log.INFO(`Removing source: ${sourceURL}`);
async removeSource(sourceURL) { const currentTime = Date.now();
log.INFO(`Removing source: ${sourceURL}`); const sourceData = this.runningSourcesToRemove[sourceURL];
const currentTime = Date.now(); if (!sourceData) {
const sourceData = this.runningSourcesToRemove[sourceURL]; this.runningSourcesToRemove[sourceURL] = { count: 1, timestamp: currentTime, ignoredAttempts: 0 };
return;
}
if (!sourceData) { const elapsedTimeSinceLastAttempt = currentTime - sourceData.timestamp;
this.runningSourcesToRemove[sourceURL] = { const waitTime = sourceData.count * 30000;
count: 1,
timestamp: currentTime, if (elapsedTimeSinceLastAttempt <= waitTime) {
ignoredAttempts: 0, sourceData.ignoredAttempts += 1;
}; return;
return; }
if (sourceData.count < this.sourceFailureLimit) {
sourceData.count += 1;
sourceData.timestamp = currentTime;
return;
}
try {
const record = await getFeedByLink(sourceURL);
if (!record) {
log.ERROR(`Source not found in storage: ${sourceURL}`);
return;
}
const results = await deleteFeedByLink(sourceURL);
if (!results) {
log.WARN(`Failed to remove source: ${sourceURL}`);
return;
}
log.DEBUG(`Source removed after exceeding failure limit: ${sourceURL}`);
// Optionally, clean up the entry from runningSourcesToRemove
delete this.runningSourcesToRemove[sourceURL];
} catch (err) {
log.ERROR(`Error removing source from storage: ${sourceURL}`, err);
}
} }
const elapsedTimeSinceLastAttempt = currentTime - sourceData.timestamp; async addSource(title, link, category, guildId, channelId, callback) {
const waitTime = sourceData.count * 30000; try {
const feed = { title, link, category, guild_id: guildId, channel_id: channelId };
if (elapsedTimeSinceLastAttempt <= waitTime) { const record = await createFeed(feed);
sourceData.ignoredAttempts += 1; log.DEBUG("Source added:", record);
return; if (callback) callback(null, record);
} catch (err) {
log.ERROR("Error adding source:", err);
if (callback) callback(err, null);
}
} }
if (sourceData.count < this.sourceFailureLimit) {
sourceData.count += 1;
sourceData.timestamp = currentTime;
return;
}
try {
const record = await getFeedByLink(sourceURL);
if (!record) {
log.ERROR(`Source not found in storage: ${sourceURL}`);
return;
}
const results = await deleteFeedByLink(sourceURL);
if (!results) {
log.WARN(`Failed to remove source: ${sourceURL}`);
return;
}
log.DEBUG(`Source removed after exceeding failure limit: ${sourceURL}`);
// Optionally, clean up the entry from runningSourcesToRemove
delete this.runningSourcesToRemove[sourceURL];
} catch (err) {
log.ERROR(`Error removing source from storage: ${sourceURL}`, err);
}
}
async addSource(title, link, category, guildId, channelId, callback) {
try {
const feed = {
title,
link,
category,
guild_id: guildId,
channel_id: channelId,
};
const record = await createFeed(feed);
log.DEBUG("Source added:", record);
if (callback) callback(null, record);
} catch (err) {
log.ERROR("Error adding source:", err);
if (callback) callback(err, null);
}
}
} }
// Create a default instance of SourceManager // Create a default instance of SourceManager
const defaultSourceManager = new SourceManager(); const defaultSourceManager = new SourceManager();
// Export the class and default instance methods // Export the class and default instance methods
export { SourceManager }; export { SourceManager };
export const addSource = export const addSource = defaultSourceManager.addSource.bind(defaultSourceManager);
defaultSourceManager.addSource.bind(defaultSourceManager); export const removeSource = defaultSourceManager.removeSource.bind(defaultSourceManager);
export const removeSource =
defaultSourceManager.removeSource.bind(defaultSourceManager);

View File

@@ -1,15 +1,15 @@
import { DebugBuilder } from "./modules/debugger.mjs"; import { DebugBuilder } from "./modules/debugger.mjs";
const log = new DebugBuilder("server", "server"); const log = new DebugBuilder("server", "server");
import { nodeIo, server } from "./modules/socketServer.mjs"; import { nodeIo, app, server } from './modules/socketServer.mjs';
import { loadAddons } from "./modules/addonManager.mjs"; import { loadAddons } from './modules/addonManager.mjs';
import { serverClient } from "./discordBot/discordBot.mjs"; import { serverClient, addEnabledEventListeners } from './discordBot/discordBot.mjs';
import dotenv from "dotenv"; import dotenv from 'dotenv';
dotenv.config(); dotenv.config()
// Startup the node server // Startup the node server
server.listen(process.env.SERVER_PORT || 3000, () => { server.listen(process.env.SERVER_PORT || 3000, () => {
log.INFO(`server running at http://localhost:${process.env.SERVER_PORT}`); log.INFO(`server running at http://localhost:${process.env.SERVER_PORT}`);
}); });
// Add objects to the others // Add objects to the others
@@ -17,4 +17,4 @@ serverClient.nodeIo = nodeIo;
nodeIo.serverClient = serverClient; nodeIo.serverClient = serverClient;
// Load the addons // Load the addons
loadAddons(nodeIo); loadAddons(nodeIo);

13
spec/support/jasmine.json Normal file
View File

@@ -0,0 +1,13 @@
{
"spec_dir": "spec",
"spec_files": [
"**/*[sS]pec.?(m)js"
],
"helpers": [
"helpers/**/*.?(m)js"
],
"env": {
"stopSpecOnExpectationFailure": false,
"random": true
}
}

View File

@@ -0,0 +1,29 @@
import * as feedHandler from '../../rss-manager/feedHandler.mjs';
import * as mw from '../../modules/mongo-wrappers/mongoFeedsWrappers.mjs';
import * as drw from '../../discordBot/modules/rssWrappers.mjs';
describe('feedHandler', () => {
it('should call updateFeeds', async () => {
// Spy on the updateFeeds function
const feedsSpy = spyOn(mw, 'getAllFeeds').and.stub();
const sendPostSpy = spyOn(drw, 'sendPost').and.stub();
// Call the function that triggers updateFeeds
// For example:
// someFunctionThatCallsUpdateFeeds();
console.log(await spyOn(feedHandler, 'updateFeeds').and.callThrough({
channels: {
cache: {
get: () => ([{
// Stub methods or properties of the channel object as needed for testing
}])
}
}
}));
// Add your expectations here to ensure updateFeeds was called
expect(feedsSpy).toHaveBeenCalled();
expect(sendPostSpy).toHaveBeenCalled();
// Add more specific expectations if needed
});
});

View File

@@ -0,0 +1,494 @@
// Import necessary modules for testing
import ioClient from 'socket.io-client';
import { deleteNodeByNuid, getNodeByNuid } from '../../modules/mongo-wrappers/mongoNodesWrappers.mjs';
import { deleteSystemByName, getSystemByName } from '../../modules/mongo-wrappers/mongoSystemsWrappers.mjs';
import { nodeDisconnectWrapper, checkIfNodeHasOpenDiscordClient, getNodeCurrentListeningSystem, checkIfNodeIsConnectedToVC, getNodeDiscordUsername, getNodeDiscordID, requestBotLeaveServer, requestNodeJoinSystem, requestNodeUpdate } from '../../modules/socketServerWrappers.mjs';
import { nodeIo } from '../../modules/socketServer.mjs';
import dotenv from 'dotenv';
dotenv.config()
process.env.SERVER_PORT = 6000
// Define necessary variables for testing, such as mocked database connections or socket instances
const localNodeConfig = {
serverIp: 'localhost',
serverPort: process.env.SERVER_PORT,
node: {
nuid: "4f29a6340901a12affc87047c0ac16b01b92496c460c880a2459abe8c7928374",
name: "testyv7",
location: "china",
capabilities: ["radio"]
},
nearbySystems: {
"Testing P25 System Name": {
"frequencies": [
155344000,
155444000,
155555000,
155588550
],
"mode": "p25",
"trunkFile": "trunk.tsv",
"whitelistFile": "whitelist.tsv"
}
}
};
const updatedLocalNodeConfig = {
node: {
nuid: localNodeConfig.node.nuid,
name: "updatedName",
location: "updatedLocation",
capabilities: ["radio", "weather"] // Updated capabilities
},
nearbySystems: {
"Testing P25 System Name": {
"frequencies": [
155444000,
155555000,
155500000
],
"mode": "p25",
"trunkFile": "trunk2.tsv",
"whitelistFile": "whitelist2.tsv"
}
}
};
describe('Socket Server - Core Tests', () => {
// Start the Socket.IO server before running tests
let clientSocket; // The socket client
let serverClientSocket // The open client socket on the server
beforeAll(done => {
// Startup the node server
nodeIo.listen(process.env.SERVER_PORT || 3000, () => {
console.log(`server running at http://localhost:${process.env.SERVER_PORT}`);
});
// Connect a client socket to the server
clientSocket = ioClient.connect(`http://localhost:${process.env.SERVER_PORT}`);
nodeIo.on('connection', (socket) => {
serverClientSocket = socket;
done();
})
});
// Close the Socket.IO server after running tests
afterAll(async () => {
// Disconnect client socket
clientSocket.disconnect();
// Close the server
nodeIo.close();
// Remove the test data
deleteNodeByNuid(localNodeConfig.node.nuid); // Delete the user
deleteSystemByName(Object.keys(localNodeConfig.nearbySystems)[0])
});
// Test Node Login functionality
describe('Node Login', () => {
it('Should add a new node if it does not exist', async () => {
// Simulate a node login request
// Use the getNodeByNuid mock function to simulate checking if node exists
const existingNode = await getNodeByNuid(localNodeConfig.node.nuid);
// Assert that existingNode is null before node login
expect(existingNode).toEqual(null);
// Wait for the update
const node_login = new Promise(res => {
clientSocket.on('node-login-successful', async () => {
res();
});
});
// Emit the login command
clientSocket.emit("node-login", localNodeConfig.node);
// Wait for the successful login event
await node_login;
// Now we need to check if the node is added to the database
// We can use getNodeByNuid again to verify if the node was added correctly
const addedNode = await getNodeByNuid(localNodeConfig.node.nuid);
console.log("Added Node:", addedNode);
// Assert that the node is added correctly
expect(addedNode['_id']).toBeDefined(); // Check if _id property exists
expect(addedNode['nuid']).toEqual(localNodeConfig.node.nuid);
expect(addedNode['name']).toEqual(localNodeConfig.node.name);
expect(addedNode['location']).toEqual(localNodeConfig.node.location);
expect(addedNode['capabilities']).toEqual(localNodeConfig.node.capabilities);
})
it('Should update a node if it exists', async () => {
// Simulate a node login request
// Use the getNodeByNuid mock function to simulate checking if node exists
const existingNode = await getNodeByNuid(localNodeConfig.node.nuid);
// Assert that existingNode is matches the existing data before logging in
expect(existingNode['_id']).toBeDefined(); // Check if _id property exists
expect(existingNode['nuid']).toEqual(localNodeConfig.node.nuid);
expect(existingNode['name']).toEqual(localNodeConfig.node.name);
expect(existingNode['location']).toEqual(localNodeConfig.node.location);
expect(existingNode['capabilities']).toEqual(localNodeConfig.node.capabilities);
// Wait for the update
const node_login = new Promise(res => {
clientSocket.on('node-login-successful', async () => {
res();
});
});
// Emit the login command
clientSocket.emit("node-login", updatedLocalNodeConfig.node);
// Wait for the successful login event
await node_login;
// Now we need to check if the node is added to the database
// We can use getNodeByNuid again to verify if the node was added correctly
const updatedNode = await getNodeByNuid(localNodeConfig.node.nuid);
console.log("Updated Node:", updatedNode);
// Assert that the node is added correctly
expect(updatedNode['_id']).toBeDefined(); // Check if _id property exists
expect(updatedNode['nuid']).toEqual(updatedLocalNodeConfig.node.nuid);
expect(updatedNode['name']).toEqual(updatedLocalNodeConfig.node.name);
expect(updatedNode['location']).toEqual(updatedLocalNodeConfig.node.location);
expect(updatedNode['capabilities']).toEqual(updatedLocalNodeConfig.node.capabilities);
})
});
// Test Node Update functionality
describe('Node Update', () => {
it('Should add a node\'s nearby systems', async () => {
// Simulate an update request sent from the client to the server
// Get the existing node in the database
const existingNode = await getNodeByNuid(localNodeConfig.node.nuid);
// Assert that existingNode matches the updatedLocalNodeConfig
expect(existingNode['_id']).toBeDefined(); // Check if _id property exists
expect(existingNode['nuid']).toEqual(updatedLocalNodeConfig.node.nuid);
expect(existingNode['name']).toEqual(updatedLocalNodeConfig.node.name);
expect(existingNode['location']).toEqual(updatedLocalNodeConfig.node.location);
expect(existingNode['capabilities']).toEqual(updatedLocalNodeConfig.node.capabilities);
// Get the system from the DB
const existsingSystem = await getSystemByName("Testing P25 System Name");
// Assert that there is no existing system in the DB
expect(existsingSystem).toEqual(null);
// Wait for the update
const node_system_update = new Promise(res => {
clientSocket.on('node-update-successful', async () => {
res();
});
});
// Emit the update command
clientSocket.emit("node-update", updatedLocalNodeConfig);
// Wait for the successful update event
await node_system_update;
// Now we need to check if the system is added to the database
// We can use getNodeByNuid again to verify if the node was added correctly
const updatedNode = await getNodeByNuid(localNodeConfig.node.nuid);
console.log("Updated Node:", updatedNode);
// Assert that the node is added correctly
expect(updatedNode['_id']).toBeDefined(); // Check if _id property exists
expect(updatedNode['nuid']).toEqual(updatedLocalNodeConfig.node.nuid);
expect(updatedNode['name']).toEqual(updatedLocalNodeConfig.node.name);
expect(updatedNode['location']).toEqual(updatedLocalNodeConfig.node.location);
expect(updatedNode['capabilities']).toEqual(updatedLocalNodeConfig.node.capabilities);
// Get the updated system
const addedSystem = await getSystemByName("Testing P25 System Name");
console.log("Added system:", addedSystem);
expect(addedSystem['_id']).toBeDefined(); // Check if _id property exists
expect(addedSystem['nodes']).toBeDefined(); // Check if nodes property exists
expect(addedSystem.nodes).toEqual(updatedLocalNodeConfig.node.nuid) // Check if this node ID is in the nodes array
expect(addedSystem['frequencies']).toEqual(updatedLocalNodeConfig.nearbySystems['Testing P25 System Name'].frequencies);
expect(addedSystem['mode']).toEqual(updatedLocalNodeConfig.nearbySystems['Testing P25 System Name'].mode);
});
it('should update a node and its nearby systems', async () => {
// Get the existing node in the database
const existingNode = await getNodeByNuid(localNodeConfig.node.nuid);
// Assert that existingNode matches the updatedLocalNodeConfig
expect(existingNode['_id']).toBeDefined(); // Check if _id property exists
expect(existingNode['nuid']).toEqual(updatedLocalNodeConfig.node.nuid);
expect(existingNode['name']).toEqual(updatedLocalNodeConfig.node.name);
expect(existingNode['location']).toEqual(updatedLocalNodeConfig.node.location);
expect(existingNode['capabilities']).toEqual(updatedLocalNodeConfig.node.capabilities);
// Get the updated system
const existingSystem = await getSystemByName("Testing P25 System Name");
expect(existingSystem['_id']).toBeDefined(); // Check if _id property exists
expect(existingSystem['nodes']).toBeDefined(); // Check if nodes property exists
expect(existingSystem.nodes).toContain(updatedLocalNodeConfig.node.nuid); // Check if this node ID is in the nodes array
expect(existingSystem['frequencies']).toEqual(updatedLocalNodeConfig.nearbySystems['Testing P25 System Name'].frequencies);
expect(existingSystem['mode']).toEqual(updatedLocalNodeConfig.nearbySystems['Testing P25 System Name'].mode);
// Wait for the update
const node_update = new Promise(res => {
clientSocket.on('node-update-successful', async () => {
res();
});
});
// Emit the update command
clientSocket.emit("node-update", localNodeConfig);
// Wait for the successful update event
await node_update;
const updatedNode = await getNodeByNuid(localNodeConfig.node.nuid);
console.log("Updated Node:", updatedNode);
// Assert that the node is added correctly
expect(updatedNode['_id']).toBeDefined(); // Check if _id property exists
expect(updatedNode['nuid']).toEqual(localNodeConfig.node.nuid);
expect(updatedNode['name']).toEqual(localNodeConfig.node.name);
expect(updatedNode['location']).toEqual(localNodeConfig.node.location);
expect(updatedNode['capabilities']).toEqual(localNodeConfig.node.capabilities);
// Get the updated system
const updatedSystem = await getSystemByName("Testing P25 System Name");
console.log("Updated system:", updatedSystem);
expect(updatedSystem['_id']).toBeDefined(); // Check if _id property exists
expect(updatedSystem['nodes']).toBeDefined(); // Check if nodes property exists
expect(updatedSystem.nodes).toContain(localNodeConfig.node.nuid); // Check if this node ID is in the nodes array
expect(updatedSystem['frequencies']).toEqual(localNodeConfig.nearbySystems['Testing P25 System Name'].frequencies);
expect(updatedSystem['mode']).toEqual(localNodeConfig.nearbySystems['Testing P25 System Name'].mode);
});
});
// Test getNodeCurrentListeningSystem
describe('Get Node Current Listening System', () => {
it('should correctly determine if the node is connected to a voice channel', async () => {
// Simulate that the client socket is listening to a system
const isConnectedToVC = true;
const guildId = 'mockGuildId';
// Emit the event to the server and wait for the response
const nodeReply = new Promise((resolve) => {
clientSocket.once('node-check-connected-status', (passedGuildId, callback) => {
// Check if the passed guild ID matches the expected guild ID
expect(passedGuildId).toEqual(guildId);
// Simulate receiving the connection status from the client
callback(isConnectedToVC);
});
// Call the function to check if the node is connected to a voice channel
const response = checkIfNodeIsConnectedToVC(nodeIo, guildId, localNodeConfig.node.nuid);
resolve(response);
});
// Wait for the promise to resolve
const response = await nodeReply;
// Assert that the response matches the expected connection status
expect(response).toEqual(isConnectedToVC);
});
});
// Test checkIfNodeIsConnectedToVC
describe('Check if Node is Connected to VC', () => {
it('Should correctly determine if the node is connected to a voice channel', async () => {
// Simulate that the client socket is listening to a system
const isConnectedToVC = true;
const guildId = 'mockGuildId';
// Emit the event to the server and wait for the response
const nodeReply = new Promise((resolve) => {
clientSocket.once('node-check-connected-status', (passedGuildId, callback) => {
// Check if the passed guild ID matches the expected guild ID
expect(passedGuildId).to.equal(guildId);
// Simulate receiving the connection status from the client
callback(isConnectedToVC);
});
// Call the function to check if the node is connected to a voice channel
const response = checkIfNodeIsConnectedToVC(nodeIo, guildId, localNodeConfig.node.nuid);
resolve(response);
});
// Wait for the promise to resolve
const response = await nodeReply;
// Assert that the response matches the expected connection status
expect(response).to.equal(isConnectedToVC);
});
});
// Test checkIfNodeHasOpenDiscordClient
describe('Check if Node has an open discord client', () => {
it('should correctly determine if the node has an open Discord client', async () => {
const isDiscordOpen = true;
// Emit the event to the server and wait for the response
const nodeReply = new Promise((resolve) => {
clientSocket.once('node-check-discord-open-client', (callback) => {
// Simulate receiving the client status from the client
callback(isDiscordOpen);
});
// Call the function to check if the node has an open Discord client
const response = checkIfNodeHasOpenDiscordClient(serverClientSocket);
resolve(response);
});
// Wait for the promise to resolve
const response = await nodeReply;
// Assert that the response matches the expected client status
expect(response).toEqual(isDiscordOpen);
});
});
// Test getNodeDiscordUsername
describe('Get the discord username from the client', () => {
it('should request the username from a specific client', async () => {
const discordUsername = "Test Discord Username";
const guildId = 'mockGuildId';
// Emit the event to the server and wait for the response
const nodeReply = new Promise((resolve) => {
clientSocket.once('node-get-discord-username', (passedGuildId, callback) => {
// Check if the passed guild ID matches the expected guild ID
expect(passedGuildId).toEqual(guildId);
// Simulate receiving the username from the client
callback(discordUsername);
});
// Call the function to get the Discord username
const username = getNodeDiscordUsername(nodeIo, guildId, localNodeConfig.node.nuid);
resolve(username);
});
// Wait for the promise to resolve
const username = await nodeReply;
// Assert that the username matches the expected username
expect(username).toEqual(discordUsername);
});
});
// Test getNodeDiscordID
describe('Get the discord ID from the client', () => {
it('Should get the ID from the client', async () => {
// Mocked Discord ID
const discordId = "mockDiscordID";
// Emit the event to the server and wait for the response
const nodeReply = new Promise((resolve) => {
// Listen for the 'node-get-discord-id' event from the server
clientSocket.once('node-get-discord-id', (callback) => {
// Simulate receiving the Discord ID from the client
callback(discordId);
});
// Call the function to get the Discord ID
const response = getNodeDiscordID(serverClientSocket);
resolve(response);
});
// Wait for the promise to resolve
const response = await nodeReply;
// Assert that the response matches the expected Discord ID
expect(response).toEqual(discordId);
});
});
// Test requestNodeJoinSystem
describe('Request Node Join System', () => {
it('Should send a request to the node to join a system', async () => {
const systemName = 'mockSystemName';
const channelId = 'mockChannelId';
const token = 'mockToken';
// Emit the event to the server and wait for the response
await new Promise(async (resolve) => {
clientSocket.once('node-join', (joinData) => {
// Check if the passed system ID matches the expected system ID
expect(joinData.clientID).toEqual(token);
expect(joinData.channelID).toEqual(channelId);
expect(joinData.system).toEqual(systemName);
// Simulate receiving a success callback from the client
resolve();
});
// Call the function to request joining a system
requestNodeJoinSystem(serverClientSocket, systemName, channelId, token);
});
});
});
// Test requestNodeLeaveSystem
describe('Request Node Leave System', () => {
it('Should send a request to the node to leave a given server', async () => {
const guildId = 'mockGuildId';
// Emit the event to the server and wait for the response
await new Promise(async (resolve) => {
clientSocket.once('node-leave', (passedGuildId) => {
// Check if the passed system ID matches the expected system ID
expect(passedGuildId).toEqual(guildId);
// Simulate receiving a success callback from the client
resolve();
});
// Call the function to request joining a system
requestBotLeaveServer(serverClientSocket, guildId);
});
});
});
// Test requestNodeUpdate
describe('Request Node Update', () => {
it('Should send the node a request to check for an update', async () => {
// Emit the event to the server and wait for the response
await new Promise((resolve) => {
clientSocket.once('node-request-update', (callback) => {
// Simulate an out of date request
expect(callback).toBeDefined();
callback(true);
});
// Call the function to request updating node information
const response = requestNodeUpdate(serverClientSocket);
resolve(response);
});
});
});
// Test nodeDisconnectWrapper
describe('Node Disconnect Wrapper', () => {
it('Should disconnect the node and trigger cleanup actions', async () => {
// Mock the socket ID
const socketId = 'mockSocketId';
// Call the nodeDisconnectWrapper function
const result = await nodeDisconnectWrapper(socketId);
// Assert that the result is as expected (if any)
expect(result).toBeUndefined();
});
});
});

File diff suppressed because it is too large Load Diff