4 Commits d45120d41f ... a187b71e88

Author SHA1 Message Date
  ghorsington a187b71e88 Only warn non-messages for normal messages 2 years ago
  ghorsington 38c5bd7bdf Don't fail on reply 2 years ago
  ghorsington c1e7aafe8b Update Node to 16 2 years ago
  ghorsington 4702b3bcf0 Update packages, refactor code 2 years ago

+ 1 - 1
bot/Dockerfile

@@ -1,4 +1,4 @@
-FROM node:14-alpine
+FROM node:16-alpine
 
 RUN apk --no-cache add make
 

File diff suppressed because it is too large
+ 9434 - 458
bot/package-lock.json


+ 31 - 32
bot/package.json

@@ -26,48 +26,47 @@
       "src": "./build"
    },
    "dependencies": {
-      "@types/cheerio": "^0.22.28",
-      "@types/humanize-duration": "^3.18.1",
-      "@types/koa": "^2.13.1",
-      "@types/koa-router": "^7.4.2",
-      "@types/lowdb": "^1.0.9",
-      "@types/node-schedule": "^1.3.1",
-      "@types/sha1": "^1.1.2",
-      "@types/turndown": "^5.0.0",
-      "@types/ws": "^7.4.1",
-      "@types/xml2js": "^0.4.8",
-      "cheerio": "^1.0.0-rc.6",
-      "discord.js": "^12.5.3",
-      "dotenv": "^8.2.0",
+      "@types/express-serve-static-core": "^4.17.21",
+      "@types/cheerio": "^0.22.30",
+      "@types/humanize-duration": "^3.25.1",
+      "@types/koa": "^2.13.4",
+      "@types/koa-router": "^7.4.4",
+      "@types/node-schedule": "^1.3.2",
+      "@types/sha1": "^1.1.3",
+      "@types/turndown": "^5.0.1",
+      "@types/ws": "^7.4.7",
+      "@types/xml2js": "^0.4.9",
+      "cheerio": "^1.0.0-rc.10",
+      "discord.js": "^13.0.1",
+      "dotenv": "^10.0.0",
       "emoji-regex": "^9.2.2",
       "express": "^4.17.1",
-      "form-data": "^3.0.1",
-      "fp-ts": "^2.10.4",
-      "google-protobuf": "^3.15.8",
+      "form-data": "^4.0.0",
+      "fp-ts": "^2.11.1",
+      "google-protobuf": "^3.17.3",
       "got": "^11.8.2",
       "html2bbcode": "^1.2.6",
-      "humanize-duration": "^3.25.2",
+      "humanize-duration": "^3.27.0",
       "interval-promise": "^1.4.0",
       "io-ts": "^2.2.16",
       "jimp": "^0.16.1",
       "koa": "^2.13.1",
       "koa-body": "^4.2.0",
       "koa-router": "^10.0.0",
-      "lowdb": "^1.0.0",
       "module-alias": "^2.2.2",
-      "node-schedule": "^1.3.3",
-      "nodemailer": "^6.5.0",
-      "pg": "^8.6.0",
+      "node-schedule": "^2.0.0",
+      "nodemailer": "^6.6.3",
+      "pg": "^8.7.1",
       "reflect-metadata": "^0.1.13",
       "rimraf": "^3.0.2",
       "rpc_ts": "^2.1.0",
       "rss-parser": "^3.12.0",
       "sha1": "^1.1.1",
-      "tsconfig-paths": "^3.9.0",
-      "turndown": "^7.0.0",
+      "tsconfig-paths": "^3.10.1",
+      "turndown": "^7.1.1",
       "type-zoo": "^3.4.1",
-      "typeorm": "^0.2.32",
-      "typescript": "^4.2.4",
+      "typeorm": "^0.2.36",
+      "typescript": "^4.3.5",
       "typescript-rest-rpc": "^1.0.10",
       "uws": "^200.0.0",
       "winston": "^3.3.3",
@@ -76,12 +75,12 @@
       "yaml": "^1.10.2"
    },
    "devDependencies": {
-      "@types/node": "^14.14.41",
-      "@types/nodemailer": "^6.4.1",
-      "@typescript-eslint/eslint-plugin": "^4.22.0",
-      "@typescript-eslint/parser": "^4.22.0",
-      "eslint": "^7.25.0",
-      "nodemon": "^2.0.7",
-      "ts-node": "9.0.0"
+      "@types/node": "^16.4.13",
+      "@types/nodemailer": "^6.4.4",
+      "@typescript-eslint/eslint-plugin": "^4.29.0",
+      "@typescript-eslint/parser": "^4.29.0",
+      "eslint": "^7.32.0",
+      "nodemon": "^2.0.12",
+      "ts-node": "^10.1.0"
    }
 }

