// Copyright 2019-2020 Campbell Crowley. All rights reserved. // Author: Campbell Crowley (dev@campbellcrowley.com) const SubModule = require('./subModule.js'); /** * @description Manages moderator logging on guilds. * @augments SubModule * @listens Command#setLogChannel * @listens Command#logChannel */ class ModLog extends SubModule { /** * @description Creates subModule. */ constructor() { super(); /** @inheritdoc */ this.myName = 'ModLog'; /** * Guild settings for raids mapped by their guild id. * * @private * @type {object.<ModLog~Settings>} * @default */ this._settings = {}; this.save = this.save.bind(this); this.getSettings = this.getSettings.bind(this); this._commandSetLogChannel = this._commandSetLogChannel.bind(this); } /** @inheritdoc */ initialize() { this.command.on(new this.command.SingleCommand( ['setlogchannel', 'logchannel'], this._commandSetLogChannel, { validOnlyInGuild: true, defaultDisabled: true, permissions: this.Discord.PermissionsBitField.Flags.ManageRoles | this.Discord.PermissionsBitField.Flags.ManageGuild | this.Discord.PermissionsBitField.Flags.BanMembers | this.Discord.PermissionsBitField.Flags.KickMembers, })); this.client.guilds.cache.forEach((g) => { this.common.readAndParse( `${this.common.guildSaveDir}${g.id}/modLog.json`, (err, parsed) => { if (err) return; this._settings[g.id] = Settings.from(parsed); }); }); } /** @inheritdoc */ shutdown() { this.command.removeListener('setlogchannel'); } /** @inheritdoc */ save(opt) { if (!this.initialized) return; Object.entries(this._settings).forEach((obj) => { if (!obj[1]._updated) return; obj[1]._updated = false; const dir = `${this.common.guildSaveDir}${obj[0]}/`; const filename = `${dir}modLog.json`; if (opt == 'async') { this.common.mkAndWrite(filename, dir, JSON.stringify(obj[1])); } else { this.common.mkAndWriteSync(filename, dir, JSON.stringify(obj[1])); } }); } /** * @description Command to set the output logging channel. Changes to the * channel the command was run in, or toggles if ran in the same channel. * * @private * @type {commandHandler} * @param {Discord~Message} msg Message that triggered command. * @listens Command#setLogChannel * @listens Command#logChannel */ _commandSetLogChannel(msg) { if (this._settings[msg.guild.id] && this._settings[msg.guild.id].channel == msg.channel.id) { this.setLogChannel(msg.guild.id, null); this.common.reply(msg, 'Disabled Log Channel'); } else { this.setLogChannel(msg.guild.id, msg.channel.id); this.common.reply(msg, 'Set Log Channel', msg.channel.name); } } /** * @description Set the log channel for a guild. * @public * @param {string} gId ID of the guild to change the setting for. * @param {?string} cId The ID of the channel to set as the output channel. */ setLogChannel(gId, cId) { const s = this.getSettings(gId); s.channel = cId || null; } /** * @description Obtain reference to settings object for a guild. * @public * @param {string} gId The ID of the guild to fetch the settings for. * @returns {ModLog~Settings} Settings object, creates one with default * settings first if it doesn't exist. */ getSettings(gId) { if (!this._settings[gId]) this._settings[gId] = new Settings(); return this._settings[gId]; } /** * @description Fetch the human readable action string. * @private * @param {string} action The action to find the human readable format of. * @returns {string} Human readable string. */ _actionString(action) { switch (action) { case 'kick': return 'Kicked'; case 'ban': return 'Banned'; case 'mute': return 'Muted'; case 'warnAndMute': return 'Warned and Muted'; case 'smite': return 'Smited'; case 'mentionAbuse': return 'Mention Abuser'; case 'messagePurge': return 'Purged Messages'; case 'messageDelete': case 'messageBotDelete': return 'Deleted a Message'; case 'messageUpdate': case 'messageBotUpdate': return 'Edited a Message'; case 'memberJoin': return 'Joined the Server'; case 'memberLeave': return 'Left the Server'; case 'lockdown': return 'Raid Lockdown Started'; default: return `${action[0].toLocaleUpperCase()}${action.slice(1)}`; } } /** * @description Fetch the color for the given action. * @private * @param {string} action The action to lookup. * @returns {Discord~ColorResolvable} The color for the given action. */ _actionColor(action) { switch (action) { case 'kick': return 'Orange'; case 'ban': return 'Red'; case 'mute': return 'Yellow'; case 'warnAndMute': return 'Gold'; case 'smite': return 'DarkGold'; case 'mentionAbuse': return 'DarkPurple'; case 'messagePurge': return 'Blue'; case 'messageDelete': case 'messageBotDelete': return 'DarkBlue'; case 'messageUpdate': case 'messageBotUpdate': return 'DarkAqua'; case 'memberJoin': return 'Green'; case 'memberLeave': return 'Grey'; case 'lockdown': return 'DarkNavy'; default: return 'Default'; } } /** * @description Log a message in a guild. * @public * @param {Discord~Guild} guild The guild the action took place in. * @param {string} action The action that was performed. * @param {?Discord~User} [user=null] User that was affected, or null * of no user was affected. * @param {?Discord~User} [owner=null] User that performed the * action. Null is for ourself. * @param {string} [message=null] Additional information to attach to the log * message. * @param {string} [message2=null] Additional information to attach to the log * message. * @param {string} [msgs] Additional messages to attach to the log. */ output(guild, action, user, owner, message, message2, ...msgs) { const s = this._settings[guild.id]; if (!s || !s.channel) return; if (!s.check(action)) return; const channel = guild.channels.resolve(s.channel); if (!channel) return; const embed = new this.Discord.EmbedBuilder(); embed.setTitle(this._actionString(action)); embed.setColor(this._actionColor(action)); embed.setFooter({text: new Date().toString()}); if (user) { embed.setThumbnail(user.displayAvatarURL({size: 32})); embed.addFields([{name: user.tag, value: `<@${user.id}>\n${user.id}`}]); } if (owner) embed.addFields([{name: 'Moderator', value: owner.tag}]); if (message) { if (message2) { embed.addFields([{name: message, value: message2}]); } else if (!user && !owner) { embed.setDescription(message); } else { embed.addFields([{name: 'Additional', value: message}]); } } for (let i = 0; i < msgs.length; i += 2) { embed.addFields([{name: msgs[i], value: msgs[i + 1] || '\u200B'}]); } embed.setTimestamp(); channel.send({embeds: [embed]}); } } /** * @description Settings for moderation logging. * @memberof ModLog * @inner */ class Settings { /** * @description Create default settings. */ constructor() { /** * @description The channel ID of where to send log messages. * @public * @type {?string} * @default */ this.channel = null; /** * @description Should the bot log when users are kicked? * @public * @type {boolean} * @default */ this.logKicks = false; /** * @description Should the bot log when users are banned? * @public * @type {boolean} * @default */ this.logBans = false; /** * @description Should the bot log when users are muted? * @public * @type {boolean} * @default */ this.logMutes = false; /** * @description Should the bot log when users abuse mentions? * @public * @type {boolean} * @default */ this.logMentionAbuse = false; /** * @description Should the bot log when messages are purged? * @public * @type {boolean} * @default */ this.logMessagePurge = false; /** * @description Should the bot log when a user's message is deleted? * @public * @type {boolean} * @default */ this.logMessageDelete = false; /** * @description Should the bot log when a bot message is deleted? * @public * @type {boolean} * @default */ this.logMessageBotDelete = false; /** * @description Should the bot log when a user's message is edited? * @public * @type {boolean} * @default */ this.logMessageUpdate = false; /** * @description Should the bot log when a bot's message is edited? * @public * @type {boolean} * @default */ this.logMessageBotUpdate = false; /** * @description Should the bot log when a user joins the guild? * @public * @type {boolean} * @default */ this.logMemberJoin = false; /** * @description Should the bot log when a user leaves the guild? * @public * @type {boolean} * @default */ this.logMemberLeave = false; /** * @description Should the bot log when a lockdown is started? * @public * @type {boolean} * @default */ this.logRaidLockdown = false; /** * Log other actions that have not been classified. * * @public * @type {boolean} * @default */ this.logOther = false; /** * @description Have these settings been changed since our last save. * @private * @type {boolean} * @default */ this._updated = false; } /** * @description Enqueue these settings to be saved to disk. * @public */ updated() { this._updated = true; } /** * @description Check if the given action should be logged. Ignores if channel * is set. * @public * @param {string} action The action to check if should log. * @returns {boolean} True if should log. */ check(action) { switch (action) { case 'kick': return this.logKicks; case 'ban': return this.logBans; case 'mute': case 'warnAndMute': case 'smite': return this.logMutes; case 'mentionAbuse': return this.logMentionAbuse; case 'messagePurge': return this.logMessagePurge; case 'messageDelete': return this.logMessageDelete; case 'messageBotDelete': return this.logMessageBotDelete; case 'messageUpdate': return this.logMessageUpdate; case 'messageBotUpdate': return this.logMessageBotUpdate; case 'memberLeave': return this.logMemberLeave; case 'memberJoin': return this.logMemberJoin; case 'lockdown': return this.logRaidLockdown; default: return this.logOther; } } } /** * @description Create a Settings object from a Settings-like object. Similar to * copy-constructor. * @public * @static * @param {object} obj Object to create a Settings object from. * @returns {ModLog~Settings} The created object. */ Settings.from = function(obj) { const output = new Settings(); if (!obj) return output; output.channel = obj.channel || null; output.logKicks = obj.logKicks || false; output.logBans = obj.logBans || false; output.logMutes = obj.logMutes || false; output.logMentionAbuse = obj.logMentionAbuse || false; output.logMessagePurge = obj.logMessagePurge || false; output.logMessageDelete = obj.logMessageDelete || false; output.logMessageBotDelete = obj.logMessageBotDelete || false; output.logMessageUpdate = obj.logMessageUpdate || false; output.logMessageBotUpdate = obj.logMessageBotUpdate || false; output.logMemberLeave = obj.logMemberLeave || false; output.logMemberJoin = obj.logMemberJoin || false; output.logRaidLockdown = obj.logRaidLockdown || false; output.logOther = obj.logOther || false; return output; }; ModLog.Settings = Settings; module.exports = new ModLog();