diff --git a/src/data/CaseType.ts b/src/data/CaseType.ts index 64845bbc..583916a8 100644 --- a/src/data/CaseType.ts +++ b/src/data/CaseType.ts @@ -6,5 +6,6 @@ export enum CaseType { Kick, Mute, Unmute, - Expunged + Expunged, + Softban } diff --git a/src/data/DefaultLogMessages.json b/src/data/DefaultLogMessages.json index 2c7ce2c4..014c1bf6 100644 --- a/src/data/DefaultLogMessages.json +++ b/src/data/DefaultLogMessages.json @@ -8,6 +8,7 @@ "MEMBER_BAN": "🔨 **{user.username}#{user.discriminator}** (`{user.id}`) was banned by {mod.username}#{mod.discriminator}", "MEMBER_UNBAN": "🔓 User (`{userId}`) was unbanned by {mod.username}#{mod.discriminator}", "MEMBER_FORCEBAN": "🔨 User (`{userId}`) was forcebanned by {mod.username}#{mod.discriminator}", + "MEMBER_SOFTBAN": "🔨 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) was softbanned by {mod.username}#{mod.discriminator}", "MEMBER_JOIN": "📥 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) joined{new} (created {account_age} ago)", "MEMBER_LEAVE": "📤 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) left the server", "MEMBER_ROLE_ADD": "🔑 **{member.user.username}#{member.user.discriminator}** (`{member.id}`) role added **{role.name}** by {mod.username}#{mod.discriminator}", diff --git a/src/data/GuildCases.ts b/src/data/GuildCases.ts index 430ebde8..b0fbbc25 100644 --- a/src/data/GuildCases.ts +++ b/src/data/GuildCases.ts @@ -1,5 +1,6 @@ import knex from "../knex"; import Case from "../models/Case"; +import CaseNote from "../models/CaseNote"; export class GuildCases { protected guildId: string; @@ -26,12 +27,12 @@ export class GuildCases { return result ? new Case(result) : null; } - async getCaseNotes(caseId: number): Promise { + async getCaseNotes(caseId: number): Promise { const results = await knex("case_notes") .where("case_id", caseId) .select(); - return results.map(r => new Case(r)); + return results.map(r => new CaseNote(r)); } async getByUserId(userId: string): Promise { @@ -43,6 +44,14 @@ export class GuildCases { return results.map(r => new Case(r)); } + async findFirstCaseNote(caseId: number): Promise { + const result = await knex("case_notes") + .where("case_id", caseId) + .first(); + + return result ? new CaseNote(result) : null; + } + async create(data): Promise { return knex .insert({ diff --git a/src/data/LogType.ts b/src/data/LogType.ts index 856b5669..540f9878 100644 --- a/src/data/LogType.ts +++ b/src/data/LogType.ts @@ -8,6 +8,7 @@ export enum LogType { MEMBER_BAN, MEMBER_UNBAN, MEMBER_FORCEBAN, + MEMBER_SOFTBAN, MEMBER_JOIN, MEMBER_LEAVE, MEMBER_ROLE_ADD, diff --git a/src/models/CaseNote.ts b/src/models/CaseNote.ts new file mode 100644 index 00000000..1b197c4c --- /dev/null +++ b/src/models/CaseNote.ts @@ -0,0 +1,10 @@ +import Model from "./Model"; + +export default class CaseNote extends Model { + public id: number; + public case_id: number; + public mod_id: string; + public mod_name: string; + public body: string; + public created_at: string; +} diff --git a/src/plugins/ModActions.ts b/src/plugins/ModActions.ts index a0fd51cc..c3d4188c 100644 --- a/src/plugins/ModActions.ts +++ b/src/plugins/ModActions.ts @@ -5,12 +5,14 @@ import humanizeDuration from "humanize-duration"; import { GuildCases } from "../data/GuildCases"; import { convertDelayStringToMS, + disableLinkPreviews, errorMessage, findRelevantAuditLogEntry, formatTemplateString, sleep, stripObjectToScalars, - successMessage + successMessage, + trimLines } from "../utils"; import { GuildMutes } from "../data/GuildMutes"; import Case from "../models/Case"; @@ -60,7 +62,7 @@ export class ModActionsPlugin extends Plugin { config: { mute_role: null, dm_on_warn: true, - dm_on_mute: true, + dm_on_mute: false, dm_on_kick: false, dm_on_ban: false, message_on_warn: false, @@ -242,28 +244,36 @@ export class ModActionsPlugin extends Plugin { @d.command(/update|updatecase/, " ") @d.permission("note") async updateCmd(msg: Message, args: any) { - const action = await this.cases.findByCaseNumber(args.caseNumber); - if (!action) { + const theCase = await this.cases.findByCaseNumber(args.caseNumber); + if (!theCase) { msg.channel.createMessage("Case not found!"); return; } - if (action.mod_id === null) { + if (theCase.mod_id === null) { // If the action has no moderator information, assume the first one to update it did the action - await this.cases.update(action.id, { + await this.cases.update(theCase.id, { mod_id: msg.author.id, mod_name: `${msg.author.username}#${msg.author.discriminator}` }); } - await this.createCaseNote(action.id, msg.author.id, args.note); - this.postCaseToCaseLog(action.id); // Post updated case to case log + await this.createCaseNote(theCase.id, msg.author.id, args.note); + this.postCaseToCaseLog(theCase.id); // Post updated case to case log + + if (msg.channel.id !== this.configValue("case_log_channel")) { + msg.channel.createMessage(successMessage(`Case \`#${theCase.case_number}\` updated`)); + } } - @d.command("note", " ") + @d.command("note", " ") @d.permission("note") async noteCmd(msg: Message, args: any) { + const user = await this.bot.users.get(args.userId); + const userName = user ? `${user.username}#${user.discriminator}` : "member"; + await this.createCase(args.userId, msg.author.id, CaseType.Note, null, args.note); + msg.channel.createMessage(successMessage(`Note added on ${userName}`)); } @d.command("warn", " ") @@ -299,7 +309,9 @@ export class ModActionsPlugin extends Plugin { await this.createCase(args.member.id, msg.author.id, CaseType.Warn, null, args.reason); - msg.channel.createMessage(successMessage("Member warned")); + msg.channel.createMessage( + successMessage(`Warned **${args.member.user.username}#${args.member.user.discriminator}**`) + ); this.serverLogs.log(LogType.MEMBER_WARN, { mod: stripObjectToScalars(msg.member.user), @@ -366,9 +378,13 @@ export class ModActionsPlugin extends Plugin { // Confirm the action to the moderator let response; if (muteTime) { - response = `Member muted for ${timeUntilUnmute}`; + response = `Muted **${args.member.user.username}#${ + args.member.user.discriminator + }** for ${timeUntilUnmute}`; } else { - response = `Member muted indefinitely`; + response = `Muted **${args.member.user.username}#${ + args.member.user.discriminator + }** indefinitely`; } if (!messageSent) response += " (failed to message user)"; @@ -408,7 +424,9 @@ export class ModActionsPlugin extends Plugin { await this.mutes.clear(args.member.id); // Confirm the action to the moderator - msg.channel.createMessage(successMessage("Member unmuted")); + msg.channel.createMessage( + successMessage(`Unmuted **${args.member.user.username}#${args.member.user.discriminator}**`) + ); // Create a case await this.createCase(args.member.id, msg.author.id, CaseType.Unmute, null, args.reason); @@ -454,7 +472,7 @@ export class ModActionsPlugin extends Plugin { await this.createCase(args.member.id, msg.author.id, CaseType.Kick, null, args.reason); // Confirm the action to the moderator - let response = `Member kicked`; + let response = `Kicked **${args.member.user.username}#${args.member.user.discriminator}**`; if (!messageSent) response += " (failed to message user)"; msg.channel.createMessage(successMessage(response)); @@ -499,7 +517,7 @@ export class ModActionsPlugin extends Plugin { await this.createCase(args.member.id, msg.author.id, CaseType.Ban, null, args.reason); // Confirm the action to the moderator - let response = `Member banned`; + let response = `Banned **${args.member.user.username}#${args.member.user.discriminator}**`; if (!messageSent) response += " (failed to message user)"; msg.channel.createMessage(successMessage(response)); @@ -510,7 +528,42 @@ export class ModActionsPlugin extends Plugin { }); } - @d.command("unban", " [reason:string$]") + @d.command("softban", " [reason:string$]") + @d.permission("ban") + async softbanCmd(msg, args) { + // Make sure we're allowed to ban this member + if (!this.canActOn(msg.member, args.member)) { + msg.channel.createMessage(errorMessage("Cannot ban: insufficient permissions")); + return; + } + + // Softban the user = ban, and immediately unban + this.serverLogs.ignoreLog(LogType.MEMBER_BAN, args.member.id); + this.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, args.member.id); + this.ignoreEvent(IgnoredEventType.Ban, args.member.id); + this.ignoreEvent(IgnoredEventType.Unban, args.member.id); + + await args.member.ban(1, args.reason); + await this.guild.unbanMember(args.member.id); + + // Create a case for this action + await this.createCase(args.member.id, msg.author.id, CaseType.Softban, null, args.reason); + + // Confirm the action to the moderator + msg.channel.createMessage( + successMessage( + `Softbanned **${args.member.user.username}#${args.member.user.discriminator}**` + ) + ); + + // Log the action + this.serverLogs.log(LogType.MEMBER_SOFTBAN, { + mod: stripObjectToScalars(msg.member.user), + member: stripObjectToScalars(args.member, ["user"]) + }); + } + + @d.command("unban", " [reason:string$]") @d.permission("ban") async unbanCmd(msg: Message, args: any) { this.serverLogs.ignoreLog(LogType.MEMBER_UNBAN, args.userId); @@ -536,7 +589,7 @@ export class ModActionsPlugin extends Plugin { }); } - @d.command("forceban", " [reason:string$]") + @d.command("forceban", " [reason:string$]") @d.permission("ban") async forcebanCmd(msg: Message, args: any) { // If the user exists as a guild member, make sure we can act on them first @@ -571,7 +624,7 @@ export class ModActionsPlugin extends Plugin { }); } - @d.command("addcase", " [reason:string$]") + @d.command("addcase", " [reason:string$]") @d.permission("addcase") async addcaseCmd(msg: Message, args: any) { // Verify the user id is a valid snowflake-ish @@ -621,30 +674,47 @@ export class ModActionsPlugin extends Plugin { * If the argument passed is a case id, display that case * If the argument passed is a user id, show all cases on that user */ - @d.command(/showcase|case|cases|usercases/, "") + @d.command(/showcase|case/, "") @d.permission("view") - async showcaseCmd(msg: Message, args: any) { - if (args.caseNumberOrUserId.length >= 17) { - // Assume user id - const actions = await this.cases.getByUserId(args.caseNumberOrUserId); + async showcaseCmd(msg: Message, args: { caseNumber: number }) { + // Assume case id + const theCase = await this.cases.findByCaseNumber(args.caseNumber); - if (actions.length === 0) { - msg.channel.createMessage("No cases found for the specified user!"); - } else { - for (const action of actions) { - await this.displayCase(action, msg.channel.id); - } - } + if (!theCase) { + msg.channel.createMessage("Case not found!"); + return; + } + + this.displayCase(theCase.id, msg.channel.id); + } + + @d.command(/cases|usercases/, "") + @d.permission("view") + async usercasesCmd(msg: Message, args: { userId: string }) { + const cases = await this.cases.getByUserId(args.userId); + const user = this.bot.users.get(args.userId); + const userName = user ? `${user.username}#${user.discriminator}` : "Unknown#0000"; + const prefix = this.knub.getGuildData(this.guildId).config.prefix; + + if (cases.length === 0) { + msg.channel.createMessage("No cases found for the specified user!"); } else { - // Assume case id - const action = await this.cases.findByCaseNumber(args.caseNumberOrUserId); - - if (!action) { - msg.channel.createMessage("Case not found!"); - return; + const lines = []; + for (const theCase of cases) { + const firstNote = await this.cases.findFirstCaseNote(theCase.id); + const reason = firstNote ? disableLinkPreviews(firstNote.body) : ""; + lines.push(`Case \`#${theCase.case_number}\` __${CaseType[theCase.type]}__ ${reason}`); } - this.displayCase(action.id, msg.channel.id); + const finalMessage = trimLines(` + Cases for **${userName}**: + + ${lines.join("\n")} + + Use \`${prefix}case \` to see more info about individual cases + `); + + msg.channel.createMessage(finalMessage); } } diff --git a/src/utils.ts b/src/utils.ts index cdf177c1..d04a5b5f 100644 --- a/src/utils.ts +++ b/src/utils.ts @@ -267,3 +267,10 @@ export function getRoleMentions(str: string) { return roleIds; } + +/** + * Disables link previews in the given string by wrapping links in < > + */ +export function disableLinkPreviews(str: string): string { + return str.replace(/(?"); +}