123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348 |
- import TurndownService from "turndown";
- import interval from "interval-promise";
- import { client, FORUMS_DOMAIN } from "../client";
- import sha1 from "sha1";
- import { TextChannel, Message, ReactionCollector, MessageReaction, 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 { logger } from "src/logging";
- import { Command, ICommandData, Plugin } from "src/model/plugin";
- 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;
- @Plugin
- 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) => node.nodeName === "A" && node.getAttribute("href") != null,
- replacement: (content: string, node: Node) => (node as HTMLElement).getAttribute("htef") ?? ""
- });
- }
- bbCodeToMarkdown(bbCode: string): string {
- const html = render(bbCode).replace(/\n/gm, "</br>");
- return this.turndown.turndown(html).trim();//.replace(/( {2}\n|\n\n){2,}/gm, "\n");
- }
- checkFeeds = async (): Promise<void> => {
- try {
- logger.info(`Checking feeds on ${new Date().toISOString()}`);
- const forumsNewsRepo = getRepository(PostedForumNewsItem);
- const postVerifyMessageRepo = getRepository(PostVerifyMessage);
- const forumThreads = await client.forum.getForumThreads(NEWS_FORUM_ID);
- for (const thread of [...forumThreads.threads, ...forumThreads.sticky]) {
- const firstPost = await client.forum.getPost(thread.first_post_id);
- const 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
- })
- });
- const 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) {
- const 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) {
- logger.error("Failed to check forums: %s", err);
- }
- }
- async initPendingReactors(): Promise<void> {
- if (!this.verifyChannelId)
- return;
- const verifyChannel = client.bot.channels.resolve(this.verifyChannelId);
- if (!verifyChannel)
- return;
- const repo = getRepository(PostedForumNewsItem);
- const verifyMessageRepo = getRepository(PostVerifyMessage);
- const pendingVerifyMessages = await repo.find({
- where: { verifyMessage: Not(IsNull()) },
- select: ["id"],
- relations: ["verifyMessage"]
- });
- for (const msg of pendingVerifyMessages) {
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- const m = await this.tryFetchMessage(verifyChannel, msg.verifyMessage!.messageId);
- if (!m) {
- await repo.update({ id: msg.id }, { verifyMessage: undefined });
- // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
- await verifyMessageRepo.delete(msg.verifyMessage!);
- continue;
- }
- const collector = m.createReactionCollector(this.isVerifyReaction, { maxEmojis: 1 });
- collector.on("collect", this.collectorFor(collector));
- this.reactionCollectors[m.id] = collector;
- this.verifyMessageIdToPost[m.id] = msg.id;
- }
- }
- async addVerifyMessage(item: PostedForumNewsItem): Promise<void> {
- if (!this.verifyChannelId)
- return;
- if (!item.verifyMessage)
- throw new Error("No verify message! This shouldn't happen!");
- const verifyChannel = client.bot.channels.resolve(this.verifyChannelId) as TextChannel;
- if (!verifyChannel) {
- logger.warn(`Skipping adding item ${item.id} because no verify channel is set up!`);
- return;
- }
- const verifyMessageRepo = getRepository(PostVerifyMessage);
- const forumsNewsRepo = getRepository(PostedForumNewsItem);
- if (item.verifyMessage.messageId) {
- const oldMessage = await this.tryFetchMessage(verifyChannel, item.verifyMessage.messageId);
- if (oldMessage)
- await oldMessage.delete();
- }
- const newMessage = await verifyChannel.send(this.toVerifyString(item.id, item.verifyMessage)) as Message;
- item.verifyMessage.messageId = newMessage.id;
- await newMessage.react("✅");
- await newMessage.react("❌");
- const collector = newMessage.createReactionCollector(this.isVerifyReaction, { maxEmojis: 1 });
- collector.on("collect", this.collectorFor(collector));
- this.reactionCollectors[newMessage.id] = collector;
- this.verifyMessageIdToPost[newMessage.id] = item.id;
- item.verifyMessage = await verifyMessageRepo.save(item.verifyMessage);
- await forumsNewsRepo.save(item);
- }
- collectorFor = (collector: ReactionCollector) =>
- async (reaction: MessageReaction): Promise<void> => {
- const verifyMessageRepo = getRepository(PostVerifyMessage);
- const postRepo = getRepository(PostedForumNewsItem);
-
- const m = reaction.message;
- collector.stop();
- delete this.reactionCollectors[m.id];
- const postId = this.verifyMessageIdToPost[m.id];
-
- const 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): Promise<void> {
- const channelRepo = getRepository(KnownChannel);
- const newsPostRepo = getRepository(PostedForumNewsItem);
- const outChannel = await channelRepo.findOne({
- where: { channelType: NEWS_FEED_CHANNEL }
- });
- if (!outChannel)
- return;
- const sentMessage = await this.postNewsItem(outChannel.channelId, item);
- item.postedMessageId = sentMessage?.id;
- item.verifyMessage = undefined;
- await newsPostRepo.save(item);
- }
- isVerifyReaction = (reaction: MessageReaction, user: User): boolean => {
- return (reaction.emoji.name == "✅" || reaction.emoji.name == "❌") && !user.bot;
- }
- async tryFetchMessage(channel?: Channel, messageId?: string): Promise<Message | null> {
- if(!channel || !messageId)
- return null;
- try {
- if (!(channel instanceof TextChannel))
- return null;
- return await channel.messages.fetch(messageId);
- } catch (error) {
- return null;
- }
- }
- get shouldVerify(): boolean {
- return this.verifyChannelId != undefined;
- }
- async postNewsItem(channel: string, item: PostedForumNewsItem): Promise<Message | null> {
- if (!item.verifyMessage)
- throw new Error("No message to send!");
- const newsMessage = this.toNewsString(item.verifyMessage);
- const ch = client.bot.channels.resolve(channel);
- if (!(ch instanceof TextChannel))
- return null;
- if (item.postedMessageId) {
- const 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): string {
- return `**${item.title}**
- Posted by ${item.author}
- ${item.link}
-
- ${item.text}`;
- }
- toVerifyString(postId: string, item: PostVerifyMessage): string {
- return `[${item.isNew ? "🆕 ADD" : "✏️ EDIT"}]
- Post ID: **${postId}**
-
- ${this.toNewsString(item)}
-
- React with ✅ (approve) or ❌ (deny).`;
- }
- @Command({
- type: "mention",
- pattern: /^edit (\d+)\s+((.*[\n\r]*)+)$/i
- })
- async editPreview({ message, contents }: ICommandData): Promise<void> {
- if (message.channel.id != this.verifyChannelId)
- return;
-
- const match = contents as RegExpMatchArray;
- const id = match[1];
- const newContents = match[2].trim();
- const repo = getRepository(PostedForumNewsItem);
- const verifyRepo = getRepository(PostVerifyMessage);
- const post = await repo.findOne({
- where: { id: id },
- relations: ["verifyMessage"]
- });
- if (!post || !post.verifyMessage) {
- message.reply(`no unapproved news items with id ${id}!`);
- return;
- }
- const editMsg = await this.tryFetchMessage(client.bot.channels.resolve(this.verifyChannelId) ?? undefined, post.verifyMessage.messageId);
- if (!editMsg) {
- message.reply(`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 message.delete();
- }
- async start(): Promise<void> {
- const repo = getRepository(KnownChannel);
- const verifyChannel = await repo.findOne({
- channelType: NEWS_POST_VERIFY_CHANNEL
- });
- if (!verifyChannel)
- return;
- this.verifyChannelId = verifyChannel.channelId;
- const user = await client.forum.getMe();
- this.botUserId = user.user_id;
- await this.initPendingReactors();
- interval(this.checkFeeds, RSS_UPDATE_INTERVAL_MIN * 60 * 1000);
- }
- }
|