const TurndownService = require("turndown"); const RSSParser = require("rss-parser"); const db = require("../db.js"); const interval = require("interval-promise"); const client = require("../client.js"); const sha1 = require("sha1"); const verifyChannelID = db.get("newsPostVerifyChannel").value(); const reactionCollectors = {}; const verifyMessageIdToPostId = {}; const turndown = new TurndownService(); turndown.addRule("image", { filter: "img", replacement: () => "" }); turndown.addRule("link", { filter: node => node.nodeName === "A" &&node.getAttribute("href"), replacement: (content, node) => node.getAttribute("href") }); const parser = new RSSParser(); const RSS_UPDATE_INTERVAL_MIN = 5; function getThreadId(url) { let result = url.substring(url.lastIndexOf(".") + 1); if(result.endsWith("/")) result = result.substring(0, result.length - 1); return result; } async function checkFeeds() { console.log(`Checking feeds on ${new Date().toISOString()}`); let feeds = db.get("rssFeeds").value(); let oldNews = db.get("postedNewsGuids"); for(let feedEntry of feeds) { let feed = await parser.parseURL(feedEntry.url); if(feed.items.length == 0) continue; let printableItems = feed.items.filter(i => i.isoDate > feedEntry.lastUpdate).sort((a, b) => a.isoDate.localeCompare(b.isoDate)); if(printableItems.length > 0) { printableItems.forEach(item => { let itemID = getThreadId(item.guid); let contents = item[feedEntry.contentElement]; let itemObj = { id: itemID, title: item.title, link: item.link, creator: item.creator, contents: contents, hash: sha1(contents), messageId: null, verifyMessageId: null }; if(oldNews.has(itemObj.id).value()){ let data = oldNews.get(itemObj.id).value(); // Old type, don't care if(data === true) return; // Add message ID to mark for edit if(data.hash != itemObj.hash) itemObj.messageId = data.messageId; else return; } if(!shouldVerify()) sendNews(itemObj); else addVerifyMessage(itemObj); }); let lastUpdateDate = printableItems[printableItems.length - 1].isoDate; console.log(`Setting last update marker on ${feedEntry.url} to ${lastUpdateDate}`); db.get("rssFeeds").find({ url: feedEntry.url}).assign({lastUpdate: lastUpdateDate}).write(); } } } function initPendingReactors() { let verifyChannel = client.channels.get(verifyChannelID); db.get("newsCache").forOwn(async i => { let m = await verifyChannel.fetchMessage(i.verifyMessageId); let collector = m.createReactionCollector(isVerifyReaction, { maxEmojis: 1 }); collector.on("collect", collectReaction) reactionCollectors[m.id] = collector; verifyMessageIdToPostId[m.id] = i.id; }).value(); } async function addVerifyMessage(item) { let verifyChannel = client.channels.get(verifyChannelID); let cache = db.get("newsCache"); let postedNews = db.get("postedNewsGuids"); let process = "🆕 ADD"; if(postedNews.has(item.id).value()) process = "✏️ EDIT"; if(cache.has(item.id).value()) { let oldItem = cache.get(item.id).value(); let oldMessage = await verifyChannel.fetchMessage(oldItem.verifyMessageId); if(oldMessage) await oldMessage.delete(); } let newMessage = await verifyChannel.send(`[${process}] Post ID: **${item.id}** ${newsToString(item)} React with ✅ (approve) or ❌ (deny).` ); await newMessage.react("✅"); await newMessage.react("❌"); var collector = newMessage.createReactionCollector(isVerifyReaction, { maxEmojis: 1 }); collector.on("collect", collectReaction) reactionCollectors[newMessage.id] = collector; verifyMessageIdToPostId[newMessage.id] = item.id; item.verifyMessageId = newMessage.id; cache.set(item.id, item).write(); } function collectReaction(reaction, collector) { let cache = db.get("newsCache"); let oldNews = db.get("postedNewsGuids"); let m = reaction.message; collector.stop(); delete reactionCollectors[m.id]; let postId = verifyMessageIdToPostId[m.id]; let item = cache.get(postId).value(); cache.unset(postId).write(); m.delete(); if(reaction.emoji.name == "✅") sendNews(item); else oldNews.set(item.id, { hash: item.hash, messageId: null }).write(); } async function sendNews(item) { let outChannel = db.get("feedOutputChannel").value(); let oldNews = db.get("postedNewsGuids"); let sentMessage = await postNewsItem(outChannel, item); oldNews.set(item.id, { hash: item.hash, messageId: sentMessage.id }).write(); } function isVerifyReaction(reaction, user) { return (reaction.emoji.name == "✅" || reaction.emoji.name == "❌") && !user.bot; } function shouldVerify() { return verifyChannelID != ""; } async function postNewsItem(channel, item) { let newsText = newsToString(item); let ch = client.channels.get(channel); if(item.messageId) { let message = await ch.fetchMessage(item.messageId); if(message) return await message.edit(newsText); else return await ch.send(newsText); } else return await ch.send(newsText); } function newsToString(item) { return `**${item.title}** Posted by ${item.creator} ${item.link} ${turndown.turndown(item.contents).replace(/( {2}\n|\n\n){2,}/gm, "\n").replace(item.link, "")}`; } const onStart = () => { initPendingReactors(); interval(checkFeeds, RSS_UPDATE_INTERVAL_MIN * 60 * 1000); }; module.exports = { onStart: onStart };