diff --git a/src/structure/components/commands/moderation/History.js b/src/structure/components/commands/moderation/History.js new file mode 100644 index 0000000..de75c92 --- /dev/null +++ b/src/structure/components/commands/moderation/History.js @@ -0,0 +1,208 @@ +const { stripIndents } = require("common-tags"); +const { MessageAttachment } = require("discord.js"); +const moment = require('moment'); +const { Infractions, UploadLimit } = require("../../../../constants/Constants"); +const { Util } = require("../../../../utilities"); +const { SlashCommand } = require("../../../interfaces"); + +const Constants = { + PageSize: 5, + MaxCharacters: 256, + MaxCharactersVerbose: 128 //Displays more information in the field, decreases characters. +}; + +class HistoryCommand extends SlashCommand { + + constructor(client) { + super(client, { + name: 'history', + description: 'List past infractions', + module: 'moderation', + memberPermissions: ['MANAGE_MESSAGES'], + guildOnly: true, + options: [{ + name: ['before', 'after'], + type: 'DATE', + description: 'Filter by a date, must be in YYYY/MM/DD or YYYY-MM-DD format' + }, { + name: ['verbose', 'oldest', 'export', 'private'], + description: [ + 'Show more infromation about the listed infractions', + 'Show oldest infraction first', + 'Export the list of infractions', + 'DM the command response' + ], + type: 'BOOLEAN' + }, { + name: 'type', + description: 'Filter infractions by type', + choices: Infractions.map((inf) => { + return { name: inf.toLowerCase(), value: inf }; + }) + }, { + name: ['pagesize', 'page'], + description: ['Amount of infractions to list per page', 'Page to select'], + type: 'INTEGER', + minimum: 1 + }, { + name: ['user', 'moderator'], // + description: ['User whose infractions to query, overrides channel if both are given', 'Query by moderator'], + type: 'USER' + }, { + name: 'channel', + description: 'Infractions done on channels, e.g. slowmode, lockdown', + type: 'TEXT_CHANNEL' + }] + }); + } + + async execute(invoker, opts) { + + const { guild } = invoker; + const { user, moderator, channel, before, after, verbose, oldest, + export: exp, private: priv, type: infType, pagesize, page } = opts; + if (exp?.value) return this._exportLogs(invoker, priv?.value); + + const query = { + guild: invoker.guild.id, + }; + + if (channel) query.target = channel.value.id; + if (user) query.target = user.value.id; + if (moderator) query.executor = moderator.value.id; + + if (before || after) { + query.timestamp = {}; + if (before) query.timestamp.$lt = before.value.valueOf(); + if (after) query.timestamp.$gt = after.value.valueOf(); + } + + if (infType) query.type = infType.value; + + const pageSize = pagesize ? pagesize.value : Constants.PageSize; + let _page = page ? page.value : 1; + + const { infractions } = this.client.storageManager.mongodb; + const resultsAmt = await infractions.count(query); + if (!resultsAmt) return { emoji: 'failure', index: 'COMMAND_HISTORY_NORESULTS' }; + + const maxPage = Math.ceil(resultsAmt / pageSize); + if(_page > maxPage) _page = maxPage; + const results = await infractions.find(query, { projection: { _id: 0 } }, { + sort: { timestamp: oldest?.value ? 1 : -1 }, + skip: (_page - 1) * pageSize, + limit: pageSize + }); + + const embed = { + author: { + name: 'Infraction History', + icon_url: invoker.guild.iconURL() //eslint-disable-line camelcase + }, + fields: [], + footer: { + text: `• Page ${_page}/${maxPage} | ${resultsAmt} Results` + }, + color: invoker.guild.me.roles.highest.color + }; + + let long = false; + const handleReason = (text) => { + const MaxCharacters = Constants[verbose?.value ? 'MaxCharactersVerbose' : 'MaxCharacters']; + text = Util.escapeMarkdown(text); + if (text.length > MaxCharacters) { + text = `${text.substring(0, MaxCharacters - 3)}...`; + long = true; + } + text = text.replace(/\\n/giu, ' '); + return text; + }; + + for (let i = 0; i < results.length; i++) { + const infraction = results[i]; + let target = null; + if (infraction.targetType === 'USER') { + target = await this.client.resolver.resolveUser(infraction.target); + } else { + target = await guild.resolveChannel(infraction.target); + } + + const executor = await this.client.resolver.resolveUser(infraction.executor); + + let string = stripIndents`**Target:** ${Util.escapeMarkdown(target.tag || target.name)}${verbose?.value ? ` (${target.id})` : ''} + **Moderator:** ${executor ? `${Util.escapeMarkdown(executor.tag)}${verbose?.value ? ` (${infraction.executor})` : ''}` : infraction.executor}`; + + if (infraction.data.roleIds) { + string += `\n${guild.format('INFRACTION_DESCRIPTIONROLES', { + plural: infraction.data.roleIds.length === 1 ? '' : 's', + roles: priv ? infraction.data.roleNames.join(', ') : infraction.data.roleIds.map((r) => `<@&${r}>`).join(' ') + })}`; + } + + if (infraction.duration) string += `\n**Duration:** ${Util.duration(infraction.duration)}`; + if (infraction.points) string += `\n**Points:** ${infraction.points}`; + + string += `\n**Reason:** \`${handleReason(infraction.reason)}\``; + if (i !== results.length - 1) string += `\n\u200b`; //Space out cases (as long as its not at the end) + + embed.fields.push({ + name: `__**${infraction.type} \`[case-${infraction.case}]\`** *(${moment(infraction.timestamp).fromNow()})*__`, + value: string + }); + + } + + if (long) embed.footer.text += ` • To see the full reason, use the /case command.`; + + const type = invoker.format('COMMAND_HISTORY_SUCCESSTYPE', { old: oldest?.value || false }, { code: true }); + try { + return { + content: invoker.format('COMMAND_HISTORY_SUCCESS', { + targets: user || channel ? invoker.format('COMMAND_HISTORY_SUCCESSTARGETS', { + //plural: parsed.length === 1 ? '' : 's', + targets: `**${Util.escapeMarkdown(user?.value.tag || channel?.value.name)}**` //parsed.map((p) => `**${Util.escapeMarkdown(p.display)}**`).join(' ') + }) : '', + type + }), + emoji: 'success', + embed, + dm: Boolean(priv?.value) + }; + } catch (e) { + return { index: 'COMMAND_HISTORY_FAILTOOLONG', emoji: 'failure' }; + } + + } + + async _exportLogs(invoker, priv = false) { + + const member = await invoker.memberWrapper(); + if(!member.isAdmin()) return { emoji: 'failure', index: 'COMMAND_HISTORY_NO_EXPORT_PERMS' }; + const logs = await this.client.storageManager.mongodb.infractions.find({ guild: invoker.guild.id }, { projection: { _id: 0 } }); + const string = JSON.stringify(logs); + const attachment = new MessageAttachment(Buffer.from(string), `${invoker.guild.id}-moderation.json`); + + const bytes = Buffer.byteLength(attachment.attachment); + + const premium = priv ? '0' : invoker.guild.premiumTier; + if (bytes > UploadLimit[premium] * 1024 * 1024) { + return { + index: 'COMMAND_HISTORY_FAILEXPORT', params: { + amount: bytes, + max: UploadLimit[premium] * 1024 * 1024 + } + }; + } + + return { + index: 'COMMAND_HISTORY_SUCCESSEXPORT', + files: [attachment], + emoji: 'success', + dm: priv + }; + + } + +} + +module.exports = HistoryCommand; \ No newline at end of file