Explorar el Código

Add ESLint, fix code style

ghorsington hace 4 años
padre
commit
00a1490a2c

+ 5 - 0
.vscode/settings.json

@@ -0,0 +1,5 @@
+{
+    "editor.codeActionsOnSave": {
+        "source.fixAll.eslint": true
+    }
+}

+ 0 - 30
bot/.eslintrc.js

@@ -1,30 +0,0 @@
-module.exports = {
-    "env": {
-        "browser": true,
-        "commonjs": true,
-        "es6": true
-    },
-    "extends": "eslint:recommended",
-    "parserOptions": {
-        "sourceType": "module",
-        "ecmaVersion": 2017
-    },
-    "rules": {
-        "indent": [
-            "error",
-            4
-        ],
-        "linebreak-style": [
-            "error",
-            "windows"
-        ],
-        "quotes": [
-            "error",
-            "double"
-        ],
-        "semi": [
-            "error",
-            "always"
-        ]
-    }
-};

+ 41 - 0
bot/.eslintrc.json

@@ -0,0 +1,41 @@
+{
+    "env": {
+        "es6": true,
+        "node": true
+    },
+    "extends": [
+        "eslint:recommended",
+        "plugin:@typescript-eslint/eslint-recommended",
+        "plugin:@typescript-eslint/recommended"
+    ],
+    "globals": {
+        "Atomics": "readonly",
+        "SharedArrayBuffer": "readonly"
+    },
+    "parser": "@typescript-eslint/parser",
+    "parserOptions": {
+        "ecmaVersion": 11,
+        "sourceType": "module"
+    },
+    "plugins": [
+        "@typescript-eslint"
+    ],
+    "rules": {
+        "indent": [
+            "error",
+            4
+        ],
+        "linebreak-style": [
+            "error",
+            "windows"
+        ],
+        "quotes": [
+            "error",
+            "double"
+        ],
+        "semi": [
+            "error",
+            "always"
+        ]
+    }
+}

+ 133 - 128
bot/src/bbcode-parser/bbcode-js.ts

@@ -1,6 +1,6 @@
 import { Dict } from "src/util";
 
