const { Util } = require('../../../util/'); const Constants = { MaxCharacters: 1024, // Max embed description is 2048 characters, however some of those description characters are going to usernames, types, filler text, etc. RemovedInfractions: ['BAN', 'SOFTBAN', 'KICK'] }; class Infraction { constructor(client, opts = {}) { this.client = client; this.message = opts.message; //NOT REQUIRED this.arguments = opts.arguments || {}; this.target = opts.target; //User or channel being targeted. User object, not GuildMember. this.targetType = opts.targetType; // 'user' or 'channel'. this.executor = opts.executor; //Whoever executed the command. User object, not GuildMember. this.guild = opts.guild; this.channel = opts.channel; this.case = null; this.type = opts.type; //What type of infraction (mute, kick, etc.) this.timestamp = Date.now(); this.duration = isNaN(opts.duration) ? null : opts.duration; //How long the action will last. Must be in milliseconds. this.callback = isNaN(opts.duration) ? null : Date.now()+opts.duration*1000; //At what epoch(?) time it will callback. this.reason = opts.reason.length ? opts.reason : 'N/A'; this.silent = opts.silent; this.points = opts.points || 0; this.totalPoints = 0; this.expiration = opts.expiration || 0; this.data = opts.data || {}; //Miscellaneous data that may need to be saved for future use. this.color = opts.color; //Infraction-defined hexadecimal value to dictate what color the embed is. this.dictionary = opts.dictionary || {}; // { past: 'banned', present: 'ban' } Infraction-defined object for the correct spellings. this.hyperlink = opts.hyperlink || null; // To overwrite hyperlink (if it's from a callback) this._logMessage = null; //The message embed sent in the moderation-log. Full message, not the ID. this._moderationLog = null; //Moderation log channel } async handle() { const { moderationLog } = this.guild._settings; this.guild._settings.caseId++; this.case = this.guild._settings.caseId; await this.guild._updateSettings({ caseId: this.case }); /* Logging */ if(moderationLog.channel) { if(moderationLog.infractions.includes(this.type)) { this._moderationLog = await this.client.resolver.resolveChannel(moderationLog.channel, true, this.guild); if(!this._moderationLog) return undefined; this._logMessage = await this._moderationLog.send('', { embed: this.embed() }); } else { this.client.logger.debug(`Did not log infraction ${this.type} because it is not in the infractions.`); } } if(this.guild._settings.dmInfraction.enabled && this.targetType === 'user' && this.type !== 'NOTE') { let message = this.guild._settings.dmInfraction.custom[this.type] || this.guild._settings.dmInfraction.custom.default; if(!message) message = ""; message = message .replace(/\{(guild|server)\}/ugim, this.guild.name) .replace(/\{user\}/ugim, this.target.tag) .replace(/\{infraction\}/ugim, this.dictionary.past) .replace(/\{from\|on\}/ugim, Constants.RemovedInfractions.includes(this.type) ? 'from' : 'on'); //add more if you want i should probably add a better system for this... try { this.target.send(message, { embed: this.embed(true) }); } catch(e) {} //eslint-disable-line no-empty } if(this.duration) { await this.client.moderationManager._handleExpirations([this.json]); } /* LMAOOOO PLEASE DONT JUDGE ME */ if(this.data.roles && this.data.roles.length > 0) { this.data.roles = this.data.roles.map((r) => r.id); } return this.save(); } async save() { return this.client.transactionHandler.send({ provider: 'mongodb', request: { type: 'insertOne', collection: 'infractions', data: this.json } }).catch((error) => { this.client.logger.error(`There was an issue saving infraction data to the database.\n${error.stack || error}`); }); } embed(dm = false) { let description = this.guild.format('INFRACTION_DESCRIPTION', { type: this.dictionary.past.toUpperCase(), moderator: `${Util.escapeMarkdown(this.executor.tag)}`, reason: Util.escapeMarkdown(this.reason.length > Constants.MaxCharacters ? `${this.reason.substring(0, Constants.MaxCharacters-3)}...` : this.reason, { italic: false, underline: false, strikethrough: false }) }); if(this.duration) { description += `\n${this.guild.format('INFRACTION_DESCRIPTIONDURATION', { duration: Util.duration(this.duration) })}`; } if(this.points > 0) { description += `\n${this.guild.format('INFRACTION_DESCRIPTIONPOINTS', { points: this.points, total: this.totalPoints })}`; } if(this.description && this.description instanceof Function) { description += this.description(dm); } if((!this.silent && (this.message || this.hyperlink)) && (dm && !Constants.RemovedInfractions.includes(this.type))) { description += `\n${this.guild.format('INFRACTION_DESCRIPTIONJUMPTO', { name: this.hyperlink ? 'Case' : 'Message', link: this.hyperlink ? this.hyperlink : `https://discord.com/channels/${this.guild.id}/${this.channel.id}/${this.message.id}` })}`; } return { author: { name: `${this.targetName} (${this.target.id})`, icon_url: this.targetIcon //eslint-disable-line camelcase }, timestamp: new Date(), color: this.color, footer: { text: `》 Case ${this.case}` }, description }; } get json() { return { id: `${this.guild.id}:${this.case}`, guild: this.guild.id, channel: this.channel ? this.channel.id : null, message: this.message ? this.message.id : null, executor: this.executor.id, target: this.target.id, targetType: this.targetType, type: this.type, case: this.case, timestamp: this.timestamp, duration: this.duration, callback: this.callback, reason: this.reason, points: this.points, expiration: this.expiration, data: this.data, actions: this.actions, logMessage: this._logMessage ? this._logMessage.id : null, //the message id sent in modlog channel moderationLog: this._moderationLog ? this._moderationLog.id : null, callbacked: false }; } get targetName() { return this.targetType === 'user' ? this.target.tag : `#${this.target.name}`; } get targetIcon() { return this.targetType === 'user' ? this.target.displayAvatarURL() : this.guild.iconURL(); } get actions() { const actions = []; for(const argument of Object.values(this.arguments)) { if(['silent', 'prune', 'force'].includes(argument)) actions.push(argument); } return actions; } get _reason() { let str = `[${this.type}][targetId:${this.target.id}] Executed by ${this.executor.tag} (${this.executor.id}) because: ${this.reason}`; if(str.length > 512) str = `${this.reason.substring(0, 509)}...`; return str; } //Super Functions _succeed() { return { error: false, infraction: this }; } _fail(message, fatal = false) { return { error: true, infraction: this, reason: message, fatal }; } verify() { return this._verify(); } _verify() { if(this.guild && this.guild._settings.protection.enabled) { const executorHighest = this.executor.roles.highest; const targetHighest = this.member.roles.highest; if(executorHighest.comparePositionTo(targetHighest) > 0) { return this._fail('INFRACTION_PROTECTIONERROR'); } } return this._succeed(); } } module.exports = Infraction;