Initial RSS implementation

- Added debug command to trigger RSS feed update from discord
This commit is contained in:
Logan Cusano
2024-05-22 00:17:36 -04:00
parent fac5274715
commit 4e71c7b167
8 changed files with 680 additions and 3 deletions

View File

@@ -0,0 +1,71 @@
import { SlashCommandBuilder } from 'discord.js';
import { DebugBuilder } from "../../modules/debugger.mjs";
import { addSource } from '../../rss-manager/feedHandler.mjs'
const log = new DebugBuilder("server", "add");
// Exporting data property that contains the command structure for discord including any params
export const data = new SlashCommandBuilder()
.setName('add')
.setDescription('Add RSS Source')
.addStringOption(option =>
option.setName('title')
.setDescription('The title of the RSS feed')
.setRequired(true))
.addStringOption(option =>
option.setName('link')
.setDescription('The link to the RSS feed')
.setRequired(true))
.addStringOption(option =>
option.setName('category')
.setDescription('The category for the RSS feed *("ALL" by default")*')
.setRequired(false))
// 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 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
*/
// TODO - Setup autocorrect for the category
/*
export async function autocomplete(nodeIo, interaction) {
const focusedValue = interaction.options.getFocused();
const choices = [];
const filtered = choices.filter(choice => choice.name.startsWith(focusedValue));
console.log(focusedValue, choices, filtered);
await interaction.respond(filtered);
}
*/
/**
* The function to run when the command is called by a discord user
* @param {any} nodeIo The nodeIO server for manipulation of sockets
* @param {any} interaction The interaction object
*/
export const execute = async (nodeIo, interaction) => {
try {
var title = interaction.options.getString('title');
var link = interaction.options.getString('link');
var category = interaction.options.getString('category');
if (!category) category = "ALL";
await addSource(title, link, category, interaction.guildId, interaction.channelId, (err, result) => {
log.DEBUG("Result from adding entry", result);
if (result) {
interaction.reply(`Adding ${title} to the list of RSS sources`);
} else {
interaction.reply(`${title} already exists in the list of RSS sources`);
}
});
} catch (err) {
log.ERROR(err)
await interaction.reply(err.toString());
}
}

View File

@@ -0,0 +1,45 @@
import { SlashCommandBuilder } from 'discord.js';
import { updateFeeds } from '../../rss-manager/feedHandler.mjs'
// Exporting data property that contains the command structure for discord including any params
export const data = new SlashCommandBuilder()
.setName('trigger-rss')
.setDescription('Manually triggers an RSS feed update');
// 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 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 = [];
const filtered = choices.filter(choice => choice.name.startsWith(focusedValue));
console.log(focusedValue, choices, filtered);
await interaction.respond(filtered);
}
*/
/**
* The function to run when the command is called by a discord user
* @param {any} nodeIo The nodeIO server for manipulation of sockets
* @param {any} interaction The interaction object
*/
export const execute = async (nodeIo, interaction) => {
try {
//const sockets = await nodeIo.allSockets();
//console.log("All open sockets: ", sockets);
//await interaction.reply(`**Online Sockets: '${sockets}'**`);
await interaction.reply('Triggering RSS update');
await updateFeeds(interaction.client);
//await interaction.channel.send('**Pong.**');
} catch (err) {
console.error(err);
// await interaction.reply(err.toString());
}
}

View File

@@ -0,0 +1,107 @@
// Import necessary modules
import { EmbedBuilder } from 'discord.js';
import { DebugBuilder } from "../../modules/debugger.mjs";
import { parse } from "node-html-parser";
import { config } from 'dotenv';
// Load environment variables
config();
const log = new DebugBuilder("server", "libUtils");
const imageRegex = /(http(s?):)([/|.|\w|\s|-])*((\.(?:jpg|gif|png|webm))|(\/gallery\/(?:[/|.|\w|\s|-])*))/g;
const youtubeVideoRegex = /((?:https?:)?\/\/)?((?:www|m)\.)?((?:youtube(-nocookie)?\.com|youtu.be))(\/(?:[\w\-]+\?v=|embed\/|v\/)?)([\w\-]+)/g;
export class EmmeliaEmbedBuilder extends EmbedBuilder {
constructor() {
super();
this.setTimestamp();
this.setFooter({ text: 'Brought to you by Emmelia.' });
}
}
export const sendPost = (post, source, channel) => {
log.DEBUG("Sending post from source: ", post, source);
const postTitle = String(post.title).substring(0, 150);
const postLink = post.link;
let postContent;
if (post.content) {
// Reset the content parameter with the encoded parameter
post.content = parse(post['content:encoded'] ?? post.content);
// Get the post content and trim it to length or add a placeholder if necessary
let postText = String(post.content.text);
if (postText.length >= 3800) postText = `${postText.slice(0, 3800).substring(0, Math.min(postText.length, postText.lastIndexOf(" ")))} [...](${post.link})`;
else if (postText.length === 0) postText = `*This post has no content* [Direct Link](${post.link})`;
postContent = postText;
} else {
postContent = `*This post has no content* [Direct Link](${post.link})`;
}
// Check for embedded youtube videos and add the first four as links
const ytVideos = String(post.content).match(youtubeVideoRegex);
if (ytVideos) {
for (let ytVideo of ytVideos.slice(0, 4)) {
// If the video is an embed, replace the embed to make it watchable
if (ytVideo.includes("embed")) ytVideo = ytVideo.replace("embed/", "watch?v=");
postContent += `\nEmbeded Video from Post: [YouTube](${ytVideo})`;
}
}
log.DEBUG("Post content: ", postContent);
const postId = post.postId;
if (!post.pubDate) post.pubDate = Date.now();
const postPubDate = new Date(post.pubDate).toISOString();
const postSourceLink = source.title;
let postImage = post.image ?? undefined;
if (!postImage) {
if (post.content) {
const linksInPost = post.content.querySelectorAll("a");
if (linksInPost) {
log.DEBUG("Found links in post:", linksInPost);
for (const link of linksInPost) {
// Check to see if this link is a youtube video that was already found, if so skip it
if (ytVideos?.includes(link)) continue;
const images = String(link.getAttribute("href")).match(imageRegex);
log.DEBUG("Images found in post:", images);
if (images) {
postImage = images[0];
}
}
}
}
}
log.DEBUG("Sending an RSS post to discord", postTitle, postId, postContent);
try {
const rssMessage = new EmmeliaEmbedBuilder()
.setColor(0x0099FF)
.setTitle(postTitle)
.setURL(postLink)
.addFields({ name: 'Source', value: postSourceLink, inline: true })
.addFields({ name: 'Published', value: postPubDate, inline: true });
// TODO - If there is more than one image, create a canvas and post the created canvas
if (postImage) {
log.DEBUG("Image from post:", postImage);
rssMessage.setImage(postImage);
}
// Add the main content if it's present
postContent = postContent.slice(0, 4090).trim();
if (postContent) rssMessage.setDescription(postContent);
const channelResponse = rssMessage;
//const channelResponse = channel.send({ embeds: [rssMessage] });
log.DEBUG("Channel send response", channelResponse);
return channelResponse;
} catch (err) {
log.ERROR("Error sending message: ", postTitle, postId, postContent, postPubDate, err);
return err;
}
};