123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343 |
- import TurndownService, { Options } from "turndown";
- import interval from "interval-promise";
- import { client, FORUMS_DOMAIN } from "../client";
- import sha1 from "sha1";
- import { TextChannel, Message, ReactionCollector, MessageReaction, Collector, User, Channel } from "discord.js";
- import { Dict } from "../util";
- import { getRepository, Not, IsNull } from "typeorm";
- import { PostedForumNewsItem } from "@shared/db/entity/PostedForumsNewsItem";
- import { KnownChannel } from "@shared/db/entity/KnownChannel";
- import { PostVerifyMessage } from "@shared/db/entity/PostVerifyMessage";
- import { render } from "../bbcode-parser/bbcode-js";
- import { CommandSet, Command } from "src/model/command";
- const PREVIEW_CHAR_LIMIT = 300;
- const NEWS_POST_VERIFY_CHANNEL = "newsPostVerify";
- const NEWS_FEED_CHANNEL = "newsFeed";
- const RSS_UPDATE_INTERVAL_MIN = process.env.NODE_ENV == "dev" ? 60 : 5;
- const NEWS_FORUM_ID = 49;
- @CommandSet
- export class ForumsNewsChecker {
- verifyMessageIdToPost: Dict<string> = {}
- reactionCollectors: Dict<ReactionCollector> = {};
- verifyChannelId?: string;
- botUserId = 0;
- turndown = new TurndownService();
- constructor() {
- this.turndown.addRule("image", {
- filter: "img",
- replacement: () => ""
- });
- this.turndown.addRule("link", {
- filter: (node: HTMLElement, opts: Options) => node.nodeName === "A" && node.getAttribute("href") != null,
- replacement: (content: string, node: Node) => (node instanceof HTMLElement ? node.getAttribute("href") : null) ?? ""
- });
- }
- bbCodeToMarkdown(bbCode: string) {
- let html = render(bbCode).replace(/\n/gm, "</br>");
- return this.turndown.turndown(html).trim();//.replace(/( {2}\n|\n\n){2,}/gm, "\n");
- }
- checkFeeds = async () => {
- try {
- console.log(`Checking feeds on ${new Date().toISOString()}`);
- let forumsNewsRepo = getRepository(PostedForumNewsItem);
- let postVerifyMessageRepo = getRepository(PostVerifyMessage);
- let forumThreads = await client.forum.getForumThreads(NEWS_FORUM_ID);
- for (let thread of [...forumThreads.threads, ...forumThreads.sticky]) {
- let firstPost = await client.forum.getPost(thread.first_post_id);
- let contents = this.bbCodeToMarkdown(firstPost.message);
- let itemObj = forumsNewsRepo.create({
- id: thread.thread_id.toString(),
- hash: sha1(firstPost.message),
- verifyMessage: postVerifyMessageRepo.create({
- author: thread.username,
- link: `${FORUMS_DOMAIN}/index.php?threads/${thread.thread_id}/`,
- title: thread.title,
- text: `${contents.substr(0, Math.min(contents.length, PREVIEW_CHAR_LIMIT))}...`,
- isNew: true
- })
- });
- let postItem = await forumsNewsRepo.findOne({
- where: { id: itemObj.id },
- relations: ["verifyMessage"]
- });
- if (postItem) {
- if (process.env.IGNORE_CHANGED_NEWS === "TRUE") {
- await forumsNewsRepo.update({
- id: postItem.id
- }, {
- hash: itemObj.hash
- });
- continue;
- }
- // Add message ID to mark for edit
- if (postItem.hash != itemObj.hash) {
- let newHash = itemObj.hash;
- if (!postItem.verifyMessage)
- postItem.verifyMessage = itemObj.verifyMessage;
- itemObj = postItem;
- if (itemObj.verifyMessage)
- itemObj.verifyMessage.isNew = false;
- itemObj.hash = newHash;
- }
- else
- continue;
- }
- if (!this.verifyChannelId || firstPost.user_id == this.botUserId)
- await this.sendNews(itemObj);
- else
- await this.addVerifyMessage(itemObj);
- }
- } catch (err) {
- console.log(`Failed to check forums because ${err}`);
- }
- }
- async initPendingReactors() {
- if (!this.verifyChannelId)
- return;
- let verifyChannel = client.bot.channels.resolve(this.verifyChannelId);
- if (!verifyChannel)
- return;
- let repo = getRepository(PostedForumNewsItem);
- let verifyMessageRepo = getRepository(PostVerifyMessage);
- let pendingVerifyMessages = await repo.find({
- where: { verifyMessage: Not(IsNull()) },
- select: ["id"],
- relations: ["verifyMessage"]
- });
- for (let msg of pendingVerifyMessages) {
- let m = await this.tryFetchMessage(verifyChannel, msg.verifyMessage!.messageId);
- if (!m) {
- await repo.update({ id: msg.id }, { verifyMessage: undefined });
- await verifyMessageRepo.delete(msg.verifyMessage!);
- continue;
- }
- let collector = m.createReactionCollector(this.isVerifyReaction, { maxEmojis: 1 });
- collector.on("collect", this.collectReaction);
- this.reactionCollectors[m.id] = collector;
- this.verifyMessageIdToPost[m.id] = msg.id;
- }
- }
- async addVerifyMessage(item: PostedForumNewsItem) {
- if (!this.verifyChannelId)
- return;
- if (!item.verifyMessage)
- throw new Error("No verify message! This shouldn't happen!");
- let verifyChannel = client.bot.channels.resolve(this.verifyChannelId) as TextChannel;
- if (!verifyChannel) {
- console.log(`Skipping adding item ${item.id} because no verify channel is set up!`);
- return;
- }
- let verifyMessageRepo = getRepository(PostVerifyMessage);
- let forumsNewsRepo = getRepository(PostedForumNewsItem);
- if (item.verifyMessage.messageId) {
- let oldMessage = await this.tryFetchMessage(verifyChannel, item.verifyMessage.messageId);
- if (oldMessage)
- await oldMessage.delete();
- }
- let newMessage = await verifyChannel.send(this.toVerifyString(item.id, item.verifyMessage)) as Message;
- item.verifyMessage.messageId = newMessage.id;
- await newMessage.react("✅");
- await newMessage.react("❌");
- let collector = newMessage.createReactionCollector(this.isVerifyReaction, { maxEmojis: 1 });
- collector.on("collect", this.collectReaction)
- this.reactionCollectors[newMessage.id] = collector;
- this.verifyMessageIdToPost[newMessage.id] = item.id;
- item.verifyMessage = await verifyMessageRepo.save(item.verifyMessage);
- await forumsNewsRepo.save(item);
- }
- collectReaction = async (reaction: MessageReaction, collector: Collector<string, MessageReaction>) => {
- let verifyMessageRepo = getRepository(PostVerifyMessage);
- let postRepo = getRepository(PostedForumNewsItem);
- let m = reaction.message;
- collector.stop();
- delete this.reactionCollectors[m.id];
- let postId = this.verifyMessageIdToPost[m.id];
- let post = await postRepo.findOne({
- where: { id: postId },
- relations: ["verifyMessage"]
- });
- if (!post)
- throw new Error("Post not found!");
- await postRepo.update({ id: post.id }, { verifyMessage: undefined });
- if (post.verifyMessage)
- await verifyMessageRepo.delete({ id: post.verifyMessage.id });
- await reaction.message.delete();
- if (reaction.emoji.name == "✅")
- this.sendNews(post);
- delete this.verifyMessageIdToPost[m.id];
- }
- async sendNews(item: PostedForumNewsItem) {
- let channelRepo = getRepository(KnownChannel);
- let newsPostRepo = getRepository(PostedForumNewsItem);
- let outChannel = await channelRepo.findOne({
- where: { channelType: NEWS_FEED_CHANNEL }
- });
- if (!outChannel)
- return;
- let sentMessage = await this.postNewsItem(outChannel.channelId, item);
- item.postedMessageId = sentMessage?.id;
- item.verifyMessage = undefined;
- await newsPostRepo.save(item);
- }
- isVerifyReaction = (reaction: MessageReaction, user: User) => {
- return (reaction.emoji.name == "✅" || reaction.emoji.name == "❌") && !user.bot;
- }
- async tryFetchMessage(channel?: Channel, messageId?: string) {
- if(!channel || !messageId)
- return null;
- try {
- if (!(channel instanceof TextChannel))
- return null;
- return await channel.messages.fetch(messageId);
- } catch (error) {
- return null;
- }
- }
- get shouldVerify() {
- return this.verifyChannelId != undefined;
- }
- async postNewsItem(channel: string, item: PostedForumNewsItem): Promise<Message | null> {
- if (!item.verifyMessage)
- throw new Error("No message to send!");
- let newsMessage = this.toNewsString(item.verifyMessage);
- let ch = client.bot.channels.resolve(channel);
- if (!(ch instanceof TextChannel))
- return null;
- if (item.postedMessageId) {
- let message = await this.tryFetchMessage(ch, item.postedMessageId);
- if (message)
- return await message.edit(newsMessage);
- else
- return await ch.send(newsMessage) as Message;
- }
- else
- return await ch.send(newsMessage) as Message;
- }
- toNewsString(item: PostVerifyMessage) {
- return `**${item.title}**
- Posted by ${item.author}
- ${item.link}
-
- ${item.text}`;
- }
- toVerifyString(postId: string, item: PostVerifyMessage) {
- return `[${item.isNew ? "🆕 ADD" : "✏️ EDIT"}]
- Post ID: **${postId}**
-
- ${this.toNewsString(item)}
-
- React with ✅ (approve) or ❌ (deny).`;
- }
- @Command({
- pattern: /^edit (\d+)\s+((.*[\n\r]*)+)$/i
- })
- async editPreview(msg: Message, contests: string, match: RegExpMatchArray) {
- if (msg.channel.id != this.verifyChannelId)
- return;
- let id = match[1];
- let newContents = match[2].trim();
- let repo = getRepository(PostedForumNewsItem);
- let verifyRepo = getRepository(PostVerifyMessage);
- let post = await repo.findOne({
- where: { id: id },
- relations: ["verifyMessage"]
- });
- if (!post || !post.verifyMessage) {
- msg.channel.send(`${msg.author.toString()} No unapproved news items with id ${id}!`);
- return;
- }
- let editMsg = await this.tryFetchMessage(client.bot.channels.resolve(this.verifyChannelId) ?? undefined, post.verifyMessage.messageId);
- if (!editMsg) {
- msg.channel.send(`${msg.author.toString()} No verify message found for ${id}! This is a bug: report to horse.`);
- return;
- }
- post.verifyMessage.text = newContents;
- await verifyRepo.save(post.verifyMessage);
- await editMsg.edit(this.toVerifyString(post.id, post.verifyMessage));
- await msg.delete();
- }
- async onStart() {
- let repo = getRepository(KnownChannel);
- let verifyChannel = await repo.findOne({
- channelType: NEWS_POST_VERIFY_CHANNEL
- });
- if (!verifyChannel)
- return;
- this.verifyChannelId = verifyChannel.channelId;
- let user = await client.forum.getMe();
- this.botUserId = user.user_id;
- await this.initPendingReactors();
- interval(this.checkFeeds, RSS_UPDATE_INTERVAL_MIN * 60 * 1000);
- }
- };
|