rss_checker.js 8.6 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277
  1. const TurndownService = require("turndown");
  2. const RSSParser = require("rss-parser");
  3. const db = require("../db.js");
  4. const interval = require("interval-promise");
  5. const client = require("../client.js");
  6. const sha1 = require("sha1");
  7. const html = require("node-html-parser");
  8. const axios = require("axios");
  9. const PREVIEW_CHAR_LIMIT = 300;
  10. const verifyChannelID = db.get("newsPostVerifyChannel").value();
  11. const reactionCollectors = {};
  12. const verifyMessageIdToPostId = {};
  13. const turndown = new TurndownService();
  14. turndown.addRule("image", {
  15. filter: "img",
  16. replacement: () => ""
  17. });
  18. turndown.addRule("link", {
  19. filter: node => node.nodeName === "A" &&node.getAttribute("href"),
  20. replacement: (content, node) => node.getAttribute("href")
  21. });
  22. const parser = new RSSParser();
  23. const RSS_UPDATE_INTERVAL_MIN = 5;
  24. function getThreadId(url) {
  25. let result = url.substring(url.lastIndexOf(".") + 1);
  26. if(result.endsWith("/"))
  27. result = result.substring(0, result.length - 1);
  28. return result;
  29. }
  30. async function checkFeeds() {
  31. console.log(`Checking feeds on ${new Date().toISOString()}`);
  32. let feeds = db.get("rssFeeds").value();
  33. let oldNews = db.get("postedNewsGuids");
  34. for(let feedEntry of feeds) {
  35. let feed = await parser.parseURL(feedEntry.url);
  36. if(feed.items.length == 0)
  37. continue;
  38. let printableItems = feed.items.sort((a, b) => a.isoDate.localeCompare(b.isoDate));
  39. if(printableItems.length > 0) {
  40. for(let item of printableItems) {
  41. let itemID = getThreadId(item.guid);
  42. let contents = null;
  43. try {
  44. let res = await axios.get(item.link);
  45. if(res.status != 200) {
  46. console.log(`Post ${itemID} could not be loaded because request returned status ${res.status}`);
  47. continue;
  48. }
  49. let rootNode = html.parse(res.data, {
  50. pre: true,
  51. script: false,
  52. style: false
  53. });
  54. let opDiv = rootNode.querySelector("div.bbWrapper");
  55. if (!opDiv) {
  56. console.log(`No posts found for ${itemID}!`);
  57. continue;
  58. }
  59. contents = markdownify(opDiv.outerHTML, item.link);
  60. } catch(err){
  61. console.log(`Failed to get html for item ${itemID} because ${err}`);
  62. continue;
  63. }
  64. let itemObj = {
  65. id: itemID,
  66. title: item.title,
  67. link: item.link,
  68. creator: item.creator,
  69. contents: `${contents.substr(0, Math.min(contents.length, PREVIEW_CHAR_LIMIT))}...`,
  70. hash: sha1(contents),
  71. messageId: null,
  72. verifyMessageId: null,
  73. type: null
  74. };
  75. if(oldNews.has(itemObj.id).value()){
  76. let data = oldNews.get(itemObj.id).value();
  77. // Old type, don't care
  78. if(data === true)
  79. continue;
  80. // Add message ID to mark for edit
  81. if(data.hash != itemObj.hash)
  82. itemObj.messageId = data.messageId;
  83. else
  84. continue;
  85. }
  86. if(!shouldVerify())
  87. await sendNews(itemObj);
  88. else
  89. await addVerifyMessage(itemObj);
  90. }
  91. let lastUpdateDate = printableItems[printableItems.length - 1].isoDate;
  92. console.log(`Setting last update marker on ${feedEntry.url} to ${lastUpdateDate}`);
  93. db.get("rssFeeds").find({ url: feedEntry.url}).assign({lastUpdate: lastUpdateDate}).write();
  94. }
  95. }
  96. }
  97. function initPendingReactors() {
  98. let verifyChannel = client.channels.get(verifyChannelID);
  99. db.get("newsCache").forOwn(async i => {
  100. let m = await tryFetchMessage(verifyChannel, i.verifyMessageId);
  101. let collector = m.createReactionCollector(isVerifyReaction, { maxEmojis: 1 });
  102. collector.on("collect", collectReaction)
  103. reactionCollectors[m.id] = collector;
  104. verifyMessageIdToPostId[m.id] = i.id;
  105. }).value();
  106. }
  107. async function addVerifyMessage(item) {
  108. let verifyChannel = client.channels.get(verifyChannelID);
  109. let cache = db.get("newsCache");
  110. let oldNews = db.get("postedNewsGuids");
  111. let postedNews = db.get("postedNewsGuids");
  112. item.type = "🆕 ADD";
  113. if(postedNews.has(item.id).value())
  114. item.type = "✏️ EDIT";
  115. if(cache.has(item.id).value()) {
  116. let oldItem = cache.get(item.id).value();
  117. let oldMessage = await tryFetchMessage(verifyChannel, oldItem.verifyMessageId);
  118. if(oldMessage)
  119. await oldMessage.delete();
  120. }
  121. let newMessage = await verifyChannel.send(toVerifyString(item));
  122. await newMessage.react("✅");
  123. await newMessage.react("❌");
  124. var collector = newMessage.createReactionCollector(isVerifyReaction, { maxEmojis: 1 });
  125. collector.on("collect", collectReaction)
  126. reactionCollectors[newMessage.id] = collector;
  127. verifyMessageIdToPostId[newMessage.id] = item.id;
  128. item.verifyMessageId = newMessage.id;
  129. cache.set(item.id, item).write();
  130. oldNews.set(item.id, {
  131. hash: item.hash,
  132. messageId: null
  133. }).write();
  134. }
  135. function collectReaction(reaction, collector) {
  136. let cache = db.get("newsCache");
  137. let m = reaction.message;
  138. collector.stop();
  139. delete reactionCollectors[m.id];
  140. let postId = verifyMessageIdToPostId[m.id];
  141. let item = cache.get(postId).value();
  142. cache.unset(postId).write();
  143. m.delete();
  144. if(reaction.emoji.name == "✅")
  145. sendNews(item);
  146. }
  147. async function sendNews(item) {
  148. let outChannel = db.get("feedOutputChannel").value();
  149. let oldNews = db.get("postedNewsGuids");
  150. let sentMessage = await postNewsItem(outChannel, item);
  151. oldNews.set(item.id, {
  152. hash: item.hash,
  153. messageId: sentMessage.id
  154. }).write();
  155. }
  156. function isVerifyReaction(reaction, user) {
  157. return (reaction.emoji.name == "✅" || reaction.emoji.name == "❌") && !user.bot;
  158. }
  159. async function tryFetchMessage(channel, messageId) {
  160. try {
  161. return await channel.fetchMessage(messageId);
  162. }catch(error){
  163. return null;
  164. }
  165. }
  166. function shouldVerify() {
  167. return verifyChannelID != "";
  168. }
  169. async function postNewsItem(channel, item) {
  170. let newsMessage = toNewsString(item);
  171. let ch = client.channels.get(channel);
  172. if(item.messageId) {
  173. let message = await tryFetchMessage(ch, item.messageId);
  174. if(message)
  175. return await message.edit(newsMessage);
  176. else
  177. return await ch.send(newsMessage);
  178. }
  179. else
  180. return await ch.send(newsMessage);
  181. }
  182. function markdownify(htmStr, link) {
  183. return turndown.turndown(htmStr).replace(/( {2}\n|\n\n){2,}/gm, "\n").replace(link, "");
  184. }
  185. function toNewsString(item) {
  186. return `**${item.title}**
  187. Posted by ${item.creator}
  188. ${item.link}
  189. ${item.contents}`;
  190. }
  191. function toVerifyString(item) {
  192. return `[${item.type}]
  193. Post ID: **${item.id}**
  194. ${toNewsString(item)}
  195. React with ✅ (approve) or ❌ (deny).`;
  196. }
  197. const onStart = () => {
  198. initPendingReactors();
  199. interval(checkFeeds, RSS_UPDATE_INTERVAL_MIN * 60 * 1000);
  200. };
  201. const commands = [
  202. {
  203. pattern: /^edit (\d+)\s+((.*[\n\r]*)+)$/i,
  204. action: async (msg, s, match) => {
  205. if(msg.channel.id != verifyChannelID)
  206. return;
  207. let id = match[1];
  208. let newContents = match[2].trim();
  209. if(!db.get("newsCache").has(id).value()) {
  210. msg.channel.send(`${msg.author.toString()} No unapproved news items with id ${id}!`);
  211. return;
  212. }
  213. let item = db.get("newsCache").get(id).value();
  214. let editMsg = await tryFetchMessage(client.channels.get(verifyChannelID), item.verifyMessageId);
  215. if(!editMsg){
  216. msg.channel.send(`${msg.author.toString()} No verify messafe found for ${id}! This is a bug: report to horse.`);
  217. return;
  218. }
  219. item.contents = newContents;
  220. db.get("newsCache").set(id, item).write();
  221. await editMsg.edit(toVerifyString(item));
  222. await msg.delete();
  223. }
  224. }
  225. ];
  226. module.exports = {
  227. onStart: onStart,
  228. commands: commands
  229. };