Compare commits

...

3 Commits

Author SHA1 Message Date
Logan Cusano
43dfa7b5ad RSS improvements
All checks were successful
DRB Tests / drb_mocha_tests (pull_request) Successful in 31s
- Updated rss discord command name scheme
- Implemented new sourceManager for handling feed sources
- Added wrappers to delete/get feed sources by title
2024-05-26 01:26:12 -04:00
Logan Cusano
8f56fd9b92 Updated default ping command with AC defaults 2024-05-26 01:23:33 -04:00
Logan Cusano
702e291fcb #5 improve the debugger
- Update the logic
- Will now check to make sure the given file exists before writing
2024-05-26 01:22:47 -04:00
9 changed files with 219 additions and 110 deletions

View File

@@ -19,10 +19,10 @@ 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 = []; const choices = []; // The array to be filled with the autocorrect values
const filtered = choices.filter(choice => choice.name.startsWith(focusedValue)); const filtered = choices.filter(choice => choice.name.startsWith(focusedValue));
log.INFO(focusedValue, choices, filtered); log.INFO(focusedValue, choices, filtered);
await interaction.respond(filtered); await interaction.respond(filtered.map(choice => ({name: choice.name, value: choice.name})));
} }
*/ */

View File

@@ -1,12 +1,12 @@
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/feedHandler.mjs' import { addSource } from '../../rss-manager/sourceManager.mjs'
const log = new DebugBuilder("server", "discordBot.command.add"); 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('add') .setName('rss-add')
.setDescription('Add RSS Source') .setDescription('Add RSS Source')
.addStringOption(option => .addStringOption(option =>
option.setName('title') option.setName('title')
@@ -22,7 +22,7 @@ export const data = new SlashCommandBuilder()
.setRequired(false)) .setRequired(false))
// Exporting other properties // Exporting other properties
export const example = "/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 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
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.
/** /**
@@ -59,7 +59,7 @@ export const execute = async (nodeIo, interaction) => {
log.DEBUG("Result from adding entry", result); log.DEBUG("Result from adding entry", result);
if (result) { if (result) {
interaction.reply(`Adding ${title} to the list of RSS sources`); interaction.reply(`Successfully added ${title} to the list of RSS sources`);
} else { } else {
interaction.reply(`${title} already exists in the list of RSS sources`); interaction.reply(`${title} already exists in the list of RSS sources`);
} }

View File

@@ -0,0 +1,58 @@
import { SlashCommandBuilder } from 'discord.js';
import { DebugBuilder } from "../../modules/debugger.mjs";
import { removeSource } from '../../rss-manager/sourceManager.mjs'
import { getAllFeeds, deleteFeedByTitle } from '../../modules/mongo-wrappers/mongoFeedsWrappers.mjs'
const log = new DebugBuilder("server", "discordBot.command.rssRemove");
// Exporting data property that contains the command structure for discord including any params
export const data = new SlashCommandBuilder()
.setName('rss-remove')
.setDescription('Add RSS Source')
.addStringOption(option =>
option.setName('title')
.setDescription('The title of the RSS feed')
.setRequired(true)
.setAutocomplete(true))
// 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 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 = await getAllFeeds() ?? [];
log.INFO("RSS Remove Choices:", choices);
const filtered = choices.filter(choice => choice.title.startsWith(focusedValue));
log.DEBUG(focusedValue, choices, filtered);
await interaction.respond(filtered.map(choice => ({ name: choice.title, value: choice.title })));
}
/**
* 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 {
var title = interaction.options.getString('title');
interaction.reply(`Removing ${title} from the list of RSS sources, please wait...`);
const results = await deleteFeedByTitle(title);
if (!results) {
log.WARN(`Failed to remove source: ${title}`);
interaction.editReply(`Failed to remove source: '${title}'`);
return;
}
interaction.editReply(`${title} was successfully removed from the RSS sources.`)
} catch (err) {
log.ERROR(err)
interaction.editReply(err.toString());
}
}

View File

@@ -1,15 +1,15 @@
import { DebugBuilder } from "../../modules/debugger.mjs"; import { DebugBuilder } from "../../modules/debugger.mjs";
const log = new DebugBuilder("server", "discordBot.command.triggerRss"); 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('trigger-rss') .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 = "/trigger-rss"; // 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
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.
/** /**

View File

@@ -12,7 +12,7 @@ const log = new DebugBuilder("server", "discordBot.modules.rssWrappers");
const imageRegex = /(http(s?):)([/|.|\w|\s|-])*((\.(?:jpg|gif|png|webm))|(\/gallery\/(?:[/|.|\w|\s|-])*))/g; const imageRegex = /(http(s?):)([/|.|\w|\s|-])*((\.(?:jpg|gif|png|webm))|(\/gallery\/(?:[/|.|\w|\s|-])*))/g;
const youtubeVideoRegex = /((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube(-nocookie)?\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)/g; const youtubeVideoRegex = /((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube(-nocookie)?\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)/g;
export class EmmeliaEmbedBuilder extends EmbedBuilder { export class DRBEmbedBuilder extends EmbedBuilder {
constructor() { constructor() {
super(); super();
this.setTimestamp(); this.setTimestamp();
@@ -66,7 +66,7 @@ export const sendPost = (post, source, channel) => {
log.DEBUG("Post content: ", postContent); log.DEBUG("Post content: ", postContent);
try { try {
const rssMessage = new EmmeliaEmbedBuilder() const rssMessage = new DRBEmbedBuilder()
.setColor(0x0099FF) .setColor(0x0099FF)
.setTitle(postTitle) .setTitle(postTitle)
.setURL(postLink) .setURL(postLink)

View File

@@ -1,25 +1,35 @@
// Import necessary modules // Import necessary modules
import debug from 'debug'; import debug from 'debug';
import { config } from 'dotenv'; import { config } from 'dotenv';
import { writeFile } from 'fs'; config();
import { promises as fs } from 'fs';
import { join, dirname } from 'path';
import { inspect } from 'util'; import { inspect } from 'util';
// Load environment variables /**
config(); * Write a given message to the log file
* @param {any} logMessage The message to write to the log file
const logLocation = process.env.LOG_LOCATION; * @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`);
// Ensure the log directory exists
try {
await fs.mkdir(dirname(logLocation), { recursive: true });
} catch (err) {
console.error(err);
}
// Ensure the message is a string
logMessage = `${String(logMessage)}\n`; logMessage = `${String(logMessage)}\n`;
writeFile( // Write to the file
logLocation ?? `./${appName}.log`, try {
logMessage, await fs.writeFile(logLocation, logMessage, { encoding: 'utf-8', flag: 'a+' });
{ encoding: "utf-8", flag: 'a+' }, } catch (err) {
(err) => { console.error(err);
if (err) console.error(err); }
}
);
}; };
/** /**
@@ -30,37 +40,31 @@ const writeToLog = async (logMessage, appName) => {
*/ */
export class DebugBuilder { export class DebugBuilder {
constructor(appName, fileName) { constructor(appName, fileName) {
this.INFO = (...messageParts) => { const buildLogger = (level) => (...messageParts) => {
const _info = debug(`${appName}:${fileName}:INFO`); const logger = debug(`${appName}:${fileName}:${level}`);
_info(messageParts); logger(messageParts);
writeToLog(`${new Date().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:INFO\t-\t${messageParts.map(messagePart => inspect(messagePart))}`, appName);
}; const timeStamp = new Date().toLocaleString('en-US', { timeZone: 'America/New_York' });
const message = `${timeStamp} - ${appName}:${fileName}:${level}\t-\t${messageParts.map(part => inspect(part)).join(' ')}`;
this.DEBUG = (...messageParts) => {
const _debug = debug(`${appName}:${fileName}:DEBUG`); // Write to console
_debug(messageParts); console.log(message);
writeToLog(`${new Date().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:DEBUG\t-\t${messageParts.map(messagePart => inspect(messagePart))}`, appName);
}; // Write to logfile
writeToLog(message, appName);
this.VERBOSE = (...messageParts) => {
const _verbose = debug(`${appName}:${fileName}:VERBOSE`);
_verbose(messageParts);
writeToLog(`${new Date().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:VERBOSE\t-\t${messageParts.map(messagePart => inspect(messagePart))}`, appName);
};
this.WARN = (...messageParts) => {
const _warn = debug(`${appName}:${fileName}:WARNING`);
_warn(messageParts);
writeToLog(`${new Date().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:WARNING\t-\t${messageParts.map(messagePart => inspect(messagePart))}`, appName);
}; };
this.INFO = buildLogger('INFO');
this.DEBUG = buildLogger('DEBUG');
this.VERBOSE = buildLogger('VERBOSE');
this.WARN = buildLogger('WARNING');
this.ERROR = (...messageParts) => { this.ERROR = (...messageParts) => {
const _error = debug(`${appName}:${fileName}:ERROR`); buildLogger('ERROR')(...messageParts);
_error(messageParts);
writeToLog(`${new Date().toLocaleString('en-US', { timeZone: 'America/New_York' })} - ${appName}:${fileName}:ERROR\t-\t${messageParts.map(messagePart => inspect(messagePart))}`, appName);
if (process.env.EXIT_ON_ERROR && process.env.EXIT_ON_ERROR > 0) { if (process.env.EXIT_ON_ERROR && process.env.EXIT_ON_ERROR > 0) {
writeToLog("!--- EXITING ---!", appName); writeToLog("!--- EXITING ---!", appName);
setTimeout(() => process.exit(), process.env.EXIT_ON_ERROR_DELAY ?? 0); const exitDelay = parseInt(process.env.EXIT_ON_ERROR_DELAY, 10) || 0;
setTimeout(() => process.exit(1), exitDelay);
} }
}; };
} }

View File

@@ -44,6 +44,17 @@ import {
} }
}; };
// 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 // Wrapper for updating a feed by link
export const updateFeedByLink = async (link, updatedFields) => { export const updateFeedByLink = async (link, updatedFields) => {
try { try {
@@ -66,6 +77,17 @@ import {
} }
}; };
// 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 // Wrapper for inserting a post
export const createPost = async (post) => { export const createPost = async (post) => {
try { try {

View File

@@ -1,7 +1,8 @@
import { createFeed, getAllFeeds, getFeedByLink, updateFeedByLink, deleteFeedByLink, createPost, getPostByPostId } from '../modules/mongo-wrappers/mongoFeedsWrappers.mjs'; import { getAllFeeds, deleteFeedByLink, createPost, getPostByPostId } from '../modules/mongo-wrappers/mongoFeedsWrappers.mjs';
import crypto from 'crypto'; import crypto from 'crypto';
import { sendPost } from '../discordBot/modules/rssWrappers.mjs'; 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 UserAgent from "user-agents"; import UserAgent from "user-agents";
import Parser from 'rss-parser'; import Parser from 'rss-parser';
@@ -19,14 +20,16 @@ const parser = new Parser({
}); });
const log = new DebugBuilder("server", "feedHandler"); const log = new DebugBuilder("server", "feedHandler");
const sourceFailureLimit = process.env.RSS_SOURCE_FAILURE_LIMIT ?? 5;
const runningSourcesToRemove = {}; // This holds the sources that are pending removal (they've failed to load, return data, etc.)
export const returnHash = (...stringsIncluded) => { export const returnHash = (...stringsIncluded) => {
return crypto.createHash('sha1').update(stringsIncluded.join("-<<??//\\\\??>>-")).digest("base64"); return crypto.createHash('sha1').update(stringsIncluded.join("-<<??//\\\\??>>-")).digest("base64");
}; };
/**
* Update the active RSS feeds and send any new posts to their discord channels
* @param {any} client The discord client to send posts with
* @returns {any}
*/
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");
@@ -88,56 +91,3 @@ export const updateFeeds = async (client) => {
throw error; throw error;
} }
}; };
export const addSource = async (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);
callback(null, record);
} catch (err) {
log.ERROR("Error adding source:", err);
callback(err, null);
}
};
export const removeSource = async (sourceURL) => {
log.INFO("Removing source:", sourceURL);
if (!runningSourcesToRemove[sourceURL]) {
runningSourcesToRemove[sourceURL] = { count: 1, timestamp: Date.now(), ignoredAttempts: 0 };
return;
}
const elapsedTime = Date.now() - runningSourcesToRemove[sourceURL].timestamp;
const waitTime = runningSourcesToRemove[sourceURL].count * 30000;
if (elapsedTime <= waitTime) {
runningSourcesToRemove[sourceURL].ignoredAttempts += 1;
return;
}
if (runningSourcesToRemove[sourceURL].count < sourceFailureLimit) {
runningSourcesToRemove[sourceURL].count += 1;
runningSourcesToRemove[sourceURL].timestamp = Date.now();
return;
}
try {
const record = await getFeedByLink(sourceURL);
if (!record) {
log.ERROR("Source not found in storage");
return;
}
const results = await deleteFeedByLink(sourceURL);
if (!results) {
log.WARN("Failed to remove source");
return;
}
log.DEBUG("Source removed after exceeding failure limit:", sourceURL);
} catch (err) {
log.ERROR("Error removing source from storage:", err);
}
};

View File

@@ -0,0 +1,75 @@
import { createFeed, getFeedByLink, deleteFeedByLink } from '../modules/mongo-wrappers/mongoFeedsWrappers.mjs';
class SourceManager {
constructor(sourceFailureLimit) {
this.sourceFailureLimit = sourceFailureLimit;
this.runningSourcesToRemove = {};
}
async removeSource(sourceURL) {
log.INFO(`Removing source: ${sourceURL}`);
const currentTime = Date.now();
const sourceData = this.runningSourcesToRemove[sourceURL];
if (!sourceData) {
this.runningSourcesToRemove[sourceURL] = { count: 1, timestamp: currentTime, ignoredAttempts: 0 };
return;
}
const elapsedTimeSinceLastAttempt = currentTime - sourceData.timestamp;
const waitTime = sourceData.count * 30000;
if (elapsedTimeSinceLastAttempt <= waitTime) {
sourceData.ignoredAttempts += 1;
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);
}
}
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
const defaultSourceManager = new SourceManager();
// Export the class and default instance methods
export { SourceManager };
export const addSource = defaultSourceManager.addSource.bind(defaultSourceManager);
export const removeSource = defaultSourceManager.removeSource.bind(defaultSourceManager);