+ 12 - 2
bot/src/client.ts

@@ -1,10 +1,20 @@
-import { Client, ClientUser, Util } from "discord.js";
+import { Client, ClientUser, Intents, Util } from "discord.js";
 import { XenforoClient } from "./xenforo";
 
 export const FORUMS_DOMAIN = "https://custommaid3d2.com";
 
 export class BotClient {
-    public bot = new Client();
+    public bot = new Client({
+        intents: [
+            Intents.FLAGS.GUILDS,
+            Intents.FLAGS.GUILD_MEMBERS,
+            Intents.FLAGS.GUILD_EMOJIS_AND_STICKERS,
+            Intents.FLAGS.GUILD_MESSAGES,
+            Intents.FLAGS.GUILD_MESSAGE_REACTIONS,
+            Intents.FLAGS.DIRECT_MESSAGES,
+            Intents.FLAGS.DIRECT_MESSAGE_REACTIONS,
+        ]
+    });
     public forum = new XenforoClient(`${FORUMS_DOMAIN}/api`, process.env.FORUM_API_KEY ?? "");
 
     get botUser(): ClientUser {

+ 1 - 1
bot/src/main.ts

@@ -21,7 +21,7 @@ client.bot.on("ready", async () => {
     logger.info("NoctBot is ready");
 });
 
-client.bot.on("message", async m => {
+client.bot.on("messageCreate", async m => {
     if (m.author.id == client.botUser.id)
         return;
 

+ 5 - 5
bot/src/plugin_manager.ts

@@ -81,9 +81,9 @@ export class PluginManager {
         for (const evtName of Object.keys(this.botEvents)) {
             if (!Object.prototype.hasOwnProperty.call(this.botEvents, evtName) || isCustomEvent(evtName))
                 continue;
-            client.on(evtName as keyof ClientEvents, async (...args: unknown[]) =>
-                await this.trigger(evtName as EventType, ...args)
-            );
+            client.on(evtName as keyof ClientEvents, async (...args: unknown[]) => {
+                await this.trigger(evtName as EventType, ...args);
+            });
         }
     
         for (const plugin of this.plugins) {
@@ -129,8 +129,8 @@ export class PluginManager {
             }
 
             if (match) {
-                if (!c.allowDM && m.channel.type == "dm") {
-                    await tryDo(m.reply("Sorry, this command is not available in DMs for now."));
+                if (!c.allowDM && m.channel.type == "DM") {
+                    await tryDo(m.reply({ content: "Sorry, this command is not available in DMs for now.", failIfNotExists: false }));
                     return false;
                 }
                 if (c.auth && !(await isAuthorisedAsync(m.member)))

+ 4 - 3
bot/src/plugins/facemorph.ts

@@ -66,7 +66,7 @@ export class Facemorph {
             return jimpImage;
         const emojiKeys = process.env.FOOLS != "TRUE" ? [
             ...emoteGuild
-                .emojis.cache.filter(e => !e.animated && e.name.startsWith("PADORU") == padoru)
+                .emojis.cache.filter(e => !e.animated && e.name?.startsWith("PADORU") == padoru)
                 .keys()
         ]: 
             [
@@ -249,7 +249,8 @@ export class Facemorph {
             successMessage ||
             `I noticed a face in the image. I think this looks better ${client.bot.emojis.resolve("505076258753740810")?.toString() ?? ":)"}`;
 
-        message.channel.send(messageContents, {
+        message.channel.send({
+            content: messageContents,
             files: [buffer]
         });
     }
@@ -260,7 +261,7 @@ export class Facemorph {
             .filter(v => !v.msg.author.bot && v.att != undefined).last() as AttachedMessage;
 
         if (!lastImagedMessage) {
-            msg.reply("sorry, I couldn't find any recent messages with images.");
+            msg.reply({ content: "sorry, I couldn't find any recent messages with images.", failIfNotExists: false });
             return;
         }
 

+ 4 - 0
bot/src/plugins/file_only_channel_checker.ts

@@ -27,6 +27,10 @@ export class FileOnlyChannelChecker {
 
         msg.delete();
 
+        // Non-default system message (e.g. pin or thread create) => there is no one to warn, so just remove it to keep the channel clean
+        if (msg.type != "DEFAULT")
+            return;
+
         const ch = msg.guild?.channels.resolve(entry.warningMessageChannelId);
         
         if(ch instanceof TextChannel)

+ 12 - 4
bot/src/plugins/forums_news_checker.ts

@@ -139,7 +139,12 @@ export class ForumsNewsChecker {
                 continue;
             }
 
-            const collector = m.createReactionCollector(this.isVerifyReaction, { maxEmojis: 1 });
+            
+
+            const collector = m.createReactionCollector({
+                filter: this.isVerifyReaction,
+                maxEmojis: 1,
+            });
             collector.on("collect", this.collectorFor(collector));
             this.reactionCollectors[m.id] = collector;
             this.verifyMessageIdToPost[m.id] = msg.id;
@@ -174,7 +179,10 @@ export class ForumsNewsChecker {
         await newMessage.react("✅");
         await newMessage.react("❌");
 
-        const collector = newMessage.createReactionCollector(this.isVerifyReaction, { maxEmojis: 1 });
+        const collector = newMessage.createReactionCollector({
+            filter: this.isVerifyReaction,
+            maxEmojis: 1
+        });
         collector.on("collect", this.collectorFor(collector));
         this.reactionCollectors[newMessage.id] = collector;
         this.verifyMessageIdToPost[newMessage.id] = item.id;
@@ -309,14 +317,14 @@ React with ✅ (approve) or ❌ (deny).`;
         });
 
         if (!post || !post.verifyMessage) {
-            message.reply(`no unapproved news items with id ${id}!`);
+            message.reply({ content: `no unapproved news items with id ${id}!`, failIfNotExists: false });
             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.`);
+            message.reply({ content: `no verify message found for ${id}! This is a bug: report to horse.`, failIfNotExists: false });
             return;
         }
 

+ 6 - 5
bot/src/plugins/give_role_for_react.ts

@@ -50,11 +50,11 @@ export class GiveRoleForReact {
         }
 
         const msgSendResult = await tryDo(message.channel.send({
-            embed: {
+            embeds: [{
                 title: params.title,
                 description: `${params.message}\n\nReact with ${REACT_EMOTE} to gain role ${role.toString()}`,
                 color: MSG_COLOR
-            }
+            }]
         }));
 
         if (!msgSendResult.ok) {
@@ -85,7 +85,8 @@ export class GiveRoleForReact {
         const check = (reaction: MessageReaction) => {
             return reaction.emoji.name == REACT_EMOTE;
         };
-        const collector = new ReactionCollector(msg, check, {
+        const collector = new ReactionCollector(msg, {
+            filter: check,
             dispose: true
         });
 
@@ -98,7 +99,7 @@ export class GiveRoleForReact {
             if (user.bot) {
                 return;
             }
-            const gu = guild.member(user);
+            const gu = guild.members.resolve(user);
             if (!gu) {
                 return;
             }
@@ -112,7 +113,7 @@ export class GiveRoleForReact {
             if (user.bot) {
                 return;
             }
-            const gu = guild.member(user);
+            const gu = guild.members.resolve(user);
             if (!gu) {
                 return;
             }

+ 13 - 9
bot/src/plugins/guide.ts

@@ -1,4 +1,4 @@
-import { DMChannel, Message, NewsChannel, TextChannel } from "discord.js";
+import { Message, TextBasedChannels } from "discord.js";
 import { getRepository } from "typeorm";
 import { Guide, GuideType, GuideKeyword } from "@shared/db/entity/Guide";
 import { Event, BotEventData, Command, ICommandData, Plugin } from "src/model/plugin";
@@ -38,7 +38,7 @@ export class GuideCommands {
             .getMany();
 
         const dmChannelResult = await tryDo(msg.author.createDM());
-        let channel: TextChannel | DMChannel | NewsChannel;
+        let channel: TextBasedChannels;
         if (dmChannelResult.ok){
             await tryDo(msg.delete());
             channel = dmChannelResult.result;
@@ -111,12 +111,12 @@ export class GuideCommands {
         const msgContents = match[4].trim();
 
         if (msgContents.length == 0) {
-            message.reply("the guide must have some content!");
+            message.reply({ content: "the guide must have some content!", failIfNotExists: false});
             return;
         }
         
         if (!(<string[]>Object.values(GuideType)).includes(type)) {
-            message.reply(`the type ${type} is not a valid guide type!`);
+            message.reply({ content: `the type ${type} is not a valid guide type!`, failIfNotExists: false});
             return;
         }
 
@@ -168,9 +168,10 @@ export class GuideCommands {
         } else
             await addGuide();
 
-        message.reply(
-            `Added/updated "${name}" (keywords \`${keywords.join(" ")}\`)!`
-        );
+        message.reply({
+            content: `Added/updated "${name}" (keywords \`${keywords.join(" ")}\`)!`,
+            failIfNotExists: false
+        });
     }
 
     @Command({
@@ -210,12 +211,15 @@ export class GuideCommands {
 
             if (guideKeywordsCount == dedupedKeywords.length) {
                 await guideRepo.delete({ id: existingGuide.id });
-                await message.reply(`removed ${type} "${keywords.join(" ")}"!`);
+                await message.reply({ content: `removed ${type} "${keywords.join(" ")}"!`, failIfNotExists: false});
                 return;
             }
         }
 
-        await message.reply(`no such ${type} with keywords \`${keywords.join(" ")}\`! Did you forget to specify all keywords?`);
+        await message.reply({
+            content: `no such ${type} with keywords \`${keywords.join(" ")}\`! Did you forget to specify all keywords?`,
+            failIfNotExists: false
+        });
     }
 
     @Command({

+ 1 - 1
bot/src/plugins/help.ts

@@ -43,6 +43,6 @@ export class Help {
                 return;
         }
 
-        message.reply(msg);
+        message.reply({ content: msg, failIfNotExists: false });
     }
 }

+ 4 - 3
bot/src/plugins/inspire.ts

@@ -17,16 +17,17 @@ export class Inspire {
         const result = await tryDo(got.get("https://inspirobot.me/api?generate=true"));
         if(!result.ok) {
             logger.error("Failed to get inspiration, error %s", result.error);
-            await message.reply("sorry, couldn't get inspiration :(.");
+            await message.reply({ content: "sorry, couldn't get inspiration :(.", failIfNotExists: false });
             return;
         }
 
-        const messagePostResult = await tryDo(message.reply("here is a piece of my wisdom:", {
+        const messagePostResult = await tryDo(message.reply({
+            content: "here is a piece of my wisdom:",
             files: [ result.result.body ]
         }));
         if (!messagePostResult.ok) {
             logger.error("Failed to get inspiration, error %s", messagePostResult.error);
-            await message.reply("sorry, couldn't get inspiration :(.");
+            await message.reply({ content: "sorry, couldn't get inspiration :(.", failIfNotExists: false });
         }
     }
 }

+ 16 - 12
bot/src/plugins/news_aggregator.ts

@@ -97,18 +97,22 @@ export class NewsAggregator {
             });
         }
 
-        const msg = await assertOk(ch.send(new MessageEmbed({
-            title: `${(isNew ? "**[NEW]**" : "**[EDIT]**")} ${item.title}`,
-            url: item.link,
-            color: item.embedColor,
-            timestamp: new Date(),
-            author: {
-                name: item.author
-            },
-            footer: {
-                text: "NoctBot News Aggregator"
-            }
-        }))) as Message;
+        const msg = await assertOk(ch.send({
+            embeds: [
+                new MessageEmbed({
+                    title: `${(isNew ? "**[NEW]**" : "**[EDIT]**")} ${item.title}`,
+                    url: item.link,
+                    color: item.embedColor,
+                    timestamp: new Date(),
+                    author: {
+                        name: item.author
+                    },
+                    footer: {
+                        text: "NoctBot News Aggregator"
+                    }
+                })
+            ]
+        }));
 
         newsItem.editMessageId = msg.id;
 

+ 5 - 5
bot/src/plugins/quote.ts

@@ -39,7 +39,7 @@ export class QuoteCommand {
             message: msg
         }));
 
-        message.reply(`added quote (ID: ${newQuote.id})!`);
+        message.reply({ content: `added quote (ID: ${newQuote.id})!`, failIfNotExists: false});
     }
 
     @Command({
@@ -59,7 +59,7 @@ export class QuoteCommand {
                                                 limit 1`) as Quote[];
 
         if (quotes.length == 0) {
-            message.reply("I have no quotes!");
+            message.reply({ content: "I have no quotes!", failIfNotExists: false });
             return;
         }
 
@@ -88,7 +88,7 @@ export class QuoteCommand {
         if (res.affected == 0)
             return;
 
-        message.reply(`removed quote #${val}!`);
+        message.reply({ content: `removed quote #${val}!`, failIfNotExists: false });
     }
 
     @Command({
@@ -106,11 +106,11 @@ export class QuoteCommand {
         const quotes = await repo.find();
 
         if (quotes.length == 0) {
-            message.reply("I have no quotes!");
+            message.reply({ content: "I have no quotes!", failIfNotExists: false });
             return;
         }
 
         const quotesListing = quotes.reduce((p, c) => `${p}[${c.id}] "${this.minify(c.message, 10)}" by ${c.author}\n`, "\n");
-        message.reply(`I know the following quotes:\n\`\`\`${quotesListing}\`\`\``);
+        message.reply({ content: `I know the following quotes:\n\`\`\`${quotesListing}\`\`\``, failIfNotExists: false });
     }
 }

+ 5 - 3
bot/src/plugins/rcg.ts

@@ -16,10 +16,11 @@ export class Rcg {
     async sendErrorMessage(msg: Message): Promise<void> {
         const xkcdResult = await tryDo(got.get<XkcdResponse>("https://xkcd.com/info.0.json", { responseType: "json" }));
         if(!xkcdResult.ok || !xkcdResult.result) {
-            await msg.reply("sorry, I couldn't get any comics :(.");
+            await msg.reply({ content: "sorry, I couldn't get any comics :(.", failIfNotExists: false });
             return;
         }
-        await msg.reply("sorry, I couldn't get a random comic! Here is today's XKCD instead:", {
+        await msg.reply({
+            content: "sorry, I couldn't get a random comic! Here is today's XKCD instead:",
             files: [ xkcdResult.result.body.img ]
         });
     }
@@ -82,7 +83,8 @@ export class Rcg {
         newImg.quality(80);
         const buffer = await newImg.getBufferAsync(Jimp.MIME_JPEG);
 
-        const messagePostResult = await tryDo(message.reply("I find this very funny:", {
+        const messagePostResult = await tryDo(message.reply({
+            content: "I find this very funny:",
             files: [ buffer ]
         }));
 

+ 6 - 6
bot/src/plugins/react.ts

@@ -38,7 +38,7 @@ export class ReactCommands {
             logger.verbose(`Reacting to message ${msg.id} because user ${user.tag} reacted to it`);
             const result = await tryDo(msg.react(reaction.emoji));
             if (!result.ok) {
-                logger.error("Failed to react to user %s#%s (%s): %s", msg.author.username, msg.author.discriminator, msg.author.id, result.error);
+                logger.error("Failed to react to user %s#%s (%s): %s", msg.author?.username, msg.author?.discriminator, msg.author?.id, result.error);
             }
         }
     }
@@ -60,7 +60,7 @@ export class ReactCommands {
             const reactionEmoji = reactContents[2];
 
             if (!client.bot.emojis.cache.has(reactionEmoji)) {
-                message.reply("I cannot react with this emoji :(");
+                message.reply({ content: "I cannot react with this emoji :(", failIfNotExists: false });
                 return;
             }
 
@@ -72,7 +72,7 @@ export class ReactCommands {
             });
             await repo.save(msgReaction);
 
-            message.reply("added reaction!");
+            message.reply({ content: "added reaction!", failIfNotExists: false });
         }
     }
 
@@ -91,10 +91,10 @@ export class ReactCommands {
         const result = await repo.delete({ message: content });
 
         if (result.affected == 0) {
-            message.reply("no such reaction available!");
+            message.reply({ content: "no such reaction available!", failIfNotExists: false });
             return;
         }
-        message.reply("removed reaction!");
+        message.reply({ content: "removed reaction!", failIfNotExists: false });
     }
 
     @Command({
@@ -113,7 +113,7 @@ export class ReactCommands {
         });
 
         const reactions = messages.reduce((p, c) => `${p}\n${c.message}`, "");
-        message.reply(`I'll react to the following messages:\n\`\`\`${reactions}\n\`\`\``);
+        message.reply({ content: `I'll react to the following messages:\n\`\`\`${reactions}\n\`\`\``, failIfNotExists: false });
     }
 
     @Event("message")

+ 2 - 1
bot/src/plugins/stickers.ts

@@ -31,7 +31,8 @@ export class Stickers {
         if (!deleteResult.ok) {
             logger.error("Stickers: failed to delete message %s from user %s. Reason: %s", msg.id, msg.author.id, deleteResult.error);
         }
-        const sendResult = await tryDo(msg.channel.send(`${msg.author.toString()} *sent a sticker:*`, {
+        const sendResult = await tryDo(msg.channel.send({
+            content: `${msg.author.toString()} *sent a sticker:*`,
             files: [this.stickers[stickerName]]
         }));
 

+ 48 - 44
bot/src/plugins/violation.ts

@@ -96,7 +96,7 @@ export class ViolationPlugin {
                 }
             },
             modify: (member: GuildMember | PartialGuildMember, settings: GuildViolationSettings, violation: DeepPartial<Mute>): DeepPartial<Mute> => {
-                const originalRoles = member.roles.cache.keyArray().filter(r => r != settings.muteRoleId);
+                const originalRoles = [...member.roles.cache.keys()].filter(r => r != settings.muteRoleId);
                 violation.previousRoles = originalRoles;
                 return violation;
             }
@@ -214,30 +214,30 @@ export class ViolationPlugin {
 
     private async removeTimedViolation<T extends TimedViolation>(type: ObjectType<T>, message: Message, command = "violation") {
         if (!message.guild) {
-            await message.reply("cannot do in DMs!");
+            await message.reply({ content: "cannot do in DMs!", failIfNotExists: false });
             return;
         }
 
         const settingsRepo = getRepository(GuildViolationSettings);
         const settings = await settingsRepo.findOne(message.guild.id);
         if (!settings) {
-            message.reply("this guild doesn't have violation settings set up!");
+            message.reply({ content: "this guild doesn't have violation settings set up!", failIfNotExists: false });
             return;
         }
 
         const [, userId] = parseArgs(message.content);
         if (!userId) {
-            await message.reply("no user specified!");
+            await message.reply({ content: "no user specified!", failIfNotExists: false });
             return;
         }
         if (userId == message.author.id) {
-            await message.reply(`cannot ${command} yourself!`);
+            await message.reply({ content: `cannot ${command} yourself!`, failIfNotExists: false });
             return;
         }
 
         const user = await this.resolveUser(userId);
         if (!user) {
-            await message.reply("couldn't find the given user!");
+            await message.reply({ content: "couldn't find the given user!", failIfNotExists: false });
             logger.error("Tried to un-%s user %s but couldn't find them by id!", command, userId);
             return;
         }
@@ -251,7 +251,7 @@ export class ViolationPlugin {
             }
         });
         if (!existingViolation) {
-            await message.reply(`user has no existing active ${command}s in the DB!`);
+            await message.reply({ content: `user has no existing active ${command}s in the DB!`, failIfNotExists: false });
             return;
         }
 
@@ -260,7 +260,7 @@ export class ViolationPlugin {
 
         const handler = this.getViolationHandler(type);
         await handler.stop(message.guild, user.id, settings, existingViolation);
-        await message.reply(`removed ${command} on user!`);
+        await message.reply({ content: `removed ${command} on user!`, failIfNotExists: false });
     }
 
     private async applyTimedViolation<T extends TimedViolation>(type: ObjectType<T>, info: ViolationInfo, command = "violation", apply: StartViolationFunction, remove: StopViolationFunction, modify?: ModifyViolationFunction) {
@@ -355,14 +355,14 @@ export class ViolationPlugin {
 
     private async parseCommand(message: Message, command = "violation"): Promise<Option<ViolationInfo>> {
         if (!message.guild) {
-            await message.reply("cannot do in DMs!");
+            await message.reply({ content: "cannot do in DMs!", failIfNotExists: false });
             return { ok: false };
         }
         const violationSettingsRepo = getRepository(GuildViolationSettings);
         const settings = await violationSettingsRepo.findOne(message.guild.id);
 
         if (!settings) {
-            await message.reply("sorry, this server doesn't have violation settings set up.");
+            await message.reply({ content: "sorry, this server doesn't have violation settings set up.", failIfNotExists: false });
             logger.error(
                 "%s was called in guild %s (%s) on user %s which doesn't have config set up!",
                 command,
@@ -378,37 +378,37 @@ export class ViolationPlugin {
         const noAnnounce = directive.endsWith("!");
 
         if (!userId) {
-            await message.reply("no user specified!");
+            await message.reply({ content: "no user specified!", failIfNotExists: false });
             return { ok: false };
         }
 
         const user = await this.resolveUser(userId);
 
         if (!user) {
-            await message.reply("couldn't find the given user!");
+            await message.reply({ content: "couldn't find the given user!", failIfNotExists: false });
             logger.error("Tried to %s user %s but couldn't find them by id!", command, userId);
             return { ok: false };
         }
 
         if (user.id == message.author.id) {
-            await message.reply(`cannot ${command} yourself!`);
+            await message.reply({ content: `cannot ${command} yourself!`, failIfNotExists: false });
             return { ok: false };
         }
 
         if (user.id == client.botUser.id) {
-            await message.reply(`cannot apply ${command} on me!`);
+            await message.reply({ content: `cannot apply ${command} on me!`, failIfNotExists: false });
             return { ok: false };
         }
 
         const memberResolve = await tryDo(message.guild.members.fetch(user));
         if (!memberResolve.ok) {
-            await message.reply("user is not member of the server anymore!");
+            await message.reply({ content: "user is not member of the server anymore!", failIfNotExists: false });
             logger.error("Tried to %s user %s but they are not on the server anymore!", command, userId);
             return { ok: false };
         }
 
         if (await isAuthorisedAsync(memberResolve.result)) {
-            await message.reply(`cannot apply ${command} on another moderator!`);
+            await message.reply({ content: `cannot apply ${command} on another moderator!`, failIfNotExists: false });
             return { ok: false };
         }
 
@@ -441,42 +441,46 @@ export class ViolationPlugin {
 
     private async sendViolationMessage(message: Message, info: ViolationInfo, title: string) {
         let announceChannel: TextChannel | null = null;
-        if ((info.noAnnounce || info.dryRun) && message.channel.type == "text") {
+        if ((info.noAnnounce || info.dryRun) && message.channel.type == "GUILD_TEXT") {
             announceChannel = message.channel;
         }
         else if (info.settings.violationInfoChannelId) {
             const ch = info.guild.channels.resolve(info.settings.violationInfoChannelId);
-            if (ch && ch.type == "text")
+            if (ch && ch.type == "GUILD_TEXT")
                 announceChannel = ch as TextChannel;
-            else if (message.channel.type == "text") {
+            else if (message.channel.type == "GUILD_TEXT") {
                 announceChannel = message.channel;
             }
         }
-        await announceChannel?.send(new MessageEmbed({
-            title: `${info.dryRun ? "[DRY RUN] " : ""}${title}`,
-            color: 4944347,
-            timestamp: new Date(),
-            footer: {
-                text: client.botUser.username
-            },
-            author: {
-                name: client.botUser.username,
-                iconURL: client.botUser.avatarURL() ?? undefined
-            },
-            fields: [
-                {
-                    name: "Username",
-                    value: info.member.toString()
-                },
-                {
-                    name: "Duration",
-                    value: humanizeDuration(info.duration, { unitMeasures: UNIT_MEASURES })
-                },
-                {
-                    name: "Reason",
-                    value: info.reason
-                }
+        await announceChannel?.send({
+            embeds: [
+                new MessageEmbed({
+                    title: `${info.dryRun ? "[DRY RUN] " : ""}${title}`,
+                    color: 4944347,
+                    timestamp: new Date(),
+                    footer: {
+                        text: client.botUser.username
+                    },
+                    author: {
+                        name: client.botUser.username,
+                        iconURL: client.botUser.avatarURL() ?? undefined
+                    },
+                    fields: [
+                        {
+                            name: "Username",
+                            value: info.member.toString()
+                        },
+                        {
+                            name: "Duration",
+                            value: humanizeDuration(info.duration, { unitMeasures: UNIT_MEASURES })
+                        },
+                        {
+                            name: "Reason",
+                            value: info.reason
+                        }
+                    ]
+                })
             ]
-        }));
+        });
     }
 }

+ 1 - 1
bot/src/util.ts

@@ -60,7 +60,7 @@ export async function isAuthorisedAsync(member: GuildMember | User | null | unde
         const role = await repo.findOne({
             select: ["userID"],
             where: {
-                userID: In(member.roles.cache.keyArray()),
+                userID: In([...member.roles.cache.keys()]),
                 canModerate: true
             }
         });

+ 1 - 1
docker-compose.yml

@@ -73,7 +73,7 @@ services:
       - db-data:/var/lib/postgresql/data
 
   adminer:
-    image: adminer:4.8.0
+    image: adminer:latest
     restart: unless-stopped
     ports:
       - 3030:8080

File diff suppressed because it is too large
+ 1327 - 55
shared/package-lock.json


+ 5 - 5
shared/package.json

@@ -10,16 +10,16 @@
   "author": "ghorsington",
   "license": "ISC",
   "dependencies": {
-    "@types/node": "^14.14.41",
-    "nodemailer": "^6.5.0",
+    "@types/node": "^16.4.13",
+    "nodemailer": "^6.6.3",
     "rimraf": "^3.0.2",
-    "typeorm": "^0.2.32",
-    "typescript": "^4.2.4",
+    "typeorm": "^0.2.36",
+    "typescript": "^4.3.5",
     "winston": "^3.3.3",
     "winston-mongodb": "^5.0.7",
     "winston-transport": "^4.4.0"
   },
   "devDependencies": {
-    "@types/nodemailer": "^6.4.1"
+    "@types/nodemailer": "^6.4.4"
   }
 }

+ 2 - 2
web/Dockerfile

@@ -1,6 +1,6 @@
-FROM node:14-alpine
+FROM node:16-alpine
 
-RUN apk --no-cache add make
+RUN apk --no-cache add make libc6-compat
 
 WORKDIR /app
 

File diff suppressed because it is too large
+ 15955 - 1520
web/package-lock.json


+ 36 - 36
web/package.json

@@ -14,73 +14,73 @@
 		"@improbable-eng/grpc-web-node-http-transport": "^0.13.0",
 		"compression": "^1.7.4",
 		"cookie-session": "^1.4.0",
-		"dotenv": "^8.2.0",
+		"dotenv": "^8.6.0",
 		"easymde": "^2.15.0",
 		"express": "^4.17.1",
-		"google-protobuf": "^3.15.8",
+		"google-protobuf": "^3.17.3",
 		"got": "^11.8.2",
 		"make-error": "^1.3.6",
 		"node-fetch": "^2.6.1",
-		"nodemailer": "^6.5.0",
-		"pg": "^8.6.0",
-		"postcss-nested": "^5.0.5",
+		"nodemailer": "^6.6.3",
+		"pg": "^8.7.1",
+		"postcss-nested": "^5.0.6",
 		"rpc_ts": "^2.1.0",
 		"showdown": "^1.9.1",
-		"sirv": "^1.0.11",
-		"svelte-awesome": "^2.3.1",
-		"typeorm": "^0.2.32",
+		"sirv": "^1.0.12",
+		"svelte-awesome": "^2.3.2",
+		"typeorm": "^0.2.36",
 		"vanilla-hcaptcha": "0.0.5",
 		"winston": "^3.3.3",
 		"winston-mongodb": "^5.0.7",
 		"winston-transport": "^4.4.0"
 	},
 	"devDependencies": {
-		"@babel/core": "^7.13.16",
+		"@babel/core": "^7.15.0",
 		"@babel/plugin-syntax-dynamic-import": "^7.8.3",
-		"@babel/plugin-transform-async-to-generator": "^7.13.0",
-		"@babel/plugin-transform-runtime": "^7.13.15",
-		"@babel/preset-env": "^7.13.15",
-		"@babel/runtime": "^7.13.17",
-		"@fortawesome/free-brands-svg-icons": "^5.15.3",
-		"@fortawesome/free-solid-svg-icons": "^5.15.3",
-		"@rollup/plugin-alias": "^3.1.2",
+		"@babel/plugin-transform-async-to-generator": "^7.14.5",
+		"@babel/plugin-transform-runtime": "^7.15.0",
+		"@babel/preset-env": "^7.15.0",
+		"@babel/runtime": "^7.14.8",
+		"@fortawesome/free-brands-svg-icons": "^5.15.4",
+		"@fortawesome/free-solid-svg-icons": "^5.15.4",
+		"@rollup/plugin-alias": "^3.1.5",
 		"@rollup/plugin-babel": "^5.3.0",
 		"@rollup/plugin-commonjs": "^16.0.0",
 		"@rollup/plugin-json": "^4.1.0",
 		"@rollup/plugin-node-resolve": "^10.0.0",
 		"@rollup/plugin-replace": "^2.4.2",
 		"@rollup/plugin-typescript": "^6.1.0",
-		"@types/compression": "^1.7.0",
-		"@types/cookie-session": "^2.0.42",
-		"@types/express": "^4.17.11",
-		"@types/node-fetch": "^2.5.10",
-		"@types/nodemailer": "^6.4.1",
-		"@types/showdown": "^1.9.3",
-		"@typescript-eslint/eslint-plugin": "^4.22.0",
-		"@typescript-eslint/parser": "^4.22.0",
-		"autoprefixer": "^10.2.5",
+		"@types/compression": "^1.7.1",
+		"@types/cookie-session": "^2.0.43",
+		"@types/express": "^4.17.13",
+		"@types/node-fetch": "^2.5.12",
+		"@types/nodemailer": "^6.4.4",
+		"@types/showdown": "^1.9.4",
+		"@typescript-eslint/eslint-plugin": "^4.29.0",
+		"@typescript-eslint/parser": "^4.29.0",
+		"autoprefixer": "^10.3.1",
 		"bufferutil": "^4.0.3",
 		"class-validator": "^0.12.2",
 		"cross-env": "^7.0.3",
-		"eslint": "^7.25.0",
+		"eslint": "^7.32.0",
 		"eslint-config-airbnb-base": "^14.2.1",
 		"eslint-import-resolver-typescript": "^2.4.0",
-		"eslint-plugin-import": "^2.22.1",
+		"eslint-plugin-import": "^2.23.4",
 		"eslint-plugin-svelte3": "^2.7.3",
-		"postcss": "^8.2.12",
+		"postcss": "^8.3.6",
 		"postcss-import": "^13.0.0",
-		"postcss-load-config": "^3.0.1",
+		"postcss-load-config": "^3.1.0",
 		"postcss-preset-env": "^6.7.0",
 		"reflect-metadata": "^0.1.13",
-		"rollup": "^2.45.2",
+		"rollup": "^2.56.0",
 		"rollup-plugin-svelte": "^7.1.0",
 		"rollup-plugin-terser": "^7.0.2",
-		"sapper": "^0.29.1",
+		"sapper": "^0.29.2",
 		"svelte": "~3.37.0",
-		"svelte-preprocess": "^4.7.2",
-		"tailwindcss": "^2.1.2",
-		"tslib": "^2.2.0",
-		"typescript": "^4.2.4",
-		"utf-8-validate": "^5.0.4"
+		"svelte-preprocess": "^4.7.4",
+		"tailwindcss": "^2.2.7",
+		"tslib": "^2.3.0",
+		"typescript": "^4.3.5",
+		"utf-8-validate": "^5.0.5"
 	}
 }