Browse Source

Implement winston logging

ghorsington 3 years ago
parent
commit
783d9f51ce

+ 5 - 1
.env.template

@@ -12,4 +12,8 @@ DB_NAME=
 BOT_CLIENT_ID=
 BOT_CLIENT_SECRET=
 
-ADMIN_URL=
+ADMIN_URL=
+
+GMAIL_NAME=
+GMAIL_PASSWORD=
+ERRORS_ADDR=

+ 2 - 1
.vscode/launch.json

@@ -16,7 +16,8 @@
             ],
             "env": {
                 "NODE_ENV": "dev"
-            }
+            },
+            "outputCapture": "std"
         },
         {
             "type": "node",

+ 12 - 6
Makefile

@@ -1,3 +1,9 @@
+ifeq (dev, $(ENV))
+    dc = docker-compose -f docker-compose.yml -f docker-compose.dev.yml
+else
+    dc = docker-compose
+endif
+
 
 build_shared:
 	cd shared && npm run build
@@ -9,16 +15,16 @@ build_web: build_shared
 	cd web && npm run build
 
 build:
-	docker-compose build
+	$(dc) build
 
-start_db: build
-	docker-compose up db adminer
+start_env: build
+	$(dc) up db adminer
 
 start: build
-	docker-compose up db adminer noctbot web
+	$(dc) up db adminer noctbot web
 
 start_bot: build
-	docker-compose up db adminer noctbot
+	$(dc) up db adminer noctbot
 
 start_web: build
-	docker-compose up db adminer web
+	$(dc) up db adminer web

+ 4 - 0
bot/package.json

@@ -51,6 +51,7 @@
       "lowdb": "^1.0.0",
       "module-alias": "^2.2.2",
       "node-schedule": "^1.3.2",
+      "nodemailer": "^6.4.8",
       "opencv4nodejs": "^5.6.0",
       "pg": "^8.2.1",
       "reflect-metadata": "^0.1.13",
@@ -65,10 +66,13 @@
       "typescript": "^3.9.3",
       "typescript-rest-rpc": "^1.0.10",
       "uws": "^100.0.1",
+      "winston": "^3.2.1",
+      "winston-transport": "^4.3.0",
       "yaml": "^1.10.0"
    },
    "devDependencies": {
       "@types/node": "^14.0.5",
+      "@types/nodemailer": "^6.4.0",
       "@typescript-eslint/eslint-plugin": "^3.0.2",
       "@typescript-eslint/parser": "^3.0.2",
       "eslint": "^7.1.0",

+ 8 - 0
bot/src/client.ts

@@ -12,6 +12,14 @@ export class BotClient {
             throw new Error("No bot user detected!");
         return this.bot.user;
     }
+
+    get usernameMention(): string {
+        return `<@${this.botUser.id}>`;
+    }
+
+    get nameMention(): string {
+        return `<@!${this.botUser.id}>`;
+    }
 }
 
 export const client = new BotClient();

+ 3 - 1
bot/src/commands/aggregators/com3d2_updates.ts

@@ -4,6 +4,7 @@ import { IAggregator, INewsItem } from "./aggregator";
 import { getRepository } from "typeorm";
 import { AggroNewsItem } from "@shared/db/entity/AggroNewsItem";
 import cheerio from "cheerio";
+import { logger } from "src/logging";
 
 const updatePage = "http://com3d2.jp/update/";
 const changeLogPattern = /\[\s*([^\s\]]+)\s*\]\s*((・.*)\s+)+/gim;
@@ -41,7 +42,7 @@ async function aggregate() {
         const readme = rootNode("div.readme");
 
         if(!readme) {
-            console.log("[COM3D2 JP UPDATE] Failed to find listing!");
+            logger.error("[COM3D2 JP UPDATE] Failed to find listing!");
             return [];
         }
 
@@ -67,6 +68,7 @@ async function aggregate() {
             needsTranslation: true
         }] as INewsItem[];
     } catch(err) {
+        logger.error("Failed to parse com3d2 update site", err);
         return [];
     }
 }

+ 4 - 1
bot/src/commands/aggregators/com3d2_world.ts

