From c087654979a6585dfffd3ff0649be721060cb739 Mon Sep 17 00:00:00 2001 From: Dragory Date: Mon, 9 Jul 2018 02:48:36 +0300 Subject: [PATCH] Add Logs plugin and GuildServerLogs class to communicate with it --- src/data/GuildServerLogs.ts | 26 +++++++++++ src/data/LogType.ts | 34 ++++++++++++++ src/index.ts | 4 +- src/plugins/Logs.ts | 91 +++++++++++++++++++++++++++++++++++++ src/utils.ts | 36 ++++++++++++++- 5 files changed, 188 insertions(+), 3 deletions(-) create mode 100644 src/data/GuildServerLogs.ts create mode 100644 src/data/LogType.ts create mode 100644 src/plugins/Logs.ts diff --git a/src/data/GuildServerLogs.ts b/src/data/GuildServerLogs.ts new file mode 100644 index 00000000..e9b50775 --- /dev/null +++ b/src/data/GuildServerLogs.ts @@ -0,0 +1,26 @@ +import * as EventEmitter from "events"; +import { LogType } from "./LogType"; + +// Use the same instance for the same guild, even if a new instance is created +const guildInstances: Map = new Map(); + +export class GuildServerLogs extends EventEmitter { + protected guildId: string; + + constructor(guildId) { + if (guildInstances.has(guildId)) { + // Return existing instance for this guild if one exists + return guildInstances.get(guildId); + } + + super(); + this.guildId = guildId; + + // Store the instance for this guild so it can be returned later if a new instance for this guild is requested + guildInstances.set(guildId, this); + } + + log(type: LogType, data: any) { + this.emit("log", { type, data }); + } +} diff --git a/src/data/LogType.ts b/src/data/LogType.ts new file mode 100644 index 00000000..9ad66124 --- /dev/null +++ b/src/data/LogType.ts @@ -0,0 +1,34 @@ +export enum LogType { + MEMBER_WARN = 1, + MEMBER_MUTE, + MEMBER_UNMUTE, + MEMBER_KICK, + MEMBER_BAN, + MEMBER_JOIN, + MEMBER_LEAVE, + MEMBER_ROLE_ADD, + MEMBER_ROLE_REMOVE, + MEMBER_NICK_CHANGE, + MEMBER_USERNAME_CHANGE, + MEMBER_ROLES_RESTORE, + + CHANNEL_CREATE, + CHANNEL_DELETE, + CHANNEL_EDIT, + + ROLE_CREATE, + ROLE_DELETE, + + MESSAGE_EDIT, + MESSAGE_DELETE, + MESSAGE_DELETE_BULK, + + VOICE_CHANNEL_JOIN, + VOICE_CHANNEL_LEAVE, + VOICE_CHANNEL_MOVE, + + COMMAND, + + SPAM_DELETE, + CENSOR +} diff --git a/src/index.ts b/src/index.ts index da380e33..2908e2ff 100644 --- a/src/index.ts +++ b/src/index.ts @@ -16,6 +16,7 @@ import { Knub, logger } from "knub"; import { BotControlPlugin } from "./plugins/BotControl"; import { ModActionsPlugin } from "./plugins/ModActions"; import { UtilityPlugin } from "./plugins/Utility"; +import { LogsPlugin } from "./plugins/Logs"; import knex from "./knex"; // Run latest database migrations @@ -26,7 +27,8 @@ knex.migrate.latest().then(() => { const bot = new Knub(client, { plugins: { utility: UtilityPlugin, - mod_actions: ModActionsPlugin + mod_actions: ModActionsPlugin, + logs: LogsPlugin }, globalPlugins: { bot_control: BotControlPlugin diff --git a/src/plugins/Logs.ts b/src/plugins/Logs.ts new file mode 100644 index 00000000..6a115d5e --- /dev/null +++ b/src/plugins/Logs.ts @@ -0,0 +1,91 @@ +import { Plugin } from "knub"; +import { GuildServerLogs } from "../data/GuildServerLogs"; +import { LogType } from "../data/LogType"; +import { TextChannel } from "eris"; +import { formatTemplateString } from "../utils"; +import * as moment from "moment-timezone"; + +interface ILogChannel { + include?: LogType[]; + exclude?: LogType[]; +} + +interface ILogChannelMap { + [channelId: string]: ILogChannel; +} + +export class LogsPlugin extends Plugin { + protected serverLogs: GuildServerLogs; + protected logListener; + + getDefaultOptions() { + return { + config: { + channels: {}, + format: { + timestamp: "HH:mm:ss", + MEMBER_WARN: + "⚠️ **{member.user.username}#{member.user.discriminator}** (`{member.id}`) was warned by {mod.user.username}#{mod.user.discriminator}", + MEMBER_MUTE: + "🔇 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) was muted by {mod.user.username}#{mod.user.discriminator}", + MEMBER_UNMUTE: + "🔉 **{member.user.username}#{member.user.discriminator}** was unmuted", + MEMBER_KICK: + "👢 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) was kicked by {mod.user.username}#{mod.user.discriminator}", + MEMBER_BAN: + "🔨 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) was banned by {mod.user.username}#{mod.user.discriminator}", + MEMBER_JOIN: + "📥 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) joined{new} (created {account_age})", + MEMBER_LEAVE: + "📤 **{member.user.username}#{member.user.discriminator}** left the server", + MEMBER_ROLE_ADD: + "🔑 **{member.user.username}#{member.user.discriminator}** role added **{role.name}** by {mod.user.username}#{mod.user.discriminator}", + MEMBER_ROLE_REMOVE: + "🔑 **{member.user.username}#{member.user.discriminator}** role removed **{role.name}** by {mod.user.username}#{mod.user.discriminator}" + } + } + }; + } + + onLoad() { + this.serverLogs = new GuildServerLogs(this.guildId); + + this.logListener = ({ type, data }) => this.log(type, data); + this.serverLogs.on("log", this.logListener); + } + + onUnload() { + this.serverLogs.removeListener("log", this.logListener); + } + + log(type, data) { + const logChannels: ILogChannelMap = this.configValue("channels"); + for (const [channelId, opts] of Object.entries(logChannels)) { + const channel = this.guild.channels.get(channelId); + if (!channel || !(channel instanceof TextChannel)) continue; + + if ( + (opts.include && opts.include.includes(type)) || + (opts.exclude && !opts.exclude.includes(type)) + ) { + const message = this.getLogMessage(type, data); + if (message) channel.createMessage(message); + } + } + } + + getLogMessage(type, data): string { + const format = this.configValue(`format.${LogType[type]}`, ""); + if (format === "") return; + + const formatted = formatTemplateString(format, data); + + const timestampFormat = this.configValue("format.timestamp"); + if (timestampFormat) { + const timestamp = moment().format(timestampFormat); + return `\`[${timestamp}]\` ${formatted}`; + } else { + return formatted; + } + } +} diff --git a/src/utils.ts b/src/utils.ts index d246e91a..f00d1a21 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -1,5 +1,4 @@ -import * as moment from "moment-timezone"; -import { ModActionType } from "./data/ModActionType"; +import at = require("lodash.at"); /** * Turns a "delay string" such as "1h30m" to milliseconds @@ -42,3 +41,36 @@ export function errorMessage(str) { export function uclower(str) { return str[0].toLowerCase() + str.slice(1); } + +export function stripObjectToScalars(obj, includedNested: string[]) { + const result = {}; + + for (const key in obj) { + if ( + obj[key] == null || + typeof obj[key] === "string" || + typeof obj[key] === "number" || + typeof obj[key] === "boolean" + ) { + result[key] = obj[key]; + } else if (typeof obj[key] === "object") { + const prefix = `${key}.`; + const nestedNested = includedNested + .filter(p => p === key || p.startsWith(prefix)) + .map(p => (p === key ? p : p.slice(prefix.length))); + + if (nestedNested.length) { + result[key] = stripObjectToScalars(obj[key], nestedNested); + } + } + } + + return result; +} + +const stringFormatRegex = /{([^{}]+?)}/g; +export function formatTemplateString(str: string, values) { + return str.replace(stringFormatRegex, (match, val) => { + return (at(values, val)[0] as string) || ""; + }); +}