-var VERSION = '0.4.0';
+const VERSION = "0.4.0";
 
 export interface BBCodeConfig {
     showQuotePrefix?: boolean;
@@ -11,17 +11,17 @@ export interface BBCodeConfig {
 // default options
 const defaults: BBCodeConfig = {
     showQuotePrefix: true,
-    classPrefix: 'bbcode_',
-    mentionPrefix: '@'
+    classPrefix: "bbcode_",
+    mentionPrefix: "@"
 };
 
-export var version = VERSION;
+export const version = VERSION;
 
 // copied from here:
 // http://blog.mattheworiordan.com/post/13174566389/url-regular-expression-for-links-with-or-without-the had to make an
 // update to allow / in the query string, since some sites will have a / there made another update to support colons in
 // the query string made another update to disallow an ending dot(.)
-var URL_PATTERN = new RegExp("(" // overall match
+const URL_PATTERN = new RegExp("(" // overall match
     + "(" // brackets covering match for protocol (optional) and domain
     + "([A-Za-z]{3,9}:(?:\\/\\/)?)" // allow something@ for email addresses
     + "(?:[\\-;:&=\\+\\$,\\w]+@)?[A-Za-z0-9\\.\\-]+[A-Za-z0-9\\-]"
@@ -38,13 +38,13 @@ var URL_PATTERN = new RegExp("(" // overall match
     + ")");
 
 function doReplace(content: string, matches: Replacement[], options: BBCodeConfig) {
-    var i, obj, regex, hasMatch, tmp;
+    let i, obj, regex, hasMatch, tmp;
     // match/replace until we don't change the input anymore
     do {
         hasMatch = false;
         for (i = 0; i < matches.length; ++i) {
             obj = matches[i];
-            regex = new RegExp(obj.e, 'gi');
+            regex = new RegExp(obj.e, "gi");
             tmp = content.replace(regex, obj.func.bind(undefined, options));
             if (tmp !== content) {
                 content = tmp;
@@ -56,11 +56,12 @@ function doReplace(content: string, matches: Replacement[], options: BBCodeConfi
 }
 
 function listItemReplace(options: BBCodeConfig, fullMatch: string, tag: string, value: string) {
-    return '<li>' + doReplace(value.trim(), BBCODE_PATTERN, options) + '</li>';
+    return "<li>" + doReplace(value.trim(), BBCODE_PATTERN, options) + "</li>";
 }
 
-export var extractQuotedText = function (value: string, parts?: string[]) {
-    var quotes = ["\"", "'"], i, quote, nextPart;
+export function extractQuotedText(value: string, parts?: string[]): (string | string[] | undefined)[] {
+    const quotes = ["\"", "'"];
+    let i, quote, nextPart;
 
     for (i = 0; i < quotes.length; ++i) {
         quote = quotes[i];
@@ -69,21 +70,23 @@ export var extractQuotedText = function (value: string, parts?: string[]) {
             if (value[value.length - 1] !== quote) {
                 while (parts && parts.length) {
                     nextPart = parts.shift();
+                    if (!nextPart)
+                        continue;
                     value += " " + nextPart;
-                    if (nextPart![nextPart!.length - 1] === quote) {
+                    if (nextPart[nextPart.length - 1] === quote) {
                         break;
                     }
                 }
             }
-            value = value.replace(new RegExp("[" + quote + "]+$"), '');
+            value = value.replace(new RegExp("[" + quote + "]+$"), "");
             break;
         }
     }
     return [value, parts];
-};
+}
 
-export var parseParams = function (tagName: string, params?: string) {
-    let paramMap: Dict<string> = {};
+export function parseParams(tagName: string, params?: string): Dict<string> {
+    const paramMap: Dict<string> = {};
 
     if (!params) {
         return paramMap;
@@ -94,147 +97,149 @@ export var parseParams = function (tagName: string, params?: string) {
     let parts = params.split(/\s+/);
 
     while (parts.length) {
-        let part = parts.shift() ?? "";
+        const part = parts.shift() ?? "";
         // check if the param itself is a valid url
         if (!URL_PATTERN.exec(part)) {
-            let index = part.indexOf('=');
+            const index = part.indexOf("=");
             if (index > 0) {
-                let rv = extractQuotedText(part.slice(index + 1), parts);
+                const rv = extractQuotedText(part.slice(index + 1), parts);
                 paramMap[part.slice(0, index).toLowerCase()] = rv[0] as string;
                 parts = rv[1] as string[];
             }
             else {
-                let rv = extractQuotedText(part, parts);
+                const rv = extractQuotedText(part, parts);
                 paramMap[tagName] = rv[0] as string;
                 parts = rv[1] as string[];
             }
         } else {
-            let rv = extractQuotedText(part, parts);
+            const rv = extractQuotedText(part, parts);
             paramMap[tagName] = rv[0] as string;
             parts = rv[1] as string[];
         }
     }
     return paramMap;
-};
+}
 
-const BBCODE_PATTERN = [{ e: '\\[(\\w+)(?:[= ]([^\\]]+))?]((?:.|[\r\n])*?)\\[/\\1]', func: tagReplace }];
+const BBCODE_PATTERN = [{ e: "\\[(\\w+)(?:[= ]([^\\]]+))?]((?:.|[\r\n])*?)\\[/\\1]", func: tagReplace }];
 
 function tagReplace(options: BBCodeConfig, fullMatch: string, tag: string, params: string | undefined, value: string) {
     let val: string;
     tag = tag.toLowerCase();
-    let paramsObj = parseParams(tag, params || undefined);
+    const paramsObj = parseParams(tag, params || undefined);
     let inlineValue = paramsObj[tag];
 
     switch (tag) {
-        case 'attach':
-            return '';
-        case 'spoiler':
-            return '';
-        case 'center':
-            return doReplace(value, BBCODE_PATTERN, options);
-        case 'size':
-            return doReplace(value, BBCODE_PATTERN, options);
-        case 'quote':
-            val = '<div class="' + options.classPrefix + 'quote"';
-            for (let i in paramsObj) {
-                let tmp = paramsObj[i];
-                if (!inlineValue && (i === 'author' || i === 'name')) {
-                    inlineValue = tmp;
-                } else if (i !== tag) {
-                    val += ' data-' + i + '="' + tmp + '"';
-                }
+    case "attach":
+        return "";
+    case "spoiler":
+        return "";
+    case "center":
+        return doReplace(value, BBCODE_PATTERN, options);
+    case "size":
+        return doReplace(value, BBCODE_PATTERN, options);
+    case "quote":
+        val = "<div class=\"" + options.classPrefix + "quote\"";
+        for (const i in paramsObj) {
+            const tmp = paramsObj[i];
+            if (!inlineValue && (i === "author" || i === "name")) {
+                inlineValue = tmp;
+            } else if (i !== tag) {
+                val += " data-" + i + "=\"" + tmp + "\"";
             }
-            return val + '>' + (inlineValue ? inlineValue + ' wrote:' : (options.showQuotePrefix ? 'Quote:' : '')) + '<blockquote>' + value + '</blockquote></div>';
-        case 'url':
-            return '<a class="' + options.classPrefix + 'link" target="_blank" href="' + (inlineValue || value) + '">' + value + '</a>';
-        case 'email':
-            return '<a class="' + options.classPrefix + 'link" target="_blank" href="mailto:' + (inlineValue || value) + '">' + value + '</a>';
-        case 'anchor':
-            return '<a name="' + (inlineValue || paramsObj.a || value) + '">' + value + '</a>';
-        case 'b':
-            return '<strong>' + value + '</strong>';
-        case 'i':
-            return '<em>' + value + '</em>';
-        case 'u':
-            return '<span style="text-decoration:underline">' + value + '</span>';
-        case 's':
-            return '<span style="text-decoration:line-through">' + value + '</span>';
-        case 'indent':
-            return '<blockquote>' + value + '</blockquote>';
-        case 'list':
-            tag = 'ul';
-            let className = options.classPrefix + 'list';
-            if (inlineValue && /[1Aa]/.test(inlineValue)) {
-                tag = 'ol';
-                if (/1/.test(inlineValue)) {
-                    className += '_numeric';
-                }
-                else if (/A/.test(inlineValue)) {
-                    className += '_alpha';
-                }
-                else if (/a/.test(inlineValue)) {
-                    className += '_alpha_lower';
-                }
-            }
-            val = '<' + tag + ' class="' + className + '">';
-            //  parse the value
-            val += doReplace(value, [{ e: '\\[([*])\\]([^\r\n]+)', func: listItemReplace }], options);
-            return val + '</' + tag + '>';
-        case 'code':
-        case 'php':
-        case 'java':
-        case 'javascript':
-        case 'cpp':
-        case 'ruby':
-        case 'python':
-            return '<pre class="' + options.classPrefix + (tag === 'code' ? '' : 'code_') + tag + '">' + value + '</pre>';
-        case 'highlight':
-            return '<span class="' + options.classPrefix + tag + '">' + value + '</span>';
-        case 'html':
-            return value;
-        case 'mention':
-            val = '<span class="' + options.classPrefix + 'mention"';
-            if (inlineValue) {
-                val += ' data-mention-id="' + inlineValue + '"';
+        }
+        return val + ">" + (inlineValue ? inlineValue + " wrote:" : (options.showQuotePrefix ? "Quote:" : "")) + "<blockquote>" + value + "</blockquote></div>";
+    case "url":
+        return "<a class=\"" + options.classPrefix + "link\" target=\"_blank\" href=\"" + (inlineValue || value) + "\">" + value + "</a>";
+    case "email":
+        return "<a class=\"" + options.classPrefix + "link\" target=\"_blank\" href=\"mailto:" + (inlineValue || value) + "\">" + value + "</a>";
+    case "anchor":
+        return "<a name=\"" + (inlineValue || paramsObj.a || value) + "\">" + value + "</a>";
+    case "b":
+        return "<strong>" + value + "</strong>";
+    case "i":
+        return "<em>" + value + "</em>";
+    case "u":
+        return "<span style=\"text-decoration:underline\">" + value + "</span>";
+    case "s":
+        return "<span style=\"text-decoration:line-through\">" + value + "</span>";
+    case "indent":
+        return "<blockquote>" + value + "</blockquote>";
+    case "list": {
+        tag = "ul";
+        let className = options.classPrefix + "list";
+        if (inlineValue && /[1Aa]/.test(inlineValue)) {
+            tag = "ol";
+            if (/1/.test(inlineValue)) {
+                className += "_numeric";
             }
-            return val + '>' + (options.mentionPrefix || '') + value + '</span>';
-        case 'span':
-        case 'h1':
-        case 'h2':
-        case 'h3':
-        case 'h4':
-        case 'h5':
-        case 'h6':
-            return '<' + tag + '>' + value + '</' + tag + '>';
-        case 'youtube':
-            return '<object class="' + options.classPrefix + 'video" width="425" height="350"><param name="movie" value="http://www.youtube.com/v/' + value + '"></param><embed src="http://www.youtube.com/v/' + value + '" type="application/x-shockwave-flash" width="425" height="350"></embed></object>';
-        case 'gvideo':
-            return '<embed class="' + options.classPrefix + 'video" style="width:400px; height:325px;" id="VideoPlayback" type="application/x-shockwave-flash" src="http://video.google.com/googleplayer.swf?docId=' + value + '&amp;hl=en">';
-        case 'google':
-            return '<a class="' + options.classPrefix + 'link" target="_blank" href="http://www.google.com/search?q=' + (inlineValue || value) + '">' + value + '</a>';
-        case 'wikipedia':
-            return '<a class="' + options.classPrefix + 'link" target="_blank" href="http://www.wikipedia.org/wiki/' + (inlineValue || value) + '">' + value + '</a>';
-        case 'img':
-            var dims = new RegExp('^(\\d+)x(\\d+)$').exec(inlineValue || '');
-            if (!dims || (dims.length !== 3)) {
-                dims = new RegExp('^width=(\\d+)\\s+height=(\\d+)$').exec(inlineValue || '');
+            else if (/A/.test(inlineValue)) {
+                className += "_alpha";
             }
-            if (dims && dims.length === 3) {
-                params = undefined;
+            else if (/a/.test(inlineValue)) {
+                className += "_alpha_lower";
             }
-            val = '<img class="' + options.classPrefix + 'image" src="' + value + '"';
-            if (dims && dims.length === 3) {
-                val += ' width="' + dims[1] + '" height="' + dims[2] + '"';
-            } else {
-                for (let i in paramsObj) {
-                    let tmp = paramsObj[i];
-                    if (i === 'img') {
-                        i = 'alt';
-                    }
-                    val += ' ' + i + '="' + tmp + '"';
+        }
+        val = "<" + tag + " class=\"" + className + "\">";
+        //  parse the value
+        val += doReplace(value, [{ e: "\\[([*])\\]([^\r\n]+)", func: listItemReplace }], options);
+        return val + "</" + tag + ">"; 
+    }
+    case "code":
+    case "php":
+    case "java":
+    case "javascript":
+    case "cpp":
+    case "ruby":
+    case "python":
+        return "<pre class=\"" + options.classPrefix + (tag === "code" ? "" : "code_") + tag + "\">" + value + "</pre>";
+    case "highlight":
+        return "<span class=\"" + options.classPrefix + tag + "\">" + value + "</span>";
+    case "html":
+        return value;
+    case "mention":
+        val = "<span class=\"" + options.classPrefix + "mention\"";
+        if (inlineValue) {
+            val += " data-mention-id=\"" + inlineValue + "\"";
+        }
+        return val + ">" + (options.mentionPrefix || "") + value + "</span>";
+    case "span":
+    case "h1":
+    case "h2":
+    case "h3":
+    case "h4":
+    case "h5":
+    case "h6":
+        return "<" + tag + ">" + value + "</" + tag + ">";
+    case "youtube":
+        return "<object class=\"" + options.classPrefix + "video\" width=\"425\" height=\"350\"><param name=\"movie\" value=\"http://www.youtube.com/v/" + value + "\"></param><embed src=\"http://www.youtube.com/v/" + value + "\" type=\"application/x-shockwave-flash\" width=\"425\" height=\"350\"></embed></object>";
+    case "gvideo":
+        return "<embed class=\"" + options.classPrefix + "video\" style=\"width:400px; height:325px;\" id=\"VideoPlayback\" type=\"application/x-shockwave-flash\" src=\"http://video.google.com/googleplayer.swf?docId=" + value + "&amp;hl=en\">";
+    case "google":
+        return "<a class=\"" + options.classPrefix + "link\" target=\"_blank\" href=\"http://www.google.com/search?q=" + (inlineValue || value) + "\">" + value + "</a>";
+    case "wikipedia":
+        return "<a class=\"" + options.classPrefix + "link\" target=\"_blank\" href=\"http://www.wikipedia.org/wiki/" + (inlineValue || value) + "\">" + value + "</a>";
+    case "img": {
+        let dims = new RegExp("^(\\d+)x(\\d+)$").exec(inlineValue || "");
+        if (!dims || (dims.length !== 3)) {
+            dims = new RegExp("^width=(\\d+)\\s+height=(\\d+)$").exec(inlineValue || "");
+        }
+        if (dims && dims.length === 3) {
+            params = undefined;
+        }
+        val = "<img class=\"" + options.classPrefix + "image\" src=\"" + value + "\"";
+        if (dims && dims.length === 3) {
+            val += " width=\"" + dims[1] + "\" height=\"" + dims[2] + "\"";
+        } else {
+            for (let i in paramsObj) {
+                const tmp = paramsObj[i];
+                if (i === "img") {
+                    i = "alt";
                 }
+                val += " " + i + "=\"" + tmp + "\"";
             }
-            return val + '/>';
+        }
+        return val + "/>";
+    }
     }
     // return the original
     return fullMatch;
@@ -251,7 +256,7 @@ interface Replacement {
  * @param options   optional object with control parameters
  * @returns rendered html
  */
-export var render = function (content: string, options?: BBCodeConfig) {
+export function render(content: string, options?: BBCodeConfig): string {
     options = options || {};
 
     if (!options.classPrefix)
@@ -263,4 +268,4 @@ export var render = function (content: string, options?: BBCodeConfig) {
 
     // for now, only one rule
     return doReplace(content, BBCODE_PATTERN, options);
-};
+}

+ 3 - 1
bot/src/client.ts

@@ -8,7 +8,9 @@ export class BotClient {
     public forum = new XenforoClient(`${FORUMS_DOMAIN}/api`, process.env.FORUM_API_KEY ?? "");
 
     get botUser(): ClientUser {
-        return this.bot.user!;
+        if (!this.bot.user)
+            throw new Error("No bot user detected!");
+        return this.bot.user;
     }
 }
 

+ 8 - 8
bot/src/commands/aggregators/com3d2_updates.ts

@@ -10,14 +10,14 @@ const changeLogPattern = /\[\s*([^\s\]]+)\s*\]\s*((・.*)\s+)+/gim;
 const FEED_NAME = "com3d2-jp-updates";
 
 function getVersionNumber(verStr: string) {
-    let verPart = verStr.replace(/[\.\s]/g, "");
+    let verPart = verStr.replace(/[.\s]/g, "");
     if(verPart.length < 4)
         verPart += "0";
     return +verPart;
 }
 
 async function aggregate() {
-    let repo = getRepository(AggroNewsItem);
+    const repo = getRepository(AggroNewsItem);
     
     let lastPost = await repo.findOne({
         select: [ "newsId" ],
@@ -31,27 +31,27 @@ async function aggregate() {
         });
     
     try {
-        let mainPageRes = await request(updatePage, {resolveWithFullResponse: true}) as Response;
+        const mainPageRes = await request(updatePage, {resolveWithFullResponse: true}) as Response;
 
         if(mainPageRes.statusCode != 200)
             return;
         
-        let rootNode = cheerio.load(mainPageRes.body);
+        const rootNode = cheerio.load(mainPageRes.body);
 
-        let readme = rootNode("div.readme");
+        const readme = rootNode("div.readme");
 
         if(!readme) {
             console.log("[COM3D2 JP UPDATE] Failed to find listing!");
             return [];
         }
 
-        let latestVersionChangelog = changeLogPattern.exec(readme.text());
+        const latestVersionChangelog = changeLogPattern.exec(readme.text());
 
         if(!latestVersionChangelog)
             return [];
 
-        let version = getVersionNumber(latestVersionChangelog[1]);
-        let text = latestVersionChangelog[0];
+        const version = getVersionNumber(latestVersionChangelog[1]);
+        const text = latestVersionChangelog[0];
 
         if(version <= lastPost.newsId)
             return [];

+ 12 - 12
bot/src/commands/aggregators/com3d2_world.ts

@@ -9,7 +9,7 @@ const kissDiaryRoot = "https://com3d2.world/r18/notices.php";
 const FEED_NAME = "com3d2-world-notices";
 
 async function aggregate() {
-    let repo = getRepository(AggroNewsItem);
+    const repo = getRepository(AggroNewsItem);
     
     let lastPost = await repo.findOne({
         select: [ "newsId" ],
@@ -23,27 +23,27 @@ async function aggregate() {
         });
 
     try {
-        let mainPageRes = await request(kissDiaryRoot, {resolveWithFullResponse: true}) as Response;
+        const mainPageRes = await request(kissDiaryRoot, {resolveWithFullResponse: true}) as Response;
         
         if(mainPageRes.statusCode != 200)
             return [];
 
-        let rootNode = cheerio.load(mainPageRes.body);
+        const rootNode = cheerio.load(mainPageRes.body);
 
-        let diaryEntries = rootNode("div.frame a");
+        const diaryEntries = rootNode("div.frame a");
 
         if(!diaryEntries) {
             console.log("[COM3D2 WORLD BLOG] Failed to find listing!");
         }
 
-        let result : INewsItem[] = [];
+        const result : INewsItem[] = [];
         let latestEntry = lastPost.newsId;
 
-        for(let a of diaryEntries.get() as CheerioElement[]) {
+        for(const a of diaryEntries.get() as CheerioElement[]) {
             if(!a.attribs.id)
                 continue;
             
-            let id = +a.attribs.id;
+            const id = +a.attribs.id;
 
             if(id <= lastPost.newsId)
                 continue;
@@ -51,15 +51,15 @@ async function aggregate() {
             if(id > latestEntry)
                 latestEntry = id;
 
-            let diaryLink = `${kissDiaryRoot}?no=${id}`;
-            let res = await request(diaryLink, {resolveWithFullResponse: true}) as Response;
+            const diaryLink = `${kissDiaryRoot}?no=${id}`;
+            const res = await request(diaryLink, {resolveWithFullResponse: true}) as Response;
             if(res.statusCode != 200)
                 continue;
 
-            let node = cheerio.load(res.body);
+            const node = cheerio.load(res.body);
 
-            let title = node("div.frame div.notice_title th");
-            let contents = node("div.frame div").get(1);
+            const title = node("div.frame div.notice_title th");
+            const contents = node("div.frame div").get(1);
 
             result.push({
                 newsId: id,

+ 16 - 16
bot/src/commands/aggregators/kiss_diary.ts

@@ -10,7 +10,7 @@ const kissDiaryRoot = "http://www.kisskiss.tv/kiss";
 const FEED_NAME = "kisskisstv-diary";
 
 async function aggregate() {
-    let repo = getRepository(AggroNewsItem);
+    const repo = getRepository(AggroNewsItem);
 
     let lastPost = await repo.findOne({
         select: [ "newsId" ],
@@ -24,42 +24,42 @@ async function aggregate() {
         });
     
     try {
-        let mainPageRes = await request(`${kissDiaryRoot}/diary.php`, {resolveWithFullResponse: true}) as Response;
+        const mainPageRes = await request(`${kissDiaryRoot}/diary.php`, {resolveWithFullResponse: true}) as Response;
         
         if(mainPageRes.statusCode != 200)
             return [];
 
-        let rootNode = cheerio.load(mainPageRes.body);
-        let diaryEntryNames = rootNode("table.blog_frame_top");
+        const rootNode = cheerio.load(mainPageRes.body);
+        const diaryEntryNames = rootNode("table.blog_frame_top");
         
         if(diaryEntryNames.length == 0) {
             console.log("[KISS DIARY] Failed to find listing!");
         }
 
-        let diaryTexts = rootNode("div.blog_frame_middle");
-        let items = diaryEntryNames.map((i, e) => ({ table: e, content: diaryTexts.get(i) }));
-        let result : INewsItem[] = [];
+        const diaryTexts = rootNode("div.blog_frame_middle");
+        const items = diaryEntryNames.map((i, e) => ({ table: e, content: diaryTexts.get(i) }));
+        const result : INewsItem[] = [];
         let latestEntry = lastPost.newsId;
 
-        for(let {table, content} of items.get() as {table: CheerioElement, content: CheerioElement}[]) {
-            let a = cheerio(table).find("a");
-            let link = a.attr("href");
-            let matches = link ? urlPattern.exec(link) : false;
+        for(const {table, content} of items.get() as {table: CheerioElement, content: CheerioElement}[]) {
+            const a = cheerio(table).find("a");
+            const link = a.attr("href");
+            const matches = link ? urlPattern.exec(link) : false;
             if(!matches)
                 continue;
             
-            let id = +matches[1];
+            const id = +matches[1];
             if(id <= lastPost.newsId)
                 continue;
 
             if(id > latestEntry)
                 latestEntry = id;
 
-            let diaryLink = `${kissDiaryRoot}/${link}`;
-            let contentCh = cheerio(content);
-            let title = a.text();
+            const diaryLink = `${kissDiaryRoot}/${link}`;
+            const contentCh = cheerio(content);
+            const title = a.text();
             
-            let bottomFrame = contentCh.find("div.blog_data");
+            const bottomFrame = contentCh.find("div.blog_data");
             bottomFrame.remove();
 
             result.push({

+ 0 - 27
bot/src/commands/command.ts.b

@@ -1,27 +0,0 @@
-import { Message } from "discord.js";
-
-export type BotEvent = (actionsDone: boolean, ...params: any[]) => boolean | Promise<boolean>;
-
-export interface IDocumentation {
-    auth: boolean;
-    description: string;
-};
-
-export type DocumentationSet = {
-    [command: string] : IDocumentation;
-};
-
-export interface IBotCommand {
-    pattern: string | RegExp;
-    action(message: Message, strippedContents: string, matches?: RegExpMatchArray) : void;
-};
-
-export interface ICommand {
-    commands?: Array<IBotCommand>;
-    documentation?: DocumentationSet;
-    onMessage?(actionsDone: boolean, m : Message, content: string) : boolean | Promise<boolean>;
-    onIndirectMention?(actionsDone: boolean, m: Message) : boolean | Promise<boolean>;
-    onDirectMention?(actionsDone: boolean, m: Message, content: string) : boolean | Promise<boolean>;
-    postMessage?(actionsDone: boolean, m: Message) : boolean | Promise<boolean>;
-    onStart?(): void | Promise<void>;
-};

+ 0 - 487
bot/src/commands/contest.ts.bak

@@ -1,487 +0,0 @@
-import { Dict, compareNumbers, isAuthorisedAsync } from "../util";
-import { Message, TextChannel, RichEmbed, MessageReaction, User, Collector, Collection, SnowflakeUtil } from "discord.js";
-import yaml from "yaml";
-import { getRepository, getManager } from "typeorm";
-import { Contest } from "@shared/db/entity/Contest";
-import emoji_regex from "emoji-regex";
-import { client } from "../client";
-import { scheduleJob } from "node-schedule";
-import { ContestEntry } from "@shared/db/entity/ContestEntry";
-import { ContestVote } from "@shared/db/entity/ContestVote";
-import { CommandSet, Command, Action, ActionType } from "src/model/command";
-
-const CHANNEL_ID_PATTERN = /<#(\d+)>/;
-
-interface ActiveContest {
-    id: number;
-    voteReaction: string;
-}
-
-interface ContestCreationOptions {
-    in?: string;
-    duration?: string;
-    announce_winners?: boolean;
-    vote_reaction?: string;
-    max_winners?: number;
-    unique_winners?: boolean
-}
-
-const CONTEST_DEFAULTS: ContestCreationOptions = {
-    duration: "1d",
-    announce_winners: false,
-    vote_reaction: "❤",
-    max_winners: 1,
-    unique_winners: true
-};
-
-const DURATION_MULTIPLIERS: Dict<number> = {
-    "s": 1000,
-    "min": 60 * 1000,
-    "h": 60 * 60 * 1000,
-    "d": 24 * 60 * 60 * 1000,
-    "mon": 30 * 24 * 60 * 60 * 1000,
-    "y": 365 * 24 * 60 * 60 * 1000
-};
-
-const DURATION_REGEX_STR = `(\\d+) ?(${Object.keys(DURATION_MULTIPLIERS).join("|")})`;
-const DURATION_REGEX = new RegExp(DURATION_REGEX_STR, "i");
-
-function parseDuration(duration: string): number | undefined {
-    let match = DURATION_REGEX.exec(duration);
-
-    if (match.length == 0)
-        return undefined;
-
-    let num = match[1];
-    let unit = match[2];
-
-    return +num * DURATION_MULTIPLIERS[unit];
-}
-
-type ContestEntryWithMessage = ContestEntry & { message: Message };
-
-function numberToOrdered(num: number) {
-    const prefixes = ["st", "nd", "rd"];
-    let s = num % 10;
-    return 0 < s && s <= prefixes.length ? `${num}${prefixes[s - 1]}` : `${num}th`;
-}
-
-@CommandSet
-/*export*/ class ContestCommands {
-
-    activeContests: Dict<ActiveContest> = {};
-
-    diffEntryVotes(votes: ContestVote[], currentUsers: Collection<string, User>) {
-        let votedUsersIds = new Set(votes.map(v => v.userId));
-        let currentUsersIds = new Set(currentUsers.keys());
-
-        for (let currentUserId of currentUsersIds)
-            if (votedUsersIds.has(currentUserId))
-                votedUsersIds.delete(currentUserId);
-
-        for (let votedUserId of votedUsersIds)
-            if (currentUsersIds.has(votedUserId))
-                currentUsersIds.delete(votedUserId);
-
-        return [currentUsersIds, votedUsersIds];
-    }
-
-    async updateContestStatus(contest: Contest) {
-        let voteRepo = getRepository(ContestVote);
-        let entryRepo = getRepository(ContestEntry);
-
-        let channel = client.channels.get(contest.channel) as TextChannel;
-        if (!channel) {
-            console.log(`Channel ${contest.channel} has been deleted! Removing contest ${contest.id}...`);
-            await this.removeContest(contest.id);
-            return;
-        }
-
-        let newestEntry: Date = null;
-        let contestEntryMessageIds = new Set<string>(contest.entries.map(e => e.msgId));
-        for (let entry of contest.entries) {
-            try {
-                let msg = await channel.fetchMessage(entry.msgId);
-                let existingVotes = await voteRepo.find({ where: { contest: contest, contestEntry: entry } });
-
-                let voteReaction = msg.reactions.find(r => r.emoji.toString() == contest.voteReaction);
-                if (!voteReaction) {
-                    await voteRepo.remove(existingVotes);
-                    continue;
-                }
-
-                let users = await voteReaction.fetchUsers();
-                let [newVotes, removedVotes] = this.diffEntryVotes(existingVotes, users);
-                voteRepo.remove(existingVotes.filter(v => removedVotes.has(v.userId)));
-                let newVoteEntries = [...newVotes].map(i => voteRepo.create({
-                    userId: i,
-                    contest: contest,
-                    contestEntry: entry
-                }));
-                await voteRepo.save(newVoteEntries);
-
-                entry.votes = [
-                    ...newVoteEntries,
-                    ...existingVotes.filter(v => !removedVotes.has(v.userId))
-                ];
-                await entryRepo.save(entry);
-
-                if (!newestEntry || msg.createdAt > newestEntry)
-                    newestEntry = msg.createdAt;
-            } catch (err) {
-                console.log(`Failed to update entry ${entry.msgId} for contest ${contest.id} because ${err}!`);
-
-                await voteRepo.delete({ contestEntry: entry });
-                await entryRepo.delete({ msgId: entry.msgId });
-                contestEntryMessageIds.delete(entry.msgId);
-            }
-        }
-
-        let newEntries = (await channel.fetchMessages({
-            after: SnowflakeUtil.generate(newestEntry || contest.startDate)
-        })).filter(m => m.attachments.size != 0 && !contestEntryMessageIds.has(m.id));
-
-        for (let [_, msg] of newEntries)
-            await this.registerEntry(msg, contest);
-
-        if (contest.endDate < new Date()) {
-            await this.stopContest(contest.id);
-        } else {
-            scheduleJob(contest.endDate, this.stopContest.bind(this, contest.id));
-            this.activeContests[channel.id] = {
-                id: contest.id,
-                voteReaction: contest.voteReaction
-            };
-        }
-    }
-
-    async registerEntry(msg: Message, contest: Contest) {
-        let entryRepo = getRepository(ContestEntry);
-        let voteRepo = getRepository(ContestVote);
-
-        let entry = entryRepo.create({
-            msgId: msg.id,
-            contest: contest
-        });
-
-        await entryRepo.save(entry);
-
-        let voteReaction = msg.reactions.find(r => r.emoji.toString() == contest.voteReaction);
-
-        if (!voteReaction)
-            return;
-
-        let votedUsers = await voteReaction.fetchUsers();
-
-        await voteRepo.save(votedUsers.map(u => voteRepo.create({
-            userId: u.id,
-            contest: contest,
-            contestEntry: entry
-        })));
-    }
-
-    async removeContest(contestId: number) {
-        await getManager().transaction(async em => {
-            let contestRepo = em.getRepository(Contest);
-            let contestEntryRepo = em.getRepository(ContestEntry);
-            let contestVoteRepo = em.getRepository(ContestVote);
-
-            let contest = contestRepo.create({
-                id: contestId
-            });
-
-            await contestRepo.delete({
-                id: contestId
-            });
-            await contestVoteRepo.delete({
-                contest: contest
-            });
-            await contestEntryRepo.delete({
-                contest: contest
-            });
-        });
-    }
-
-    async pickValidEntries(channel: TextChannel, contestEntries: ContestEntry[], max: number, unique = true) {
-        let addedUsers = new Set<string>();
-        let result: ContestEntryWithMessage[] = [];
-        let maxResults = Math.min(max, contestEntries.length);
-
-        for (let entry of contestEntries) {
-            try {
-                let msg = await channel.fetchMessage(entry.msgId);
-                if (unique && addedUsers.has(msg.author.id))
-                    continue;
-
-                result.push({ ...entry, message: msg });
-                addedUsers.add(msg.author.id);
-                if (result.length == maxResults)
-                    break;
-            } catch (err) { }
-        }
-
-        return result;
-    }
-
-    async printResults(contest: Contest, channel: TextChannel) {
-        let entryRepo = getRepository(ContestEntry);
-
-        let entries = await entryRepo.find({
-            where: { contest: contest },
-            relations: ["votes"]
-        });
-
-        if (entries.length == 0) {
-            // Hmmm... maybe rich embeds?
-            await channel.send("No entries were sent into this contest! Therefore I declare myself a winner!");
-            return;
-        }
-
-        let winningEntries = await this.pickValidEntries(channel, entries.sort(compareNumbers(o => o.votes.length)), contest.maxWinners, contest.uniqueWinners);
-        let totalVotes = entries.reduce((p, c) => p + c.votes.length, 0);
-
-        let embed = new RichEmbed({
-            title: "🎆 Contest results 🎆",
-            color: 0x3b8dc4,
-            timestamp: new Date(),
-            description: `The contest has ended!\nCollected ${totalVotes} votes.\nHere are the results:`,
-            fields: winningEntries.map((e, i) => ({
-                name: `${numberToOrdered(i + 1)} place (${e.votes.length} votes)`,
-                value: `${e.message.author.toString()} ([View entry](${e.message.url}))`
-            }))
-        });
-
-        await channel.send(embed);
-    }
-
-    async stopContest(contestId: number) {
-        let repo = getRepository(Contest);
-        let contest = await repo.findOne(contestId);
-
-        let channel = client.channels.get(contest.channel) as TextChannel;
-        if (!channel) {
-            // TODO: Don't remove; instead report in web manager
-            console.log(`Channel ${contest.channel} has been deleted! Removing contest ${contest.id}...`);
-            await this.removeContest(contestId);
-            return;
-        }
-
-        await channel.send(`Current contest has ended! Thank you for your participation!`);
-
-        if (contest.announceWinners)
-            await this.printResults(contest, channel);
-
-        await repo.update(contestId, { active: false });
-    }
-
-    async createContest(msg: Message, info: ContestCreationOptions) {
-        if (info.in) {
-            let matches = CHANNEL_ID_PATTERN.exec(info.in);
-            if (matches.length == 0) {
-                await msg.channel.send(`${msg.author.toString()} I can't see such a channel!`);
-                return;
-            }
-
-            let channelId = matches[1];
-            if (!msg.guild.channels.exists("id", channelId)) {
-                await msg.channel.send(`${msg.author.toString()} This channel is not in the current guild!`);
-                return;
-            }
-
-            info.in = channelId;
-        } else
-            info.in = msg.channel.id;
-
-        if (info.max_winners < 1) {
-            await msg.channel.send(`${msg.author.toString()} The contest must have at least one possible winner!`);
-            return;
-        }
-
-        let dur = parseDuration(info.duration);
-        if (!dur) {
-            await msg.channel.send(`${msg.author.toString()} Duration format is invalid!`);
-            return;
-        }
-
-        if (!msg.guild.emojis.find(e => e.toString() == info.vote_reaction) && !emoji_regex().exec(info.vote_reaction)) {
-            await msg.channel.send(`${msg.author.toString()} The vote emote must be accessible by everyone on the server!`);
-            return;
-        }
-
-        let repo = getRepository(Contest);
-
-        let contest = await repo.findOne({
-            where: { channel: info.in, active: true }
-        });
-
-        if (contest) {
-            await msg.channel.send(`${msg.author.toString()} The channel already has a contest running!`);
-            return;
-        }
-
-        contest = repo.create({
-            channel: info.in,
-            startDate: new Date(),
-            endDate: new Date(Date.now() + dur),
-            announceWinners: info.announce_winners,
-            voteReaction: info.vote_reaction,
-            maxWinners: info.max_winners,
-            uniqueWinners: info.unique_winners,
-            active: true
-        });
-
-        await repo.save(contest);
-        await msg.channel.send(`${msg.author.toString()} Started contest (ID: ${contest.id})`);
-
-        scheduleJob(contest.endDate, this.stopContest.bind(this, contest.id));
-        this.activeContests[contest.channel] = {
-            id: contest.id,
-            voteReaction: contest.voteReaction
-        };
-    }
-
-    async onReact(reaction: MessageReaction, user: User) {
-        if (user.bot)
-            return;
-
-        let channel = reaction.message.channel;
-        let activeContest = this.activeContests[channel.id];
-        if (!activeContest || reaction.emoji.toString() != activeContest.voteReaction)
-            return;
-
-        let entryRepo = getRepository(ContestEntry);
-        let voteRepo = getRepository(ContestVote);
-
-        let entry = await entryRepo.findOne({
-            where: { msgId: reaction.message.id }
-        });
-        if (!entry)
-            return;
-
-        let vote = await voteRepo.findOne({
-            where: { userId: user.id, contestId: activeContest.id }
-        });
-        if (!vote)
-            vote = voteRepo.create({ userId: user.id, contestId: activeContest.id });
-
-        vote.contestEntry = entry;
-        await voteRepo.save(vote);
-    }
-
-    @Action(ActionType.MESSAGE)
-    async addEntry(actionsDone: boolean, m: Message, content: string) {
-        if (m.attachments.size == 0)
-            return false;
-
-        let channel = m.channel;
-
-        let contestRepo = getRepository(Contest);
-
-        let contest = await contestRepo.findOne({
-            where: {
-                channel: channel.id,
-                active: true
-            },
-            select: ["id", "voteReaction"]
-        });
-
-        if (!contest)
-            return false;
-
-        await this.registerEntry(m, contest);
-
-        // Don't prevent further actions
-        return false;
-    }
-
-    async onStart() {
-        let contestRepo = getRepository(Contest);
-        let contests = await contestRepo.find({
-            where: { active: true },
-            relations: ["entries"]
-        });
-
-        for (let contest of contests)
-            await this.updateContestStatus(contest);
-
-        client.on("messageReactionAdd", this.onReact);
-    }
-
-    @Command({ pattern: "create contest", auth: true })
-    async startContest(m: Message) {
-        if (!await isAuthorisedAsync(m.member)) {
-            m.channel.send(`${m.author.toString()} You're not authorised to create contests!`);
-            return;
-        }
-
-        let message = m.content.trim().substr(client.user.toString().length).trim().substr("create contest".length).trim();
-        let contestData: ContestCreationOptions = { ...CONTEST_DEFAULTS, ...yaml.parse(message) };
-        await this.createContest(m, contestData);
-    }
-
-    @Command({ pattern: "contests" })
-    async listContests(m: Message) {
-        let repo = getRepository(Contest);
-        let contests = await repo.find({ where: { active: true } });
-
-        let contestsData = contests.map(c => ({
-            contest: c,
-            channel: client.channels.get(c.channel) as TextChannel
-        })).filter(c => c.channel);
-
-        if (contestsData.length == 0)
-            await m.channel.send(`${m.author.toString()} There are no currently running contests!`);
-        else
-            await m.channel.send(`${m.author.toString()} Currently there are contests active in the following channels:\n${contestsData.map((c, i) => `${i + 1}. ${c.channel.toString()}`).join("\n")}`);
-    }
-
-    @Command({ pattern: /end contest( (\d*))?/, auth: true })
-    async endContest(m: Message, contents: string, matches: RegExpMatchArray) {
-        if (!await isAuthorisedAsync(m.member)) {
-            m.channel.send(`${m.author.toString()} You're not authorised to create contests!`);
-            return;
-        }
-
-        let repo = getRepository(Contest);
-        let contestId = +matches[1];
-
-        let contest = await repo.findOne({
-            where: { id: contestId },
-            select: ["id", "active"]
-        });
-
-        if (!contest) {
-            await m.channel.send(`${m.author.toString()} Can't find contest with ID ${contestId}`);
-            return;
-        }
-
-        if (!contest.active) {
-            await m.channel.send(`${m.author.toString()} The contest with ID ${contestId} is already inactive!`);
-            return;
-        }
-
-        await this.stopContest(contest.id);
-    }
-
-    @Command({ pattern: "announce winners", auth: true })
-    async announceWinners(m: Message) {
-        let repo = getRepository(Contest);
-
-        let contest = await repo.findOne({
-            where: { channel: m.channel.id },
-            order: { endDate: "DESC" }
-        });
-
-        if (!contest) {
-            await m.channel.send(`${m.author.toString()} There have never been any contests!`);
-            return;
-        }
-
-        if (contest.active) {
-            await this.stopContest(contest.id);
-            if (contest.announceWinners)
-                return;
-        }
-        await this.printResults(contest, m.channel as TextChannel);
-    }
-};

+ 5 - 5
bot/src/commands/dead_chat.ts

@@ -13,18 +13,18 @@ const triggers = [
 @CommandSet
 export class DeadChat {
     @Action(ActionType.MESSAGE)
-    async onMessage(actionsDone: boolean, msg: Message, content: string) {
+    async onMessage(actionsDone: boolean, msg: Message, content: string): Promise<boolean> {
         if (actionsDone)
             return false;
 
-        let lowerContent = content.toLowerCase();
+        const lowerContent = content.toLowerCase();
 
         if (!triggers.some(s => lowerContent.includes(s)))
             return false;
 
-        let repo = getRepository(DeadChatReply);
+        const repo = getRepository(DeadChatReply);
 
-        let reply = await repo.query(`  select message
+        const reply = await repo.query(`  select message
                                         from dead_chat_reply
                                         order by random()
                                         limit 1`) as DeadChatReply[];
@@ -36,4 +36,4 @@ export class DeadChat {
 
         return true;
     }
-};
+}

+ 69 - 70
bot/src/commands/facemorph.ts

@@ -4,7 +4,7 @@ import { client } from "../client";
 import * as cv from "opencv4nodejs";
 import * as path from "path";
 import request from "request-promise-native";
-import { Message } from "discord.js";
+import { Message, MessageAttachment } from "discord.js";
 import { getRepository } from "typeorm";
 import { FaceCaptionMessage, FaceCaptionType } from "@shared/db/entity/FaceCaptionMessage";
 import { KnownChannel } from "@shared/db/entity/KnownChannel";
@@ -24,7 +24,7 @@ const CAPTION_OFFSET = 5;
 @CommandSet
 export class Facemorph {
 
-    intersects(r1: cv.Rect, r2: cv.Rect) {
+    intersects(r1: cv.Rect, r2: cv.Rect): boolean {
         return (
             r1.x <= r2.x + r2.width &&
             r1.x + r1.width >= r2.x &&
@@ -32,37 +32,39 @@ export class Facemorph {
         );
     }
 
-    morphFaces = async (faces: cv.Rect[], data: Buffer) => {
-        let padoru = Math.random() <= this.getPadoruChance();
+    morphFaces = async (faces: cv.Rect[], data: Buffer): Promise<Jimp> => {
+        const padoru = Math.random() <= this.getPadoruChance();
         let jimpImage = await Jimp.read(data);
-        let emoteGuild = client.bot.guilds.resolve(EMOTE_GUILD);
+        const emoteGuild = client.bot.guilds.resolve(EMOTE_GUILD);
         if (!emoteGuild)
             return jimpImage;
-        let emojiKeys = process.env.FOOLS != "TRUE" ? [
+        const emojiKeys = process.env.FOOLS != "TRUE" ? [
             ...emoteGuild
                 .emojis.cache.filter(e => !e.animated && e.name.startsWith("PADORU") == padoru)
                 .keys()
         ]: 
-        [
-            "505335829565276160",
-            "430434087157760003",
-            "456472341874999297",
-            "649677767348060170",
-            "589706788782342183",
-            "665272109227835422"
-        ];
+            [
+                "505335829565276160",
+                "430434087157760003",
+                "456472341874999297",
+                "649677767348060170",
+                "589706788782342183",
+                "665272109227835422"
+            ];
 
         for (const rect of faces) {
-            let dx = rect.x + rect.width / 2;
-            let dy = rect.y + rect.height / 2;
-            let emojiKey = emojiKeys[Math.floor(Math.random() * emojiKeys.length)];
-            let emoji = client.bot.emojis.resolve(emojiKey)!;
+            const dx = rect.x + rect.width / 2;
+            const dy = rect.y + rect.height / 2;
+            const emojiKey = emojiKeys[Math.floor(Math.random() * emojiKeys.length)];
+            const emoji = client.bot.emojis.resolve(emojiKey);
+            if (!emoji)
+                throw new Error("Failed to resolve emoji!");
             let emojiImage = await Jimp.read(emoji.url);
             let ew = emojiImage.getWidth();
             let eh = emojiImage.getHeight();
 
             const CONSTANT_SCALE = 1.1;
-            let scaleFactor = (Math.max(rect.width, rect.height) / Math.min(ew, eh)) * CONSTANT_SCALE;
+            const scaleFactor = (Math.max(rect.width, rect.height) / Math.min(ew, eh)) * CONSTANT_SCALE;
             ew *= scaleFactor;
             eh *= scaleFactor;
 
@@ -74,9 +76,9 @@ export class Facemorph {
         return jimpImage;
     }
 
-    async getRandomCaption(type: FaceCaptionType) {
-        let repo = getRepository(FaceCaptionMessage);
-        let caption = await repo.query(`select message
+    async getRandomCaption(type: FaceCaptionType): Promise<FaceCaptionMessage | null> {
+        const repo = getRepository(FaceCaptionMessage);
+        const caption = await repo.query(`select message
                                         from face_caption_message
                                         where type = $1
                                         order by random()
@@ -86,12 +88,12 @@ export class Facemorph {
         return caption[0];
     }
 
-    captionFace = async (faces: cv.Rect[], data: Buffer) => {
-        let padoru = Math.random() <= this.getPadoruChance();
-        let face = faces[Math.floor(Math.random() * faces.length)];
-        let squaredFace = await face.toSquareAsync();
-        let targetSize = CAPTION_IMG_SIZE;
-        let img = await Jimp.read(data);
+    captionFace = async (faces: cv.Rect[], data: Buffer): Promise<Jimp> => {
+        const padoru = Math.random() <= this.getPadoruChance();
+        const face = faces[Math.floor(Math.random() * faces.length)];
+        const squaredFace = await face.toSquareAsync();
+        const targetSize = CAPTION_IMG_SIZE;
+        const img = await Jimp.read(data);
 
         let tempImg = await Jimp.create(squaredFace.width, squaredFace.height);
         tempImg = await tempImg.blit(
@@ -105,7 +107,7 @@ export class Facemorph {
         );
         tempImg = await tempImg.scale(targetSize / squaredFace.width);
 
-        let font = await Jimp.loadFont(padoru ? Jimp.FONT_SANS_16_WHITE : Jimp.FONT_SANS_16_BLACK);
+        const font = await Jimp.loadFont(padoru ? Jimp.FONT_SANS_16_WHITE : Jimp.FONT_SANS_16_BLACK);
         let text = "";
         if(padoru)
             text = "PADORU PADORU";
@@ -114,11 +116,11 @@ export class Facemorph {
             text = titles[Math.floor(Math.random() * titles.length)];
         }
         else {
-            let prefixMessage = (await this.getRandomCaption(FaceCaptionType.PREFIX))?.message ?? "Feed them";
-            let postfixMessage = (await this.getRandomCaption(FaceCaptionType.POSTFIX)) ?? "carrots";
+            const prefixMessage = (await this.getRandomCaption(FaceCaptionType.PREFIX))?.message ?? "Feed them";
+            const postfixMessage = (await this.getRandomCaption(FaceCaptionType.POSTFIX)) ?? "carrots";
             text = `${prefixMessage} ${postfixMessage}`;
         }
-        let h = Jimp.measureTextHeight(font, text, targetSize - CAPTION_OFFSET * 2);
+        const h = Jimp.measureTextHeight(font, text, targetSize - CAPTION_OFFSET * 2);
         let finalImage = await Jimp.create(targetSize, targetSize + h + CAPTION_OFFSET * 2, padoru ? "#FD2027" : "#FFFFFF");
 
         finalImage = await finalImage.print(
@@ -133,36 +135,33 @@ export class Facemorph {
         return finalImage;
     }
 
-    /**
- * PADORU PADORU
- */
-    getPadoruChance() {
-        let now = new Date();
+    getPadoruChance(): number {
+        const now = new Date();
         if (now.getUTCMonth() != 11 || now.getUTCDate() > 25)
             return 0;
         return 1 / (27.0 - now.getUTCDate());
     }
 
-    async processFaceSwap(message: Message, attachmentUrl: string, processor?: ImageProcessor, failMessage?: string, successMessage?: string) {
-        let data = await request(attachmentUrl, { encoding: null }) as Buffer;
-        let im = await cv.imdecodeAsync(data, cv.IMREAD_COLOR);
-        let gray = await im.cvtColorAsync(cv.COLOR_BGR2GRAY);
-        let normGray = await gray.equalizeHistAsync();
-        let animeFaces = await animeCascade.detectMultiScaleAsync(
+    async processFaceSwap(message: Message, attachmentUrl: string, processor?: ImageProcessor, failMessage?: string, successMessage?: string): Promise<void> {
+        const data = await request(attachmentUrl, { encoding: null }) as Buffer;
+        const im = await cv.imdecodeAsync(data, cv.IMREAD_COLOR);
+        const gray = await im.cvtColorAsync(cv.COLOR_BGR2GRAY);
+        const normGray = await gray.equalizeHistAsync();
+        const animeFaces = await animeCascade.detectMultiScaleAsync(
             normGray,
             1.1,
             5,
             0,
             new cv.Size(24, 24)
         );
-        let normalFaces = await faceCascade.detectMultiScaleAsync(gray);
+        const normalFaces = await faceCascade.detectMultiScaleAsync(gray);
 
         if (animeFaces.objects.length == 0 && normalFaces.objects.length == 0) {
             if (failMessage) message.channel.send(failMessage);
             return;
         }
 
-        let faces = [...normalFaces.objects, ...animeFaces.objects];
+        const faces = [...normalFaces.objects, ...animeFaces.objects];
 
         let normalCount = normalFaces.objects.length;
         let animeCount = animeFaces.objects.length;
@@ -176,8 +175,8 @@ export class Facemorph {
                 const rAnime = faces[j];
 
                 if (this.intersects(rAnime, rNormal)) {
-                    let animeA = rAnime.width * rAnime.height;
-                    let faceA = rNormal.width * rNormal.height;
+                    const animeA = rAnime.width * rAnime.height;
+                    const faceA = rNormal.width * rNormal.height;
 
                     if (animeA > faceA) {
                         faces.splice(i, 1);
@@ -195,8 +194,8 @@ export class Facemorph {
 
         let jimpImage: Jimp;
         if (processor)
-                jimpImage = await processor(faces, data);
-            else {
+            jimpImage = await processor(faces, data);
+        else {
             if (Math.random() <= CAPTION_PROBABILITY)
                 jimpImage = await this.captionFace(faces, data);
             else
@@ -204,8 +203,8 @@ export class Facemorph {
         }
 
         jimpImage.quality(90);
-        let buffer = await jimpImage.getBufferAsync(Jimp.MIME_JPEG);
-        let messageContents =
+        const buffer = await jimpImage.getBufferAsync(Jimp.MIME_JPEG);
+        const messageContents =
             successMessage ||
             `I noticed a face in the image. I think this looks better ${client.bot.emojis.resolve("505076258753740810")?.toString() ?? ":)"}`;
 
@@ -214,22 +213,22 @@ export class Facemorph {
         });
     }
 
-    processLastImage(msg: Message, processor: ImageProcessor) {
-        let lastImagedMessage = msg.channel.messages.cache.filter(m => !m.author.bot && m.attachments.find(v => isValidImage(v.name)) != undefined).last();
+    processLastImage(msg: Message, processor: ImageProcessor): void {
+        type AttachedMessage = {msg: Message, att: MessageAttachment};
+        const lastImagedMessage = msg.channel.messages.cache.mapValues(m => ({msg: m, att: m.attachments.find(v => isValidImage(v.name))}))
+            .filter(v => v.att != undefined).last() as AttachedMessage;
 
         if (!lastImagedMessage) {
             msg.channel.send(`${msg.author.toString()} Sorry, I couldn't find any recent messages with images.`);
             return;
         }
 
-        let image = lastImagedMessage.attachments.find(v => isValidImage(v.name))!;
-
-        let replyEmoji = client.bot.emojis.resolve("505076258753740810");
-        let emojiText = replyEmoji ? replyEmoji.toString() : "Jiiii~";
+        const replyEmoji = client.bot.emojis.resolve("505076258753740810");
+        const emojiText = replyEmoji ? replyEmoji.toString() : "Jiiii~";
 
         this.processFaceSwap(
             msg,
-            image.url,
+            lastImagedMessage.att.url,
             processor,
             `${msg.author.toString()} Nice image! I don't see anything interesting, though.`,
             `${msg.author.toString()} ${emojiText}`
@@ -237,19 +236,19 @@ export class Facemorph {
     }
 
     @Action(ActionType.MESSAGE)
-    async morphRandomImage(actionsDone: boolean, msg: Message, contests: string) {
+    async morphRandomImage(actionsDone: boolean, msg: Message): Promise<boolean> {
         if (actionsDone) return false;
 
         if (msg.mentions.users.size > 0 && msg.mentions.users.first()?.id == client.botUser.id)
             return false;
 
-        let imageAttachment = msg.attachments.find(v => isValidImage(v.name));
+        const imageAttachment = msg.attachments.find(v => isValidImage(v.name));
 
         if (imageAttachment) {
 
-            let repo = getRepository(KnownChannel);
+            const repo = getRepository(KnownChannel);
 
-            let knownChannel = await repo.findOne({
+            const knownChannel = await repo.findOne({
                 where: { channelId: msg.channel.id },
                 select: ["faceMorphProbability"]
             });
@@ -267,10 +266,10 @@ export class Facemorph {
     }
 
     @Action(ActionType.DIRECT_MENTION)
-    async morphProvidedImage(actionsDone: boolean, msg: Message, content: string) {
+    async morphProvidedImage(actionsDone: boolean, msg: Message, content: string): Promise<boolean> {
         if (actionsDone) return false;
 
-        let image = msg.attachments.find(v => isValidImage(v.name));
+        const image = msg.attachments.find(v => isValidImage(v.name));
         if (!image) {
             if (msg.attachments.size > 0) {
                 msg.channel.send(
@@ -287,8 +286,8 @@ export class Facemorph {
         else
             processor = this.morphFaces;
 
-        let replyEmoji = client.bot.emojis.resolve("505076258753740810");
-        let emojiText = replyEmoji ? replyEmoji.toString() : "Jiiii~";
+        const replyEmoji = client.bot.emojis.resolve("505076258753740810");
+        const emojiText = replyEmoji ? replyEmoji.toString() : "Jiiii~";
 
         this.processFaceSwap(
             msg,
@@ -304,14 +303,14 @@ export class Facemorph {
     @Command({
         pattern: "caption last image"
     })
-    captionLastImage(msg: Message) {
+    captionLastImage(msg: Message): void {
         this.processLastImage(msg, this.captionFace);
     }
 
     @Command({
         pattern: "look at last image"
     })
-    lookLastImage(msg: Message) {
-        this.processLastImage(msg, this.morphFaces)
+    lookLastImage(msg: Message): void {
+        this.processLastImage(msg, this.morphFaces);
     }
-};
+}

La diferencia del archivo ha sido suprimido porque es demasiado grande
+ 7 - 7
bot/src/commands/file_only_channel_checker.ts


+ 64 - 62
bot/src/commands/forums_news_checker.ts

@@ -1,4 +1,4 @@
-import TurndownService, { Options } from "turndown";
+import TurndownService from "turndown";
 import interval from "interval-promise";
 import { client, FORUMS_DOMAIN } from "../client";
 import sha1 from "sha1";
@@ -32,30 +32,30 @@ export class ForumsNewsChecker {
             replacement: () => ""
         });
         this.turndown.addRule("link", {
-            filter: (node: HTMLElement, opts: Options) => node.nodeName === "A" && node.getAttribute("href") != null,
+            filter: (node: HTMLElement) => 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>");
+    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 () => {
+    checkFeeds = async (): Promise<void> => {
         try {
             console.log(`Checking feeds on ${new Date().toISOString()}`);
-            let forumsNewsRepo = getRepository(PostedForumNewsItem);
-            let postVerifyMessageRepo = getRepository(PostVerifyMessage);
+            const forumsNewsRepo = getRepository(PostedForumNewsItem);
+            const postVerifyMessageRepo = getRepository(PostVerifyMessage);
 
-            let forumThreads = await client.forum.getForumThreads(NEWS_FORUM_ID);
+            const 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);
+            for (const thread of [...forumThreads.threads, ...forumThreads.sticky]) {
+                const firstPost = await client.forum.getPost(thread.first_post_id);
 
-                let contents = this.bbCodeToMarkdown(firstPost.message);
+                const contents = this.bbCodeToMarkdown(firstPost.message);
                 let itemObj = forumsNewsRepo.create({
                     id: thread.thread_id.toString(),
                     hash: sha1(firstPost.message),
@@ -68,7 +68,7 @@ export class ForumsNewsChecker {
                     })
                 });
 
-                let postItem = await forumsNewsRepo.findOne({
+                const postItem = await forumsNewsRepo.findOne({
                     where: { id: itemObj.id },
                     relations: ["verifyMessage"]
                 });
@@ -79,14 +79,14 @@ export class ForumsNewsChecker {
                         await forumsNewsRepo.update({
                             id: postItem.id
                         }, {
-                                hash: itemObj.hash
-                            });
+                            hash: itemObj.hash
+                        });
                         continue;
                     }
 
                     // Add message ID to mark for edit
                     if (postItem.hash != itemObj.hash) {
-                        let newHash = itemObj.hash;
+                        const newHash = itemObj.hash;
                         if (!postItem.verifyMessage)
                             postItem.verifyMessage = itemObj.verifyMessage;
 
@@ -109,70 +109,72 @@ export class ForumsNewsChecker {
         }
     }
 
-    async initPendingReactors() {
+    async initPendingReactors(): Promise<void> {
         if (!this.verifyChannelId)
             return;
 
-        let verifyChannel = client.bot.channels.resolve(this.verifyChannelId);
+        const verifyChannel = client.bot.channels.resolve(this.verifyChannelId);
 
         if (!verifyChannel)
             return;
 
-        let repo = getRepository(PostedForumNewsItem);
-        let verifyMessageRepo = getRepository(PostVerifyMessage);
+        const repo = getRepository(PostedForumNewsItem);
+        const verifyMessageRepo = getRepository(PostVerifyMessage);
 
-        let pendingVerifyMessages = await repo.find({
+        const 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);
+        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;
             }
 
-            let collector = m.createReactionCollector(this.isVerifyReaction, { maxEmojis: 1 });
+            const 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) {
+    async addVerifyMessage(item: PostedForumNewsItem): Promise<void> {
         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;
+        const 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);
+        const verifyMessageRepo = getRepository(PostVerifyMessage);
+        const forumsNewsRepo = getRepository(PostedForumNewsItem);
 
         if (item.verifyMessage.messageId) {
-            let oldMessage = await this.tryFetchMessage(verifyChannel, item.verifyMessage.messageId);
+            const 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;
+        const 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)
+        const collector = newMessage.createReactionCollector(this.isVerifyReaction, { maxEmojis: 1 });
+        collector.on("collect", this.collectReaction);
         this.reactionCollectors[newMessage.id] = collector;
         this.verifyMessageIdToPost[newMessage.id] = item.id;
 
@@ -180,16 +182,16 @@ export class ForumsNewsChecker {
         await forumsNewsRepo.save(item);
     }
 
-    collectReaction = async (reaction: MessageReaction, collector: Collector<string, MessageReaction>) => {
-        let verifyMessageRepo = getRepository(PostVerifyMessage);
-        let postRepo = getRepository(PostedForumNewsItem);
+    collectReaction = async (reaction: MessageReaction, collector: Collector<string, MessageReaction>): Promise<void> => {
+        const verifyMessageRepo = getRepository(PostVerifyMessage);
+        const postRepo = getRepository(PostedForumNewsItem);
 
-        let m = reaction.message;
+        const m = reaction.message;
         collector.stop();
         delete this.reactionCollectors[m.id];
-        let postId = this.verifyMessageIdToPost[m.id];
+        const postId = this.verifyMessageIdToPost[m.id];
 
-        let post = await postRepo.findOne({
+        const post = await postRepo.findOne({
             where: { id: postId },
             relations: ["verifyMessage"]
         });
@@ -208,18 +210,18 @@ export class ForumsNewsChecker {
         delete this.verifyMessageIdToPost[m.id];
     }
 
-    async sendNews(item: PostedForumNewsItem) {
-        let channelRepo = getRepository(KnownChannel);
-        let newsPostRepo = getRepository(PostedForumNewsItem);
+    async sendNews(item: PostedForumNewsItem): Promise<void> {
+        const channelRepo = getRepository(KnownChannel);
+        const newsPostRepo = getRepository(PostedForumNewsItem);
 
-        let outChannel = await channelRepo.findOne({
+        const outChannel = await channelRepo.findOne({
             where: { channelType: NEWS_FEED_CHANNEL }
         });
 
         if (!outChannel)
             return;
 
-        let sentMessage = await this.postNewsItem(outChannel.channelId, item);
+        const sentMessage = await this.postNewsItem(outChannel.channelId, item);
 
         item.postedMessageId = sentMessage?.id;
         item.verifyMessage = undefined;
@@ -227,11 +229,11 @@ export class ForumsNewsChecker {
         await newsPostRepo.save(item);
     }
 
-    isVerifyReaction = (reaction: MessageReaction, user: User) => {
+    isVerifyReaction = (reaction: MessageReaction, user: User): boolean => {
         return (reaction.emoji.name == "✅" || reaction.emoji.name == "❌") && !user.bot;
     }
 
-    async tryFetchMessage(channel?: Channel, messageId?: string) {
+    async tryFetchMessage(channel?: Channel, messageId?: string): Promise<Message | null> {
         if(!channel || !messageId)
             return null;
         try {
@@ -243,7 +245,7 @@ export class ForumsNewsChecker {
         }
     }
 
-    get shouldVerify() {
+    get shouldVerify(): boolean {
         return this.verifyChannelId != undefined;
     }
 
@@ -251,14 +253,14 @@ export class ForumsNewsChecker {
         if (!item.verifyMessage)
             throw new Error("No message to send!");
 
-        let newsMessage = this.toNewsString(item.verifyMessage);
-        let ch = client.bot.channels.resolve(channel);
+        const newsMessage = this.toNewsString(item.verifyMessage);
+        const ch = client.bot.channels.resolve(channel);
 
         if (!(ch instanceof TextChannel))
             return null;
 
         if (item.postedMessageId) {
-            let message = await this.tryFetchMessage(ch, item.postedMessageId);
+            const message = await this.tryFetchMessage(ch, item.postedMessageId);
             if (message)
                 return await message.edit(newsMessage);
             else
@@ -268,7 +270,7 @@ export class ForumsNewsChecker {
             return await ch.send(newsMessage) as Message;
     }
 
-    toNewsString(item: PostVerifyMessage) {
+    toNewsString(item: PostVerifyMessage): string {
         return `**${item.title}**
 Posted by ${item.author}
 ${item.link}
@@ -276,7 +278,7 @@ ${item.link}
 ${item.text}`;
     }
 
-    toVerifyString(postId: string, item: PostVerifyMessage) {
+    toVerifyString(postId: string, item: PostVerifyMessage): string {
         return `[${item.isNew ? "🆕 ADD" : "✏️ EDIT"}]
 Post ID: **${postId}**
         
@@ -288,17 +290,17 @@ React with ✅ (approve) or ❌ (deny).`;
     @Command({
         pattern: /^edit (\d+)\s+((.*[\n\r]*)+)$/i
     })
-    async editPreview(msg: Message, contests: string, match: RegExpMatchArray) {
+    async editPreview(msg: Message, contests: string, match: RegExpMatchArray): Promise<void> {
         if (msg.channel.id != this.verifyChannelId)
             return;
 
-        let id = match[1];
-        let newContents = match[2].trim();
+        const id = match[1];
+        const newContents = match[2].trim();
 
-        let repo = getRepository(PostedForumNewsItem);
-        let verifyRepo = getRepository(PostVerifyMessage);
+        const repo = getRepository(PostedForumNewsItem);
+        const verifyRepo = getRepository(PostVerifyMessage);
 
-        let post = await repo.findOne({
+        const post = await repo.findOne({
             where: { id: id },
             relations: ["verifyMessage"]
         });
@@ -308,7 +310,7 @@ React with ✅ (approve) or ❌ (deny).`;
             return;
         }
 
-        let editMsg = await this.tryFetchMessage(client.bot.channels.resolve(this.verifyChannelId) ?? undefined, post.verifyMessage.messageId);
+        const 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.`);
@@ -322,10 +324,10 @@ React with ✅ (approve) or ❌ (deny).`;
         await msg.delete();
     }
 
-    async onStart() {
-        let repo = getRepository(KnownChannel);
+    async onStart(): Promise<void> {
+        const repo = getRepository(KnownChannel);
 
-        let verifyChannel = await repo.findOne({
+        const verifyChannel = await repo.findOne({
             channelType: NEWS_POST_VERIFY_CHANNEL
         });
 
@@ -334,10 +336,10 @@ React with ✅ (approve) or ❌ (deny).`;
 
         this.verifyChannelId = verifyChannel.channelId;
 
-        let user = await client.forum.getMe();
+        const user = await client.forum.getMe();
         this.botUserId = user.user_id;
 
         await this.initPendingReactors();
         interval(this.checkFeeds, RSS_UPDATE_INTERVAL_MIN * 60 * 1000);
     }
-};
+}

+ 38 - 38
bot/src/commands/guide.ts

@@ -6,8 +6,8 @@ import { CommandSet, Action, ActionType, Command } from "src/model/command";
 
 @CommandSet
 export class GuideCommands {
-    async matchGuide(keywords: string[]) {
-        let a = await getRepository(Guide).query(
+    async matchGuide(keywords: string[]): Promise<Guide | null> {
+        const a = await getRepository(Guide).query(
             `select guide.*
              from guide
              inner join (select gk."guideId", count("guideKeywordId") as gc
@@ -28,10 +28,10 @@ export class GuideCommands {
         return a[0];
     }
 
-    async listGuides(msg: Message, guideType: string, message: string) {
-        let repo = getRepository(Guide);
+    async listGuides(msg: Message, guideType: string, message: string): Promise<void> {
+        const repo = getRepository(Guide);
 
-        let allGuides = await repo.createQueryBuilder("guide")
+        const allGuides = await repo.createQueryBuilder("guide")
             .select(["guide.displayName"])
             .leftJoinAndSelect("guide.keywords", "keyword")
             .where("guide.type = :type", { type: guideType })
@@ -40,7 +40,7 @@ export class GuideCommands {
         const MAX_GUIDES_PER_MSG = 30;
         let guides = `${msg.author.toString()} ${message}\n\`\`\``;
         let guideNum = 0;
-        for (let guide of allGuides) {
+        for (const guide of allGuides) {
             guides += `${guide.displayName} -- ${guide.keywords.map(k => k.keyword).join(" ")}\n`;
             guideNum++;
 
@@ -59,16 +59,16 @@ export class GuideCommands {
     }
 
     @Action(ActionType.DIRECT_MENTION)
-    async displayGuide(actionsDone: boolean, msg: Message, content: string) {
+    async displayGuide(actionsDone: boolean, msg: Message, content: string): Promise<boolean> {
         if (actionsDone)
             return false;
 
         if (msg.attachments.size > 0 || content.length == 0)
             return false;
 
-        let parts = content.split(" ").map(s => s.trim()).filter(s => s.length != 0);
+        const parts = content.split(" ").map(s => s.trim()).filter(s => s.length != 0);
 
-        let guide = await this.matchGuide(parts);
+        const guide = await this.matchGuide(parts);
 
         if (guide) {
             msg.channel.send(guide.content);
@@ -85,12 +85,12 @@ export class GuideCommands {
             example: "make <GUIDE TYPE> <NEWLINE>name: <NAME> <NEWLINE> keywords: <KEYWORDS> <NEWLINE> contents: <CONTENTS>"
         }
     })
-    async makeGuide(msg: Message, content: string, match: RegExpMatchArray) {
+    async makeGuide(msg: Message, content: string, match: RegExpMatchArray): Promise<void> {
         if (!await isAuthorisedAsync(msg.member)) return;
-        let type = match[1].toLowerCase();
-        let name = match[2].trim();
-        let keywords = match[3].toLowerCase().split(" ").map(s => s.trim()).filter(s => s.length != 0);
-        let contents = match[4].trim();
+        const type = match[1].toLowerCase();
+        const name = match[2].trim();
+        const keywords = match[3].toLowerCase().split(" ").map(s => s.trim()).filter(s => s.length != 0);
+        const contents = match[4].trim();
 
         if (contents.length == 0) {
             msg.channel.send(
@@ -106,26 +106,26 @@ export class GuideCommands {
             return;
         }
 
-        let repo = getRepository(GuideKeyword);
-        let guideRepo = getRepository(Guide);
+        const repo = getRepository(GuideKeyword);
+        const guideRepo = getRepository(Guide);
 
-        let existingKeywords = await repo.find({
+        const existingKeywords = await repo.find({
             where: [
                 ...keywords.map(k => ({ keyword: k }))
             ]
         });
 
-        let existingGuide = await this.matchGuide(keywords);
+        const existingGuide = await this.matchGuide(keywords);
 
-        let addGuide = async () => {
-            let newKeywords = new Set<string>();
-            let knownKeywords = new Set(existingKeywords.map(e => e.keyword));
-            for (let word of keywords) {
+        const addGuide = async () => {
+            const newKeywords = new Set<string>();
+            const knownKeywords = new Set(existingKeywords.map(e => e.keyword));
+            for (const word of keywords) {
                 if (!knownKeywords.has(word))
                     newKeywords.add(word);
             }
 
-            let addedKeywords = await repo.save([...newKeywords].map(k => repo.create({
+            const addedKeywords = await repo.save([...newKeywords].map(k => repo.create({
                 keyword: k
             })));
 
@@ -138,7 +138,7 @@ export class GuideCommands {
         };
 
         if (existingGuide) {
-            let guideKeywordsCount = await repo
+            const guideKeywordsCount = await repo
                 .createQueryBuilder("keywords")
                 .leftJoinAndSelect("keywords.relatedGuides", "guide")
                 .where("guide.id = :id", { id: existingGuide.id })
@@ -167,10 +167,10 @@ export class GuideCommands {
             description: "Deletes a guide with the specified keywords"
         }
     })
-    async deleteGuide(msg: Message, content: string, match: RegExpMatchArray) {
+    async deleteGuide(msg: Message, content: string, match: RegExpMatchArray): Promise<void> {
         if (!await isAuthorisedAsync(msg.member)) return;
-        let type = match[1];
-        let keywords = match[2].toLowerCase().split(" ").map(s => s.trim()).filter(s => s.length != 0);
+        const type = match[1];
+        const keywords = match[2].toLowerCase().split(" ").map(s => s.trim()).filter(s => s.length != 0);
 
         if (!(<string[]>Object.values(GuideType)).includes(type)) {
             await msg.channel.send(
@@ -179,14 +179,14 @@ export class GuideCommands {
             return;
         }
 
-        let dedupedKeywords = [...new Set(keywords)];
+        const dedupedKeywords = [...new Set(keywords)];
 
-        let repo = getRepository(GuideKeyword);
-        let guideRepo = getRepository(Guide);
-        let existingGuide = await this.matchGuide(keywords);
+        const repo = getRepository(GuideKeyword);
+        const guideRepo = getRepository(Guide);
+        const existingGuide = await this.matchGuide(keywords);
 
         if (existingGuide) {
-            let guideKeywordsCount = await repo
+            const guideKeywordsCount = await repo
                 .createQueryBuilder("keywords")
                 .leftJoinAndSelect("keywords.relatedGuides", "guide")
                 .where("guide.id = :id", { id: existingGuide.id })
@@ -203,17 +203,17 @@ export class GuideCommands {
     }
 
     @Command({ pattern: "guides", documentation: { description: "Lists all guides and keywords that trigger them.", example: "guides" } })
-    async showGuides(msg: Message) {
+    async showGuides(msg: Message): Promise<void> {
         await this.listGuides(msg, "guide", "Here are the guides I have:");
     }
 
     @Command({ pattern: "memes", documentation: {description: "Lists all memes and keywords that trigger them.", example: "memes"} })
-    async showMemes(msg: Message) {
-        await this.listGuides(msg, "meme", "Here are some random memes I have:")
+    async showMemes(msg: Message): Promise<void> {
+        await this.listGuides(msg, "meme", "Here are some random memes I have:");
     }
 
     @Command({ pattern: "misc", documentation: {description: "Lists all additional keywords the bot reacts to.", example: "misc"} })
-    async showMisc(msg: Message) {
-        await this.listGuides(msg, "misc", "These are some misc stuff I can also do:")
+    async showMisc(msg: Message): Promise<void> {
+        await this.listGuides(msg, "misc", "These are some misc stuff I can also do:");
     }
-};
+}

+ 4 - 4
bot/src/commands/help.ts

@@ -6,20 +6,20 @@ import { getDocumentation } from "src/main";
 @CommandSet
 export class Help {
     @Command({ pattern: "help" })
-    async showHelp(msg: Message) {
-        let isAuthed = await isAuthorisedAsync(msg.member);
+    async showHelp(msg: Message): Promise<void> {
+        const isAuthed = await isAuthorisedAsync(msg.member);
 
         let baseCommands = "\n";
         let modCommands = "\n";
 
-        for (let doc of getDocumentation()) {
+        for (const doc of getDocumentation()) {
             if (isAuthed && doc.auth)
                 modCommands = `${modCommands}${doc.example}  -  ${doc.doc}\n`;
             else if (!doc.auth)
                 baseCommands = `${baseCommands}${doc.example} - ${doc.doc}\n`;
         }
 
-        let name = process.env.FOOLS == "TRUE" ? "HorseBot" : "NoctBot";
+        const name = process.env.FOOLS == "TRUE" ? "HorseBot" : "NoctBot";
         let message = `Hello! I am ${name}! My job is to help with C(O)M-related problems!\nPing me with one of the following commands:\n\`\`\`${baseCommands}\`\`\``;
 
         if (isAuthed)

+ 3 - 3
bot/src/commands/inspire.ts

@@ -5,15 +5,15 @@ import { CommandSet, Command } from "src/model/command";
 @CommandSet
 export class Inspire {
 
-    async doInspire(msg: Message) {
-        let result = await request("https://inspirobot.me/api?generate=true");
+    async doInspire(msg: Message): Promise<void> {
+        const result = await request("https://inspirobot.me/api?generate=true");
         msg.channel.send(`${msg.author.toString()} Here is a piece of my wisdom:`, {
             files: [ result ]
         });
     }
 
     @Command({ pattern: "inspire me", documentation: {description: "Generates an inspiring quote just for you", example: "inspire me"}})
-    inspire(msg: Message) {
+    inspire(msg: Message): void {
         this.doInspire(msg);
     }
 }

+ 55 - 54
bot/src/commands/news_aggregator.ts

@@ -1,6 +1,6 @@
-import TurndownService, { Options } from "turndown";
+import TurndownService from "turndown";
 import interval from "interval-promise";
-import { client, FORUMS_DOMAIN, BotClient } from "../client";
+import { client, FORUMS_DOMAIN } from "../client";
 import sha1 from "sha1";
 import * as path from "path";
 import * as fs from "fs";
@@ -37,24 +37,24 @@ export class NewsAggregator {
             replacement: () => ""
         });
         this.turndown.addRule("link", {
-            filter: (node: HTMLElement, opts: Options) => node.nodeName === "A" && node.getAttribute("href") != null,
+            filter: (node: HTMLElement) => node.nodeName === "A" && node.getAttribute("href") != null,
             replacement: (content: string, node: Node) => (node instanceof HTMLElement ? node.getAttribute("href") : null) ?? ""
         });
     }
 
-    checkFeeds = async () => {
+    checkFeeds = async (): Promise<void> => {
         console.log(`Aggregating feeds on ${new Date().toISOString()}`);
 
-        let aggregatorJobs = [];
+        const aggregatorJobs = [];
 
-        for (let aggregator of this.aggregators) {
+        for (const aggregator of this.aggregators) {
             aggregatorJobs.push(aggregator.aggregate());
         }
-        let aggregatedItems = await Promise.all(aggregatorJobs);
+        const aggregatedItems = await Promise.all(aggregatorJobs);
 
-        for (let itemSet of aggregatedItems) {
-            for (let item of itemSet) {
-                let itemObj = {
+        for (const itemSet of aggregatedItems) {
+            for (const item of itemSet) {
+                const itemObj = {
                     ...item,
                     cacheMessageId: undefined,
                     postedMessageId: undefined
@@ -67,20 +67,20 @@ export class NewsAggregator {
         }
     }
 
-    clipText(text: string) {
+    clipText(text: string): string {
         if (text.length <= MAX_PREVIEW_LENGTH)
             return text;
 
         return `${text.substring(0, MAX_PREVIEW_LENGTH)}...`;
     }
 
-    async addNewsItem(item: NewsPostItem) {
+    async addNewsItem(item: NewsPostItem): Promise<void> {
         if (!this.aggregateChannelID)
             return;
 
-        let repo = getRepository(AggroNewsItem);
+        const repo = getRepository(AggroNewsItem);
 
-        let ch = client.bot.channels.resolve(this.aggregateChannelID);
+        const ch = client.bot.channels.resolve(this.aggregateChannelID);
 
         if (!(ch instanceof TextChannel))
             return;
@@ -113,7 +113,7 @@ export class NewsAggregator {
 
         if (item.needsTranslation && process.env.GOOGLE_APP_ID)
             try {
-                let request = {
+                const request = {
                     parent: this.tlClient.locationPath(process.env.GOOGLE_APP_ID, "global"),
                     contents: [item.title, item.contents],
                     mimeType: "text/html",
@@ -121,8 +121,8 @@ export class NewsAggregator {
                     targetLanguageCode: "en"
                 };
 
-                let [res] = await this.tlClient.translateText(request);
-                let [translatedTitle, translatedContents] = res.translations ?? [undefined, undefined];
+                const [res] = await this.tlClient.translateText(request);
+                const [translatedTitle, translatedContents] = res.translations ?? [undefined, undefined];
                 
                 if (translatedTitle?.translatedText && translatedContents?.translatedText) {
                     item.title = translatedTitle.translatedText;
@@ -135,14 +135,14 @@ export class NewsAggregator {
         item.contents = this.bbCodeParser.feed(item.contents).toString();
 
         if (!newsItem.forumsEditPostId) {
-            let createResponse = await client.forum.createThread(FORUMS_STAGING_ID, item.title, item.contents);
+            const createResponse = await client.forum.createThread(FORUMS_STAGING_ID, item.title, item.contents);
             newsItem.forumsEditPostId = createResponse.thread.thread_id;
         } else if(newsItem.forumsNewsPostId){
             await client.forum.postReply(newsItem.forumsNewsPostId, item.contents);
         }
 
 
-        let msg = await ch.send(new MessageEmbed({
+        const msg = await ch.send(new MessageEmbed({
             title: item.title,
             url: item.link,
             color: item.embedColor,
@@ -161,39 +161,39 @@ export class NewsAggregator {
         await msg.react("✅");
         await msg.react("❌");
 
-        let collector = msg.createReactionCollector(this.isVerifyReaction, { maxEmojis: 1 });
-        collector.on("collect", this.collectReaction)
+        const collector = msg.createReactionCollector(this.isVerifyReaction, { maxEmojis: 1 });
+        collector.on("collect", this.collectReaction);
         this.reactionCollectors[msg.id] = collector;
         this.verifyMessageIdToPost[msg.id] = newsItem;
 
         await repo.save(newsItem);
     }
 
-    isVerifyReaction(reaction: MessageReaction, user: User) {
+    isVerifyReaction(reaction: MessageReaction, user: User): boolean {
         return (reaction.emoji.name == "✅" || reaction.emoji.name == "❌") && !user.bot && user.id != client.botUser.id;
     }
 
-    collectReaction = async (reaction: MessageReaction, collector: Collector<string, MessageReaction>) => {
-        let repo = getRepository(AggroNewsItem);
+    collectReaction = async (reaction: MessageReaction, collector: Collector<string, MessageReaction>): Promise<void> => {
+        const repo = getRepository(AggroNewsItem);
 
-        let m = reaction.message;
+        const m = reaction.message;
         collector.stop();
         delete this.reactionCollectors[m.id];
-        let post = this.verifyMessageIdToPost[m.id];
+        const post = this.verifyMessageIdToPost[m.id];
 
         if(!post.forumsEditPostId) {
             throw new Error("No forum edit post found!");
         }
 
         if (reaction.emoji.name == "✅") {
-            let res = await client.forum.getThread(post.forumsEditPostId);
-            let forumPost = await client.forum.getPost(res.thread.first_post_id);
+            const res = await client.forum.getThread(post.forumsEditPostId);
+            const forumPost = await client.forum.getPost(res.thread.first_post_id);
 
             if (!post.forumsNewsPostId) {
-                let newThread = await client.forum.createThread(FORUMS_NEWS_ID, res.thread.title, forumPost.message);
+                const newThread = await client.forum.createThread(FORUMS_NEWS_ID, res.thread.title, forumPost.message);
                 post.forumsNewsPostId = newThread.thread.thread_id;
             } else {
-                let curThread = await client.forum.editThread(post.forumsNewsPostId, {
+                const curThread = await client.forum.editThread(post.forumsNewsPostId, {
                     title: res.thread.title
                 });
 
@@ -205,27 +205,27 @@ export class NewsAggregator {
 
         await client.forum.deleteThread(post.forumsEditPostId);
         await repo.update({ newsId: post.newsId, feedName: post.feedName }, { 
-                editMessageId: undefined, 
-                forumsEditPostId: undefined, 
-                forumsNewsPostId: post.forumsNewsPostId });
+            editMessageId: undefined, 
+            forumsEditPostId: undefined, 
+            forumsNewsPostId: post.forumsNewsPostId });
         await reaction.message.delete();
         delete this.verifyMessageIdToPost[m.id];
     };
 
-    async deleteCacheMessage(messageId: string) {
+    async deleteCacheMessage(messageId: string): Promise<void> {
         if(!this.aggregateChannelID)
             return;
-        let ch = client.bot.channels.resolve(this.aggregateChannelID);
+        const ch = client.bot.channels.resolve(this.aggregateChannelID);
         if (!(ch instanceof TextChannel))
             return;
 
-        let msg = await this.tryFetchMessage(ch, messageId);
+        const msg = await this.tryFetchMessage(ch, messageId);
 
         if (msg)
             await msg.delete();
     }
 
-    async tryFetchMessage(channel: Channel, messageId: string) {
+    async tryFetchMessage(channel: Channel, messageId: string): Promise<Message | null> {
         try {
             if (!(channel instanceof TextChannel))
                 return null;
@@ -235,20 +235,20 @@ export class NewsAggregator {
         }
     }
 
-    initAggregators() {
-        let aggregatorsPath = path.join(path.dirname(module.filename), "aggregators");
-        let files = fs.readdirSync(aggregatorsPath);
+    initAggregators(): void {
+        const aggregatorsPath = path.join(path.dirname(module.filename), "aggregators");
+        const files = fs.readdirSync(aggregatorsPath);
 
-        for (let file of files) {
-            let ext = path.extname(file);
-            let name = path.basename(file);
+        for (const file of files) {
+            const ext = path.extname(file);
+            const name = path.basename(file);
 
             if (name == "aggregator.js")
                 continue;
             if (ext != ".js")
                 continue;
 
-            let obj = require(path.resolve(aggregatorsPath, file)).default as IAggregator;
+            const obj = require(path.resolve(aggregatorsPath, file)).default as IAggregator;
 
             if (obj)
                 this.aggregators.push(obj);
@@ -258,39 +258,40 @@ export class NewsAggregator {
         }
     }
 
-    async initPendingReactors() {
+    async initPendingReactors(): Promise<void> {
         if(!this.aggregateChannelID)
             return;
 
-        let verifyChannel = client.bot.channels.resolve(this.aggregateChannelID);
+        const verifyChannel = client.bot.channels.resolve(this.aggregateChannelID);
         if(!verifyChannel)
             throw new Error("Couldn't find verify channel!");
 
-        let repo = getRepository(AggroNewsItem);
+        const repo = getRepository(AggroNewsItem);
 
-        let pendingVerifyMessages = await repo.find({
+        const pendingVerifyMessages = await repo.find({
             where: { editMessageId: Not(IsNull()) }
         });
 
-        for (let msg of pendingVerifyMessages) {
-            let m = await this.tryFetchMessage(verifyChannel, msg.editMessageId!);
+        for (const msg of pendingVerifyMessages) {
+            // eslint-disable-next-line @typescript-eslint/no-non-null-assertion
+            const m = await this.tryFetchMessage(verifyChannel, msg.editMessageId!);
 
             if (!m) {
                 await repo.update({ feedName: msg.feedName, newsId: msg.newsId }, { editMessageId: undefined });
                 continue;
             }
 
-            let collector = m.createReactionCollector(this.isVerifyReaction, { maxEmojis: 1 });
+            const collector = m.createReactionCollector(this.isVerifyReaction, { maxEmojis: 1 });
             collector.on("collect", this.collectReaction);
             this.reactionCollectors[m.id] = collector;
             this.verifyMessageIdToPost[m.id] = msg;
         }
     }
 
-    async onStart() {
-        let repo = getRepository(KnownChannel);
+    async onStart(): Promise<void> {
+        const repo = getRepository(KnownChannel);
 
-        let ch = await repo.findOne({
+        const ch = await repo.findOne({
             where: { channelType: AGGREGATOR_MANAGER_CHANNEL }
         });
 

+ 20 - 20
bot/src/commands/quote.ts

@@ -9,7 +9,7 @@ const quotePattern = /add quote by "([^"]+)"\s*(.*)/i;
 @CommandSet
 export class QuoteCommand {
 
-    minify(str: string, maxLength: number) {
+    minify(str: string, maxLength: number): string {
         let result = str.replace("\n", "");
         if (result.length > maxLength)
             result = `${result.substring(0, maxLength - 3)}...`;
@@ -17,21 +17,21 @@ export class QuoteCommand {
     }
 
     @Command({ pattern: "add quote", auth: true, documentation: {description: "Adds a quote", example: "add quote by \"<NAME>\" <NEWLINE> <QUOTE>"} })
-    async addQuote(msg: Message, c: string) {
+    async addQuote(msg: Message, c: string): Promise<void> {
         if (!isAuthorisedAsync(msg.member))
             return;
 
-        let result = quotePattern.exec(c);
+        const result = quotePattern.exec(c);
 
         if (result == null)
             return;
 
-        let author = result[1].trim();
-        let message = result[2].trim();
+        const author = result[1].trim();
+        const message = result[2].trim();
 
-        let repo = getRepository(Quote);
+        const repo = getRepository(Quote);
 
-        let newQuote = await repo.save(repo.create({
+        const newQuote = await repo.save(repo.create({
             author: author,
             message: message
         }));
@@ -40,10 +40,10 @@ export class QuoteCommand {
     }
 
     @Command({ pattern: "random quote", documentation: {description: "Shows a random quote by someone special...", example: "random quote"} })
-    async postRandomQuote(msg: Message) {
-        let repo = getRepository(Quote);
+    async postRandomQuote(msg: Message): Promise<void> {
+        const repo = getRepository(Quote);
 
-        let quotes = await repo.query(`  select *
+        const quotes = await repo.query(`  select *
                                                 from quote
                                                 order by random()
                                                 limit 1`) as Quote[];
@@ -53,20 +53,20 @@ export class QuoteCommand {
             return;
         }
 
-        let quote = quotes[0];
+        const quote = quotes[0];
         msg.channel.send(`Quote #${quote.id}:\n*"${quote.message}"*\n- ${quote.author}`);
     }
 
     @Command({ pattern: "remove quote", auth: true, documentation: {description: "Removes quote. Use \"quotes\" to get the <quote_index>!", example: "remove quote <quote_index>"} })
-    async removeQuote(msg: Message, c: string) {
-        let quoteNum = c.substring("remove quote".length).trim();
-        let val = parseInt(quoteNum);
+    async removeQuote(msg: Message, c: string): Promise<void> {
+        const quoteNum = c.substring("remove quote".length).trim();
+        const val = parseInt(quoteNum);
         if (isNaN(val))
             return;
 
-        let repo = getRepository(Quote);
+        const repo = getRepository(Quote);
 
-        let res = await repo.delete({ id: val });
+        const res = await repo.delete({ id: val });
         if (res.affected == 0)
             return;
 
@@ -74,22 +74,22 @@ export class QuoteCommand {
     }
 
     @Command({ pattern: "quotes", documentation: {description: "Lists all known quotes.", example: "quotes"}, auth: true })
-    async listQuotes(msg: Message) {
+    async listQuotes(msg: Message): Promise<void> {
         if (!isAuthorisedAsync(msg.member)) {
             msg.channel.send(`${msg.author.toString()} To prevent spamming, only bot moderators can view all quotes!`);
             return;
         }
 
-        let repo = getRepository(Quote);
+        const repo = getRepository(Quote);
 
-        let quotes = await repo.find();
+        const quotes = await repo.find();
 
         if (quotes.length == 0) {
             msg.channel.send("I have no quotes!");
             return;
         }
 
-        let quotesListing = quotes.reduce((p, c) => `${p}[${c.id}] "${this.minify(c.message, 10)}" by ${c.author}\n`, "\n");
+        const quotesListing = quotes.reduce((p, c) => `${p}[${c.id}] "${this.minify(c.message, 10)}" by ${c.author}\n`, "\n");
         msg.channel.send(`${msg.author.toString()}I know the following quotes:\n\`\`\`${quotesListing}\`\`\``);
     }
 }

+ 4 - 4
bot/src/commands/random_react.ts

@@ -9,18 +9,18 @@ const timeout = (ms: number) => new Promise(r => setTimeout(r, ms));
 @CommandSet
 export class RandomReact {
     @Action(ActionType.MESSAGE)
-    async showHelp(actionsDone: boolean, msg: Message) {
+    async showHelp(actionsDone: boolean, msg: Message): Promise<boolean> {
         if(actionsDone)
             return false;
 
-        let repo = getRepository(RandomMessageReaction);
+        const repo = getRepository(RandomMessageReaction);
 
-        let reactInfo = await repo.findOne({ where: { userId: msg.author.id } });
+        const reactInfo = await repo.findOne({ where: { userId: msg.author.id } });
 
         if(!reactInfo)
             return false;
 
-        let emote = client.bot.emojis.resolve(reactInfo.reactionEmoteId);
+        const emote = client.bot.emojis.resolve(reactInfo.reactionEmoteId);
 
         if(!emote)
             return false;

+ 4 - 4
bot/src/commands/rcg.ts

@@ -11,10 +11,10 @@ export class Rcg {
         auth: false,
         documentation: {description: "Generates a comic just for you!", example: "random comic"}
     })
-    async randomComic(msg: Message) {
-        let result = await request("http://explosm.net/rcg/view/?promo=false");
+    async randomComic(msg: Message): Promise<void> {
+        const result = await request("http://explosm.net/rcg/view/?promo=false");
     
-        let regexResult = rcgRe.exec(result);
+        const regexResult = rcgRe.exec(result);
 
         if(!regexResult)
             return;
@@ -23,4 +23,4 @@ export class Rcg {
             files: [ regexResult[1].trim() ]
         });
     }
-};
+}

+ 36 - 36
bot/src/commands/react.ts

@@ -7,12 +7,12 @@ import { isAuthorisedAsync } from "../util";
 import { CommandSet, Command, Action, ActionType } from "src/model/command";
 import { Message } from "discord.js";
 
-const pattern = /^react to\s+"([^"]+)"\s+with\s+\<:[^:]+:([^\>]+)\>$/i;
+const pattern = /^react to\s+"([^"]+)"\s+with\s+<:[^:]+:([^>]+)>$/i;
 
 async function getRandomEmotes(allowedTypes: ReactionType[], limit: number) {
-    let reactionEmotesRepo = getRepository(ReactionEmote);
+    const reactionEmotesRepo = getRepository(ReactionEmote);
 
-    let a = await reactionEmotesRepo.query(`
+    const a = await reactionEmotesRepo.query(`
         select distinct on (type) type, "reactionId"
         from
                 (   select *
@@ -29,23 +29,23 @@ async function getRandomEmotes(allowedTypes: ReactionType[], limit: number) {
 export class ReactCommands {
 
     @Command({ pattern: "react to", auth: true, documentation: {description: "React to <message> with <emote>.", example: "react to \"<message>\" with <emote>"} })
-    async addReaction(msg: Message, s: string) {
+    async addReaction(msg: Message, s: string): Promise<void> {
         if (!await isAuthorisedAsync(msg.member))
             return;
-        let contents = pattern.exec(s);
+        const contents = pattern.exec(s);
 
         if (contents != null) {
-            let reactable = contents[1].trim().toLowerCase();
-            let reactionEmoji = contents[2];
+            const reactable = contents[1].trim().toLowerCase();
+            const reactionEmoji = contents[2];
 
             if (!client.bot.emojis.cache.has(reactionEmoji)) {
                 msg.channel.send(`${msg.author.toString()} I cannot react with this emoji :(`);
                 return;
             }
 
-            let repo = getRepository(MessageReaction);
+            const repo = getRepository(MessageReaction);
 
-            let message = repo.create({
+            const message = repo.create({
                 message: reactable,
                 reactionEmoteId: reactionEmoji
             });
@@ -56,13 +56,13 @@ export class ReactCommands {
     }
 
     @Command({ pattern: "remove reaction to", auth: true, documentation: {description: "Stops reacting to <message>.", example: "remove reaction to <message>"} })
-    async removeReaction(msg: Message, s: string) {
+    async removeReaction(msg: Message, s: string): Promise<void> {
         if (!await isAuthorisedAsync(msg.member))
             return;
 
-        let content = s.substring("remove reaction to ".length).trim().toLowerCase();
-        let repo = getRepository(MessageReaction);
-        let result = await repo.delete({ message: content });
+        const content = s.substring("remove reaction to ".length).trim().toLowerCase();
+        const repo = getRepository(MessageReaction);
+        const result = await repo.delete({ message: content });
 
         if (result.affected == 0) {
             msg.channel.send(`${msg.author.toString()} No such reaction available!`);
@@ -72,31 +72,31 @@ export class ReactCommands {
     }
 
     @Command({ pattern: "reactions", documentation: {description: "Lists all known messages this bot can react to.", example: "reactions"} })
-    async listReactions(msg: Message) {
-        let reactionsRepo = getRepository(MessageReaction);
+    async listReactions(msg: Message): Promise<void> {
+        const reactionsRepo = getRepository(MessageReaction);
 
-        let messages = await reactionsRepo.find({
+        const messages = await reactionsRepo.find({
             select: ["message"]
         });
 
-        let reactions = messages.reduce((p, c) => `${p}\n${c.message}`, "");
+        const reactions = messages.reduce((p, c) => `${p}\n${c.message}`, "");
         msg.channel.send(`I'll react to the following messages:\n\`\`\`${reactions}\n\`\`\``);
     }
 
     @Action(ActionType.MESSAGE)
-    async reactToMentions(actionsDone: boolean, msg: Message, content: string) {
+    async reactToMentions(actionsDone: boolean, msg: Message, content: string): Promise<boolean> {
         if (actionsDone)
             return false;
 
-        let lowerContent = content.toLowerCase();
+        const lowerContent = content.toLowerCase();
 
-        let reactionRepo = getRepository(MessageReaction);
-        let usersRepo = getRepository(KnownUser);
+        const reactionRepo = getRepository(MessageReaction);
+        const usersRepo = getRepository(KnownUser);
 
-        let message = await reactionRepo.findOne({ message: lowerContent });
+        const message = await reactionRepo.findOne({ message: lowerContent });
 
         if (message) {
-            let emoji = client.bot.emojis.resolve(message.reactionEmoteId);
+            const emoji = client.bot.emojis.resolve(message.reactionEmoteId);
             if (emoji)
                 msg.react(emoji);
             return true;
@@ -105,7 +105,7 @@ export class ReactCommands {
         if (msg.mentions.users.size == 0)
             return false;
 
-        let knownUsers = await usersRepo.find({
+        const knownUsers = await usersRepo.find({
             select: ["mentionReactionType"],
             where: [...msg.mentions.users.map(u => ({ userID: u.id }))]
         });
@@ -113,9 +113,9 @@ export class ReactCommands {
         if (knownUsers.length == 0)
             return false;
 
-        let reactionEmoteTypes = new Set<ReactionType>();
+        const reactionEmoteTypes = new Set<ReactionType>();
 
-        for (let user of knownUsers) {
+        for (const user of knownUsers) {
             if (user.mentionReactionType == ReactionType.NONE)
                 continue;
             reactionEmoteTypes.add(user.mentionReactionType);
@@ -124,13 +124,13 @@ export class ReactCommands {
         if(reactionEmoteTypes.size == 0)
             return false;
 
-        let randomEmotes = await getRandomEmotes([...reactionEmoteTypes], 5);
+        const randomEmotes = await getRandomEmotes([...reactionEmoteTypes], 5);
 
         if (randomEmotes.length == 0)
             return false;
 
-        for (let emote of randomEmotes) {
-            let emoji = client.bot.emojis.resolve(emote.reactionId);
+        for (const emote of randomEmotes) {
+            const emoji = client.bot.emojis.resolve(emote.reactionId);
             if(emoji)
                 await msg.react(emoji);
         }
@@ -139,14 +139,14 @@ export class ReactCommands {
     }
 
     @Action(ActionType.INDIRECT_MENTION)
-    async reactToPing(actionsDone: boolean, msg: Message) {
+    async reactToPing(actionsDone: boolean, msg: Message): Promise<boolean> {
         if (actionsDone)
             return false;
         let emoteType = ReactionType.ANGERY;
 
-        let repo = getRepository(KnownUser);
+        const repo = getRepository(KnownUser);
 
-        let knownUser = await repo.findOne({
+        const knownUser = await repo.findOne({
             select: ["replyReactionType"],
             where: [{
                 userID: msg.author.id
@@ -159,17 +159,17 @@ export class ReactCommands {
             emoteType = knownUser.replyReactionType;
         }
 
-        let emotes = await getRandomEmotes([emoteType], 1);
+        const emotes = await getRandomEmotes([emoteType], 1);
 
         if (emotes.length != 1)
             return false;
 
-        let emote = client.bot.emojis.resolve(emotes[0].reactionId);
+        const emote = client.bot.emojis.resolve(emotes[0].reactionId);
 
         if (!emote) {
             console.log(`WARNING: Emote ${emotes[0]} no longer is valid. Deleting invalid emojis from the list...`);
 
-            let emotesRepo = getRepository(ReactionEmote);
+            const emotesRepo = getRepository(ReactionEmote);
             await emotesRepo.delete({ reactionId: emotes[0].reactionId });
             return false;
         }
@@ -177,4 +177,4 @@ export class ReactCommands {
         msg.channel.send(emote.toString());
         return true;
     }
-};
+}

+ 0 - 257
bot/src/lowdb_migrator.ts

@@ -1,257 +0,0 @@
-import { existsSync, readFileSync, renameSync } from "fs";
-import { getRepository, Repository } from "typeorm";
-import { Dictionary } from "lodash";
-import { KnownUser, User, UserRole } from "@shared/db/entity/KnownUser";
-import { ReactionType, ReactionEmote } from "@shared/db/entity/ReactionEmote";
-import { Guide, GuideKeyword, GuideType } from "@shared/db/entity/Guide";
-import { Quote } from "@shared/db/entity/Quote";
-import { MessageReaction } from "@shared/db/entity/MessageReaction";
-import { FaceCaptionMessage, FaceCaptionType } from "@shared/db/entity/FaceCaptionMessage";
-import { PostedForumNewsItem } from "@shared/db/entity/PostedForumsNewsItem";
-import { KnownChannel } from "@shared/db/entity/KnownChannel";
-import { DeadChatReply } from "@shared/db/entity/DeadChatReply";
-
-type EmoteTable = { [reactionType: string]: string[] };
-
-interface IGuide {
-    name: string;
-    displayName: string;
-    content: string;
-}
-
-interface IEditors {
-    roles: string[];
-    users: string[];
-}
-
-interface INewsItem {
-    hash: string;
-    messageId: string;
-}
-
-interface IQuote {
-    author: string;
-    message: string;
-}
-
-interface IFaceCaptionTable {
-    pre: string[];
-    post: string[];
-}
-
-type NewsTable = { [id: string] : INewsItem | boolean};
-type MessageReactionsTable = { [message: string] : string };
-type FaceEditProbabilityTable = { [channel: string] : number };
-
-interface IOldDatabase {
-    emotes: EmoteTable;
-    reactableMentionedUsers: string[];
-    specialUsers: string[];
-    bigUsers: string[];
-    dedUsers: string[];
-    editors: IEditors;
-    memes: IGuide[];
-    miscs: IGuide[];
-    guides: IGuide[];
-    quotes: IQuote[];
-    messageReactions: MessageReactionsTable;
-    faceCaptions: IFaceCaptionTable;
-    postedNewsGuids: NewsTable;
-    faceEditChannels: FaceEditProbabilityTable;
-    deadChatReplies: string[];
-    newsPostVerifyChannel: string;
-    feedOutputChannel: string;
-    aggregateChannel: string;
-    latestKissDiaryEntry: number;
-    latestCom3D2WorldDiaryEntry: number;
-    lastCOMJPVersion: number;
-}
-
-async function migrateEmotes(db: IOldDatabase) {
-    let repo = getRepository(ReactionEmote);
-
-    for (const emoteType in db.emotes) {
-        if(!(<string[]>Object.values(ReactionType)).includes(emoteType)) {
-            console.log(`WARN: emote type ${emoteType} is not a predefined emote type!`)
-            continue;
-        }
-
-        await repo.save(db.emotes[emoteType].map(id => repo.create({
-            reactionId: id,
-            type: emoteType as ReactionType
-        })));
-    }
-}
-
-async function migrateUsers(db: IOldDatabase) {
-    let userRepo = getRepository(User);
-    let roleRepo = getRepository(UserRole);
-
-    let users : Dictionary<User> = {};
-    let roles : Dictionary<UserRole> = {};
-
-    let iterator = <T extends KnownUser>(targetDict : Dictionary<T>, repo: Repository<T>, ids: string[], action: (u: T) => void) => {
-        for(let userId of ids) {
-            let u : T;
-
-            if(userId in users)
-                u = targetDict[userId];
-            else{
-                u = targetDict[userId] = repo.create();
-                u.userID = userId;
-            }
-
-            action(u);
-        }
-    };
-
-    iterator(users, userRepo, db.reactableMentionedUsers, u => u.mentionReactionType = ReactionType.ANGERY);
-    iterator(users, userRepo, db.specialUsers, u => u.replyReactionType = ReactionType.HUG);
-    iterator(users, userRepo, db.bigUsers, u => u.replyReactionType = ReactionType.BIG);
-    iterator(users, userRepo, db.dedUsers, u => u.replyReactionType = ReactionType.DED);
-
-    iterator(users, userRepo, db.editors.users, u => u.canModerate = true);
-    iterator(roles, roleRepo, db.editors.roles, r => r.canModerate = true);
-
-    await userRepo.save(Object.values(users));
-    await roleRepo.save(Object.values(roles));
-}
-
-async function migrateGuides(db : IOldDatabase) {
-    let guideRepo = getRepository(Guide);
-    let keywordsRepo = getRepository(GuideKeyword);
-
-    let keywords : Dictionary<GuideKeyword> = {};
-    let dbGuides : Guide[] = [];
-
-    let process = async (guides: IGuide[], type: GuideType) => {
-        for(let guide of guides){
-            if(!guide.displayName)
-                continue;
-
-            let guideKeywords = guide.name.split(" ").map(s => s.trim().toLowerCase());
-            let keywordsObjects : GuideKeyword[] = [];
-            
-            for(let keyword of guideKeywords) {
-                let keywordObj : GuideKeyword;
-    
-                if(keyword in keywords)
-                    keywordObj = keywords[keyword];
-                else
-                    keywordObj = keywords[keyword] = await keywordsRepo.save(keywordsRepo.create({ keyword: keyword }));
-                keywordsObjects.push(keywordObj);
-            }
-    
-            let guideObj = guideRepo.create({
-                keywords: keywordsObjects,
-                type: type,
-                displayName: guide.displayName,
-                content: guide.content
-            });
-    
-            dbGuides.push(guideObj);
-        }
-    };
-
-    await process(db.memes, GuideType.MEME);
-    await process(db.guides, GuideType.GUIDE);
-    await process(db.miscs, GuideType.MISC);
-
-    // await keywordsRepo.save(Object.values(keywords));
-    await guideRepo.save(dbGuides);
-}
-
-async function migrateQuotes(db: IOldDatabase) {
-    let repo = getRepository(Quote);
-
-    await repo.save(db.quotes.map(q => repo.create({
-        author: q.author,
-        message: q.message
-    })));
-}
-
-async function migrateMessageReactions(db: IOldDatabase) {
-    let repo = getRepository(MessageReaction);
-
-    await repo.save(Object.entries(db.messageReactions).map(([message, emoteId]) => repo.create({
-        message: message,
-        reactionEmoteId: emoteId
-    })));
-}
-
-async function migrateFaceCaptions(db: IOldDatabase) {
-    let repo = getRepository(FaceCaptionMessage);
-
-    await repo.save([
-        ...db.faceCaptions.pre.map(s => repo.create( {
-            message: s,
-            type: FaceCaptionType.PREFIX
-        })),
-        ...db.faceCaptions.post.map(s => repo.create( {
-            message: s,
-            type: FaceCaptionType.POSTFIX
-        }))
-    ]);
-}
-
-async function migrateNews(db: IOldDatabase) {
-    let repo = getRepository(PostedForumNewsItem);
-
-    await repo.save(Object.entries(db.postedNewsGuids).filter(([id, item]) => typeof item != "boolean").map(([id, item]) => repo.create({
-        hash: (item as INewsItem).hash,
-        postedMessageId: (item as INewsItem).messageId,
-        id: id
-    })));
-}
-
-async function migrateChannels(db: IOldDatabase) {
-    let repo = getRepository(KnownChannel);
-
-    await repo.save([
-        repo.create({
-            channelId: db.newsPostVerifyChannel,
-            channelType: "newsPostVerify"            
-        }),
-        repo.create({
-            channelId: db.aggregateChannel,
-            channelType: "aggregatorManager"
-        }),
-        repo.create({
-            channelId: db.feedOutputChannel,
-            channelType: "newsFeed"
-        }),
-        ...Object.entries(db.faceEditChannels).map(([channelId, prob]) => repo.create({
-            channelId: channelId,
-            faceMorphProbability: prob
-        }))
-    ]);
-}
-
-async function migrateDeadMessages(db: IOldDatabase) {
-    let repo = getRepository(DeadChatReply);
-
-    await repo.save(db.deadChatReplies.map(r => repo.create({
-        message: r
-    })));
-}
-
-export async function migrate() {
-    if(!existsSync("./db.json"))
-        return;
-
-    let json = readFileSync("./db.json", { encoding: "utf-8" });
-
-    let db = JSON.parse(json) as IOldDatabase;
-
-    await migrateEmotes(db);
-    await migrateUsers(db);
-    await migrateGuides(db);
-    await migrateQuotes(db);
-    await migrateMessageReactions(db);
-    await migrateFaceCaptions(db);
-    await migrateNews(db);
-    await migrateChannels(db);
-    await migrateDeadMessages(db);
-
-    renameSync("./db.json", "./db_migrated.json");
-}

+ 36 - 27
bot/src/main.ts

@@ -23,16 +23,15 @@ import "reflect-metadata";
 import { createConnection, getConnectionOptions } from "typeorm";
 import { getNumberEnums } from "./util";
 import { DB_ENTITIES } from "@shared/db/entities";
-import { BOT_COMMAND_DESCRIPTOR } from "./model/command";
 
 const REACT_PROBABILITY = 0.3;
 
-async function trigger(type: mCmd.ActionType, ...params: any[]) {
+async function trigger(type: mCmd.ActionType, ...params: unknown[]) {
     let actionDone = false;
-    let actions = botEvents[type];
+    const actions = botEvents[type];
     for (let i = 0; i < actions.length; i++) {
-        const action = actions[i] as (...args: any[]) => boolean | Promise<boolean>;
-        let actionResult = action(actionDone, ...params);
+        const action = actions[i] as (...args: unknown[]) => boolean | Promise<boolean>;
+        const actionResult = action(actionDone, ...params);
         if (actionResult instanceof Promise)
             actionDone = (await actionResult) || actionDone;
         else
@@ -41,19 +40,21 @@ async function trigger(type: mCmd.ActionType, ...params: any[]) {
     return actionDone;
 }
 
-let commandSets: mCmd.ICommand[] = [];
-let botCommands: mCmd.IBotCommand[] = [];
-let botEvents: { [event in mCmd.ActionType]: mCmd.BotAction[] } = getNumberEnums(mCmd.ActionType).reduce((p, c) => { p[c] = []; return p; }, {} as any);
-let startActions: Array<() => void | Promise<void>> = [];
+type BotEventCollection = { [event in mCmd.ActionType]: mCmd.BotAction[] };
 
-client.bot.on('ready', async () => {
+const commandSets: mCmd.ICommand[] = [];
+const botCommands: mCmd.IBotCommand[] = [];
+const botEvents: BotEventCollection = getNumberEnums(mCmd.ActionType).reduce((p, c) => { p[c as mCmd.ActionType] = []; return p; }, {} as BotEventCollection);
+const startActions: Array<() => void | Promise<void>> = [];
+
+client.bot.on("ready", async () => {
     console.log("Starting up NoctBot!");
     client.botUser.setActivity(process.env.NODE_ENV == "dev" ? "Maintenance" : "@NoctBot help", {
         type: "PLAYING"
     });
     for (let i = 0; i < startActions.length; i++) {
         const action = startActions[i];
-        let val = action();
+        const val = action();
         if (val instanceof Promise)
             await val;
     }
@@ -80,14 +81,14 @@ client.bot.on("message", async m => {
         if (m.content.trim().startsWith(client.botUser.id) || m.content.trim().startsWith(client.botUser.discriminator)) {
             content = content.substring(`@${client.botUser.username}`.length).trim();
 
-            let lowerCaseContent = content.toLowerCase();
-            for (let c of botCommands) {
+            const lowerCaseContent = content.toLowerCase();
+            for (const c of botCommands) {
                 if (typeof (c.pattern) == "string" && lowerCaseContent.startsWith(c.pattern)) {
                     c.action(m, content);
                     return;
                 }
                 else if (c.pattern instanceof RegExp) {
-                    let result = c.pattern.exec(content);
+                    const result = c.pattern.exec(content);
                     if (result != null) {
                         c.action(m, content, result);
                         return;
@@ -113,25 +114,25 @@ client.bot.on("messageReactionAdd", (r, u) => {
     }
 });
 
-function loadCommand(mod: any) {
-    for (let i in mod) {
-        if (!mod.hasOwnProperty(i))
+function loadCommand(mod: Record<string, unknown>) {
+    for (const i in mod) {
+        if (!Object.prototype.hasOwnProperty.call(mod, i))
             continue;
 
-        let commandClass = mod[i] as any;
+        const commandClass = mod[i] as unknown;
         // Ensure this is indeed a command class
-        if (!commandClass.prototype || commandClass.prototype.BOT_COMMAND !== BOT_COMMAND_DESCRIPTOR)
+        if (!mCmd.isCommandSet(commandClass))
             continue;
-
-        let cmd = new commandClass() as mCmd.ICommand;
+        
+        const cmd = new commandClass();
         commandSets.push(cmd);
 
         if (cmd._botCommands)
             botCommands.push(...cmd._botCommands.map(c => ({ ...c, action: c.action.bind(cmd) })));
 
         if (cmd._botEvents)
-            for (let [i, event] of Object.entries(cmd._botEvents)) {
-                botEvents[+i as mCmd.ActionType].push((event as Function).bind(cmd));
+            for (const [i, event] of Object.entries(cmd._botEvents)) {
+                botEvents[+i as mCmd.ActionType].push((event as mCmd.BotAction).bind(cmd));
             }
 
         if(cmd.onStart)
@@ -139,7 +140,14 @@ function loadCommand(mod: any) {
     }
 }
 
-export function getDocumentation() {
+interface IDocumentationData {
+    name: string;
+    doc?: string;
+    example?: string;
+    auth: boolean;
+}
+
+export function getDocumentation() : IDocumentationData[] {
     return botCommands.filter(m => m.documentation !== undefined).map(m => ({
         name: m.pattern.toString(),
         doc: m.documentation?.description,
@@ -154,14 +162,15 @@ async function main() {
         entities: DB_ENTITIES
     });
 
-    let commandsPath = path.resolve(path.dirname(module.filename), "commands");
-    let files = fs.readdirSync(commandsPath);
+    const commandsPath = path.resolve(path.dirname(module.filename), "commands");
+    const files = fs.readdirSync(commandsPath);
 
     for (const file of files) {
-        let ext = path.extname(file);
+        const ext = path.extname(file);
         if (ext != ".js")
             continue;
 
+        // eslint-disable-next-line @typescript-eslint/no-var-requires
         loadCommand(require(path.resolve(commandsPath, file)));
     }
 

+ 26 - 16
bot/src/model/command.ts

@@ -1,4 +1,5 @@
 import { Message } from "discord.js";
+import { EsModuleClass, isModuleClass } from "src/util";
 
 export interface CommandDocumentation {
     description: string;
@@ -9,23 +10,24 @@ export interface CommandOptions {
     pattern: string | RegExp;
     documentation?: CommandDocumentation;
     auth?: boolean;
-};
+}
 
 export type BotAction = (actionsDone: boolean, m : Message, content: string) => boolean | Promise<boolean>;
+export type BotMessageCommand = (message: Message, strippedContents: string, matches?: RegExpMatchArray) => void;
 
 export interface ICommand {
     onStart?(): void | Promise<void>;
     _botCommands?: IBotCommand[];
     _botEvents?: { [action in ActionType]?: BotAction };
     BOT_COMMAND?: string;
-};
+}
 
 export interface IBotCommand extends CommandOptions {
-    action(message: Message, strippedContents: string, matches?: RegExpMatchArray) : void;
-};
+    action : BotMessageCommand;
+}
 
 export const BOT_COMMAND_DESCRIPTOR = "BOT_COMMAND";
-export function CommandSet<T extends {new(...params: any[]): {}}>(base: T) {
+export function CommandSet<T extends {new(...params: unknown[]): unknown}>(base: T): void {
     base.prototype.BOT_COMMAND = BOT_COMMAND_DESCRIPTOR;
 }
 
@@ -35,23 +37,31 @@ export enum ActionType {
     DIRECT_MENTION,
     POST_MESSAGE
 }
-export function Action(type: ActionType) {
-    return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
-        if(!target._botEvents)
-            target._botEvents= {};
+export function Action(type: ActionType): MethodDecorator {
+    return function<T>(target: unknown, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>): void {
+        const command = target as ICommand;
+        if(!command._botEvents)
+            command._botEvents= {};
 
-        target._botEvents[type] = descriptor.value;
+        command._botEvents[type] = descriptor.value as BotAction | undefined;
     };
 }
 
-export function Command(opts: CommandOptions) {
-    return function(target: any, propertyKey: string, descriptor: PropertyDescriptor) {
-        if(!target._botCommands)
-            target._botCommands = [];
+export function Command(opts: CommandOptions) : MethodDecorator {
+    return function<T>(target: unknown, propertyKey: string | symbol, descriptor: TypedPropertyDescriptor<T>): void {
+        if(!descriptor.value)
+            throw new Error("The decorator value must be initialized!");
+        const command = target as ICommand;
+        if(!command._botCommands)
+            command._botCommands = [];
 
-        target._botCommands.push(<IBotCommand>{
-            action: descriptor.value,
+        command._botCommands.push({
+            action: descriptor.value as unknown as BotMessageCommand,
             ...opts
         });
     };
+}
+
+export function isCommandSet(obj: unknown): obj is EsModuleClass<ICommand> {
+    return isModuleClass<ICommand>(obj) && obj.prototype.BOT_COMMAND == BOT_COMMAND_DESCRIPTOR;
 }

+ 0 - 52
bot/src/rpc_service.ts.bak

@@ -1,52 +0,0 @@
-import { createServerRouter } from "typescript-rest-rpc/lib/server";
-import Koa from "koa";
-import KoaRouter from "koa-router";
-import koaBody from "koa-body";
-import { Backend, UserInfo } from "@shared/rpc/backend";
-import { getRepository } from "typeorm";
-import { KnownUser } from "@shared/db/entity/KnownUser";
-import { client } from "./client";
-import { GuildMember } from "discord.js";
-import { isAuthorisedAsync } from "./util";
-
-class BackendImpl implements Backend {
-    async getModeratorUserInfo({ id }: { id: string }): Promise<UserInfo> {
-        let repo = getRepository(KnownUser);
-        
-        let member: GuildMember;
-        for(let [_, guild] of client.guilds) {
-            try {
-                member = await guild.fetchMember(id);
-                break;
-            } catch(e) {
-                member = null;
-            }
-        }
-
-        if(!member || !(await isAuthorisedAsync(member)))
-            throw new Error("User not authorised!");
-
-        return <UserInfo>{
-            userId: member.user.id,
-            username: member.user.tag,
-            avatarURL: member.user.displayAvatarURL,
-            guildId: member.guild.id,
-            timestamp: new Date()
-        };
-    }
-}
-
-const PORT = 3010;
-let app : Koa;
-let router : KoaRouter;
-export function startServer() {
-    app = new Koa();
-    app.use(koaBody({ multipart: true }));
-
-    router = createServerRouter("/rpc", new BackendImpl());
-    app.use(router.routes());
-    app.use(router.allowedMethods());
-
-    app.listen(PORT);
-    console.log(`Started RPC service at ${PORT}`);
-}

+ 18 - 13
bot/src/util.ts

@@ -9,27 +9,32 @@ const VALID_EXTENSIONS = new Set([
     "bmp",
 ]);
 
-export function isDevelopment() {
+export type EsModuleClass<T> = { prototype: T; new(...params: unknown[]): T; };
+export function isModuleClass<T>(obj: unknown) : obj is EsModuleClass<T> {
+    return Object.prototype.hasOwnProperty.call(obj, "prototype");
+}
+
+export function isDevelopment(): boolean {
     return process.env.NODE_ENV == "dev";
 }
 
-export function isValidImage(fileName?: string) {
+export function isValidImage(fileName?: string): boolean {
     if (!fileName)
         return false;
-    let extPosition = fileName.lastIndexOf(".");
+    const extPosition = fileName.lastIndexOf(".");
     if (extPosition < 0)
         return false;
-    let ext = fileName.substring(extPosition + 1).toLowerCase();
+    const ext = fileName.substring(extPosition + 1).toLowerCase();
     return VALID_EXTENSIONS.has(ext);
 }
 
-export async function isAuthorisedAsync(member: GuildMember | null | undefined) {
+export async function isAuthorisedAsync(member: GuildMember | null | undefined): Promise<boolean> {
     if (!member)
         return false;
 
-    let repo = getRepository(KnownUser);
+    const repo = getRepository(KnownUser);
 
-    let user = await repo.findOne({
+    const user = await repo.findOne({
         where: { userID: member.id },
         select: ["canModerate"]
     });
@@ -37,7 +42,7 @@ export async function isAuthorisedAsync(member: GuildMember | null | undefined)
     if (user && user.canModerate)
         return true;
 
-    let role = await repo.findOne({
+    const role = await repo.findOne({
         select: ["userID"],
         where: {
             userID: In(member.roles.cache.keyArray()),
@@ -50,9 +55,9 @@ export async function isAuthorisedAsync(member: GuildMember | null | undefined)
 }
 
 export function compareNumbers<T>(prop: (o: T) => number) {
-    return (a: T, b: T) => {
-        let ap = prop(a);
-        let bp = prop(b);
+    return (a: T, b: T): -1 | 0 | 1 => {
+        const ap = prop(a);
+        const bp = prop(b);
 
         if (ap < bp)
             return 1;
@@ -64,6 +69,6 @@ export function compareNumbers<T>(prop: (o: T) => number) {
 
 export type Dict<TVal> = { [key: string]: TVal };
 
-export function getNumberEnums(e: any): number[] {
-    return Object.keys(e).filter(k => typeof e[k as any] === "number").map(k => e[k as any]);
+export function getNumberEnums<E>(e : Record<keyof E, number>) : number[] {
+    return Object.keys(e).filter(k => typeof e[k as keyof E] === "number").map(k => e[k as keyof E]);
 }

+ 39 - 39
bot/src/xenforo.ts

@@ -11,7 +11,7 @@ enum ReqMethod {
 export interface RequestError {
     code: string;
     message: string;
-    params: { key: string; value: any; }[];
+    params: { key: string; value: unknown; }[];
 }
 
 export type RequestErrorSet = { errors: RequestError[] };
@@ -21,54 +21,54 @@ export class XenforoClient {
     constructor(private endpoint: string, private userKey: string) {
     }
 
-    private async makeRequest<T>(uri: string, method: ReqMethod, data?: any) {
-        let result = await request(`${this.endpoint}/${uri}`, {
+    private async makeRequest<TResult, TData>(uri: string, method: ReqMethod, data?: TData): Promise<TResult> {
+        const result = await request(`${this.endpoint}/${uri}`, {
             method: method,
             headers: {
                 "XF-Api-Key": this.userKey
             },
-            form: data || undefined,
+            form: data,
             resolveWithFullResponse: true
         }) as Response;
 
         if (result.statusCode != 200) {
             throw await JSON.parse(result.body) as RequestErrorSet;
         } else {
-            return await JSON.parse(result.body) as T;
+            return await JSON.parse(result.body) as TResult;
         }
     }
 
-    async getMe() {
-        let { me } = await this.makeRequest<{me: User}>(`me/`, ReqMethod.GET);
+    async getMe(): Promise<User> {
+        const { me }: {me: User} = await this.makeRequest("me/", ReqMethod.GET);
         return me;
     }
 
-    async postReply(thread_id: number, message: string, attachment_key?: string) {
-        return await this.makeRequest<void>(`posts/`, ReqMethod.POST, {
+    async postReply(thread_id: number, message: string, attachment_key?: string): Promise<void> {
+        return await this.makeRequest("posts/", ReqMethod.POST, {
             thread_id,
             message,
             attachment_key
         });
     }
 
-    async editThread(id: number, opts?: EditThreadOptions) {
-        return await this.makeRequest<CreateThreadResponse>(`threads/${id}`, ReqMethod.POST, opts || {});
+    async editThread(id: number, opts?: EditThreadOptions): Promise<CreateThreadResponse> {
+        return await this.makeRequest(`threads/${id}`, ReqMethod.POST, opts);
     }
 
-    async editPost(id: number, opts?: EditPostOptions) {
-        return await this.makeRequest<EditPostResponse>(`posts/${id}`, ReqMethod.POST, opts || {});
+    async editPost(id: number, opts?: EditPostOptions): Promise<EditPostResponse> {
+        return await this.makeRequest(`posts/${id}`, ReqMethod.POST, opts);
     }
 
-    async getThread(id: number, opts?: GetThreadOptions) {
-        return await this.makeRequest<GetThreadResponse>(`threads/${id}`, ReqMethod.GET, opts || {});
+    async getThread(id: number, opts?: GetThreadOptions): Promise<GetThreadResponse> {
+        return await this.makeRequest(`threads/${id}`, ReqMethod.GET, opts);
     }
 
-    async deleteThread(id: number, opts?: DeleteThreadOptions) {
-        return await this.makeRequest<SuccessResponse>(`threads/${id}`, ReqMethod.DELETE, opts || {});
+    async deleteThread(id: number, opts?: DeleteThreadOptions): Promise<SuccessResponse> {
+        return await this.makeRequest(`threads/${id}`, ReqMethod.DELETE, opts);
     }
 
-    async createThread(forumId: number, title: string, message: string, opts?: CreateThreadOptions) {
-        return await this.makeRequest<CreateThreadResponse>(`threads/`, ReqMethod.POST, {
+    async createThread(forumId: number, title: string, message: string, opts?: CreateThreadOptions): Promise<CreateThreadResponse> {
+        return await this.makeRequest("threads/", ReqMethod.POST, {
             node_id: forumId,
             title: title,
             message: message,
@@ -76,13 +76,13 @@ export class XenforoClient {
         });
     }
 
-    async getPost(id: number) {
-        let post = await this.makeRequest<{post: Post}>(`posts/${id}`, ReqMethod.GET);
-        return post.post;
+    async getPost(id: number): Promise<Post> {
+        const { post }: {post: Post} = await this.makeRequest(`posts/${id}`, ReqMethod.GET);
+        return post;
     }
 
-    async getForumThreads(id: number) {
-        return await this.makeRequest<GetForumThreadsResponse>(`forums/${id}/threads`, ReqMethod.GET);
+    async getForumThreads(id: number): Promise<GetForumThreadsResponse> {
+        return await this.makeRequest(`forums/${id}/threads`, ReqMethod.GET);
     }
 }
 
@@ -114,8 +114,8 @@ interface EditThreadOptions {
     discussion_open?: boolean;
     sticky?: boolean;
     custom_fields?: Dict<string>;
-    add_tags?: any[];
-    remove_tags?: any[];
+    add_tags?: unknown[];
+    remove_tags?: unknown[];
 }
 
 interface EditPostOptions {
@@ -132,7 +132,7 @@ interface EditPostOptions {
 type GetThreadResponse = {
     thread: Thread;
     messages: Post[];
-    pagination: any;
+    pagination: unknown;
 };
 
 type SuccessResponse = {
@@ -145,7 +145,7 @@ type CreateThreadResponse = SuccessResponse & { thread: Thread; };
 
 type GetForumThreadsResponse = {
     threads: Thread[];
-    pagination: object;
+    pagination: unknown;
     sticky: Thread[];
 };
 //#endregion
@@ -162,13 +162,13 @@ export interface User {
     about?: string;
     activity_visible?: boolean;
     age?: number;
-    alert_optout?: any[];
+    alert_optout?: unknown[];
     allow_post_profile?: string;
     allow_receive_news_feed?: string;
     allow_send_personal_conversation?: string;
     allow_view_identities: string;
     allow_view_profile?: string;
-    avatar_urls: object;
+    avatar_urls: unknown;
     can_ban: boolean;
     can_converse: boolean;
     can_edit: boolean;
@@ -180,9 +180,9 @@ export interface User {
     can_warn: boolean;
     content_show_signature?: boolean;
     creation_watch_state?: string;
-    custom_fields?: object;
+    custom_fields?: unknown;
     custom_title?: string;
-    dob?: object;
+    dob?: unknown;
     email?: string;
     email_on_conversation?: boolean;
     gravatar?: string;
@@ -197,14 +197,14 @@ export interface User {
     last_activity?: number;
     location: string;
     push_on_conversation?: boolean;
-    push_optout?: any[];
+    push_optout?: unknown[];
     receive_admin_email?: boolean;
-    secondary_group_ids?: any[];
+    secondary_group_ids?: unknown[];
     show_dob_date?: boolean;
     show_dob_year?: boolean;
     signature: string;
     timezone?: string;
-    use_tfa?: any[];
+    use_tfa?: unknown[];
     user_group_id?: number;
     user_state?: string;
     user_title: string;
@@ -221,8 +221,8 @@ export interface User {
 }
 
 export interface Node {
-    breadcrumbs: any[];
-    type_data: object;
+    breadcrumbs: unknown[];
+    type_data: unknown;
     node_id: number;
     title: string;
     node_name: string;
@@ -237,8 +237,8 @@ export interface Thread {
     username: string;
     is_watching?: boolean;
     visitor_post_count?: number;
-    custom_fields: object;
-    tags: any[];
+    custom_fields: unknown;
+    tags: unknown[];
     prefix?: string;
     can_edit: boolean;
     can_edit_tags: boolean;