@@ -4,6 +4,7 @@ import { INewsItem, IAggregator } from "./aggregator";
 import { getRepository } from "typeorm";
 import { AggroNewsItem } from "@shared/db/entity/AggroNewsItem";
 import cheerio from "cheerio";
+import { logger } from "src/logging";
 
 const kissDiaryRoot = "https://com3d2.world/r18/notices.php";
 const FEED_NAME = "com3d2-world-notices";
@@ -33,7 +34,8 @@ async function aggregate() {
         const diaryEntries = rootNode("div.frame a");
 
         if(!diaryEntries) {
-            console.log("[COM3D2 WORLD BLOG] Failed to find listing!");
+            logger.error("[COM3D2 WORLD BLOG] Failed to find listing!");
+            return [];
         }
 
         const result : INewsItem[] = [];
@@ -73,6 +75,7 @@ async function aggregate() {
         }
         return result;
     } catch(err) {
+        logger.error("Failed to process com3d2.world news", err);
         return [];
     }
 }

+ 4 - 1
bot/src/commands/aggregators/kiss_diary.ts

@@ -4,6 +4,7 @@ import { INewsItem, IAggregator } from "./aggregator";
 import { getRepository } from "typeorm";
 import { AggroNewsItem } from "@shared/db/entity/AggroNewsItem";
 import cheerio from "cheerio";
+import { logger } from "src/logging";
 
 const urlPattern = /diary\.php\?no=(\d+)/i;
 const kissDiaryRoot = "http://www.kisskiss.tv/kiss";
@@ -33,7 +34,8 @@ async function aggregate() {
         const diaryEntryNames = rootNode("table.blog_frame_top");
         
         if(diaryEntryNames.length == 0) {
-            console.log("[KISS DIARY] Failed to find listing!");
+            logger.error("[KISS DIARY] Failed to find listing!");
+            return [];
         }
 
         const diaryTexts = rootNode("div.blog_frame_middle");
@@ -76,6 +78,7 @@ async function aggregate() {
 
         return result;
     } catch(err) {
+        logger.error("Failed to parse KISS Diary news", err);
         return [];
     }
 }

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

@@ -9,6 +9,7 @@ import { getRepository } from "typeorm";
 import { FaceCaptionMessage, FaceCaptionType } from "@shared/db/entity/FaceCaptionMessage";
 import { KnownChannel } from "@shared/db/entity/KnownChannel";
 import { CommandSet, Action, ActionType, Command } from "src/model/command";
+import { logger } from "src/logging";
 
 const EMOTE_GUILD = "505333548694241281";
 
@@ -232,7 +233,7 @@ export class Facemorph {
             processor,
             `${msg.author.toString()} Nice image! I don't see anything interesting, though.`,
             `${msg.author.toString()} ${emojiText}`
-        ).catch(err => console.log(`Failed to run faceapp because ${err}`));
+        ).catch(err => logger.error(`Failed to run faceapp on message ${msg.id}`, err));
     }
 
     @Action(ActionType.MESSAGE)
@@ -257,7 +258,7 @@ export class Facemorph {
                 return false;
 
             this.processFaceSwap(msg, imageAttachment.url).catch(err =>
-                console.log(`Failed to run faceapp because ${err}`)
+                logger.error(`Failed to run faceapp on message ${msg.id}`, err)
             );
             return true;
         }
@@ -295,7 +296,7 @@ export class Facemorph {
             processor,
             `${msg.author.toString()} Nice image! I don't see anything interesting, though.`,
             `${msg.author.toString()} ${emojiText}`
-        ).catch(err => console.log(`Failed to run faceapp because ${err}`));
+        ).catch(err => logger.error(`Failed to run faceapp because ${msg.id}`, err));
 
         return true;
     }

+ 4 - 3
bot/src/commands/forums_news_checker.ts

@@ -10,6 +10,7 @@ import { KnownChannel } from "@shared/db/entity/KnownChannel";
 import { PostVerifyMessage } from "@shared/db/entity/PostVerifyMessage";
 import { render } from "../bbcode-parser/bbcode-js";
 import { CommandSet, Command } from "src/model/command";
+import { logger } from "src/logging";
 
 const PREVIEW_CHAR_LIMIT = 300;
 const NEWS_POST_VERIFY_CHANNEL = "newsPostVerify";
@@ -46,7 +47,7 @@ export class ForumsNewsChecker {
 
     checkFeeds = async (): Promise<void> => {
         try {
-            console.log(`Checking feeds on ${new Date().toISOString()}`);
+            logger.info(`Checking feeds on ${new Date().toISOString()}`);
             const forumsNewsRepo = getRepository(PostedForumNewsItem);
             const postVerifyMessageRepo = getRepository(PostVerifyMessage);
 
@@ -105,7 +106,7 @@ export class ForumsNewsChecker {
                     await this.addVerifyMessage(itemObj);
             }
         } catch (err) {
-            console.log(`Failed to check forums because ${err}`);
+            logger.error("Failed to check forums", err);
         }
     }
 
@@ -154,7 +155,7 @@ export class ForumsNewsChecker {
         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!`);
+            logger.warn(`Skipping adding item ${item.id} because no verify channel is set up!`);
             return;
         }
 

+ 3 - 2
bot/src/commands/news_aggregator.ts

@@ -13,6 +13,7 @@ import { KnownChannel } from "@shared/db/entity/KnownChannel";
 import { AggroNewsItem } from "@shared/db/entity/AggroNewsItem";
 import { v3beta1 } from "@google-cloud/translate";
 import { CommandSet } from "src/model/command";
+import { logger } from "src/logging";
 const { TranslationServiceClient } = v3beta1;
 
 const UPDATE_INTERVAL = process.env.NODE_ENV == "dev" ? 60 : 5;
@@ -43,7 +44,7 @@ export class NewsAggregator {
     }
 
     checkFeeds = async (): Promise<void> => {
-        console.log(`Aggregating feeds on ${new Date().toISOString()}`);
+        logger.info(`Aggregating feeds on ${new Date().toISOString()}`);
 
         const aggregatorJobs = [];
 
@@ -129,7 +130,7 @@ export class NewsAggregator {
                     item.contents = translatedContents.translatedText;
                 }
             } catch (err) {
-                console.log(`Failed to translate because ${err}`);
+                logger.error("Failed to translate news with Google", err);
             }
 
         item.contents = this.bbCodeParser.feed(item.contents).toString();

+ 2 - 1
bot/src/commands/react.ts

@@ -6,6 +6,7 @@ import { ReactionType, ReactionEmote } from "@shared/db/entity/ReactionEmote";
 import { isAuthorisedAsync } from "../util";
 import { CommandSet, Command, Action, ActionType } from "src/model/command";
 import { Message } from "discord.js";
+import { logger } from "src/logging";
 
 const pattern = /^react to\s+"([^"]+)"\s+with\s+<:[^:]+:([^>]+)>$/i;
 
@@ -167,7 +168,7 @@ export class ReactCommands {
         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...`);
+            logger.warn(`Emote ${emotes[0]} no longer is valid. Deleting invalid emojis from the list...`);
 
             const emotesRepo = getRepository(ReactionEmote);
             await emotesRepo.delete({ reactionId: emotes[0].reactionId });

+ 72 - 0
bot/src/logging.ts

@@ -0,0 +1,72 @@
+import winston from "winston";
+import nodemailer from "nodemailer";
+import TransportStream from "winston-transport";
+import Mail from "nodemailer/lib/mailer";
+
+export const logger = winston.createLogger({
+    level: "debug",
+    transports: [
+        new winston.transports.Console({
+            handleExceptions: true,
+            level: "debug",
+            debugStdout: true,
+            format: winston.format.combine(
+                winston.format.splat(),
+                winston.format.cli()
+            ),
+        }),
+    ]
+});
+
+process.on("unhandledRejection", (reason) => {
+    throw reason;
+});
+
+interface LogMessage {
+    message: string;       
+}
+
+class EmailTransport extends TransportStream {
+    
+
+    private mailer: Mail;
+
+    constructor(opts?: TransportStream.TransportStreamOptions) {
+        super(opts);
+        this.mailer = nodemailer.createTransport({
+            host: "smtp.gmail.com",
+            port: 465,
+            secure: true,
+            auth: {
+                user: process.env.GMAIL_NAME,
+                pass: process.env.GMAIL_PASSWORD
+            }
+        });
+    }
+
+    log(info: LogMessage, next: () => void): void {
+        setImmediate(() => {
+            this.emit("logged", info);
+        });
+
+        this.mailer.sendMail({
+            from: `${process.env.GMAIL_NAME}@gmail.com`,
+            to: process.env.ERRORS_ADDR,
+            subject: `Error at ${new Date().toISOString()}`,
+            text: `Received error data: ${info.message}`
+        }).catch(err => console.log(`Failed to send email! ${err}`));
+
+        next();
+    }
+}
+
+if (process.env.NODE_ENV == "production") {
+    logger.add(new EmailTransport({
+        level: "error",
+        handleExceptions: true,
+        format: winston.format.combine(
+            winston.format.splat(),
+            winston.format.simple()
+        )
+    }));
+}

+ 8 - 5
bot/src/main.ts

@@ -23,6 +23,7 @@ import "reflect-metadata";
 import { createConnection, getConnectionOptions } from "typeorm";
 import { getNumberEnums } from "./util";
 import { DB_ENTITIES } from "@shared/db/entities";
+import { logger } from "./logging";
 
 const REACT_PROBABILITY = 0.3;
 
@@ -48,7 +49,7 @@ const botEvents: BotEventCollection = getNumberEnums(mCmd.ActionType).reduce((p,
 const startActions: Array<() => void | Promise<void>> = [];
 
 client.bot.on("ready", async () => {
-    console.log("Starting up NoctBot!");
+    logger.info("Starting up NoctBot");
     client.botUser.setActivity(process.env.NODE_ENV == "dev" ? "Maintenance" : "@NoctBot help", {
         type: "PLAYING"
     });
@@ -58,7 +59,7 @@ client.bot.on("ready", async () => {
         if (val instanceof Promise)
             await val;
     }
-    console.log("NoctBot is ready!");
+    logger.info("NoctBot is ready");
 });
 
 client.bot.on("message", async m => {
@@ -77,8 +78,8 @@ client.bot.on("message", async m => {
         return;
 
     if (m.mentions.users.size > 0 && m.mentions.users.has(client.botUser.id)) {
-
-        if (m.content.trim().startsWith(client.botUser.id) || m.content.trim().startsWith(client.botUser.discriminator)) {
+        const trimmedContent = m.content.trim();
+        if (trimmedContent.startsWith(client.nameMention) || trimmedContent.startsWith(client.usernameMention)) {
             content = content.substring(`@${client.botUser.username}`.length).trim();
 
             const lowerCaseContent = content.toLowerCase();
@@ -109,7 +110,7 @@ client.bot.on("message", async m => {
 
 client.bot.on("messageReactionAdd", (r, u) => {
     if (Math.random() <= REACT_PROBABILITY && !u.bot) {
-        console.log(`Reacting to message ${r.message.id} because user ${u.tag} reacted to it`);
+        logger.verbose(`Reacting to message ${r.message.id} because user ${u.tag} reacted to it`);
         r.message.react(r.emoji);
     }
 });
@@ -162,6 +163,8 @@ async function main() {
         entities: DB_ENTITIES
     });
 
+    logger.error("Failed! Oh noes!");
+
     const commandsPath = path.resolve(path.dirname(module.filename), "commands");
     const files = fs.readdirSync(commandsPath);
 

+ 6 - 0
docker-compose.dev.yml

@@ -0,0 +1,6 @@
+version: '3.7'
+
+services:
+  db:
+    ports:
+      - 5432:5432

+ 1 - 5
docker-compose.yml

@@ -24,8 +24,6 @@ services:
       TYPEORM_USERNAME: ${DB_USERNAME}
       TYPEORM_PASSWORD: ${DB_PASSWORD}
       TYPEORM_DATABASE: ${DB_NAME}
-    ports:
-      - 3010:3010
 
   web:
     image: noctbot_web
@@ -56,14 +54,12 @@ services:
       POSTGRES_USER: ${DB_USERNAME}
     volumes:
       - db-data:/var/lib/postgresql/data
-    ports:
-      - 5432:5432
 
   adminer:
     image: adminer
     restart: always
     ports:
       - 3030:8080
-      
+
 volumes:
   db-data: