From ddbbc543c2e7994c71eca5e021cff885d0896559 Mon Sep 17 00:00:00 2001 From: Dragory <2606411+Dragory@users.noreply.github.com> Date: Sun, 9 Aug 2020 22:44:07 +0300 Subject: [PATCH] Add !deletecase --- backend/src/data/CaseTypes.ts | 2 +- backend/src/data/DefaultLogMessages.json | 4 +- backend/src/data/GuildCases.ts | 32 +++++++++ backend/src/data/LogType.ts | 2 + .../1596994103885-AddCaseNotesForeignKey.ts | 22 ++++++ .../plugins/Cases/functions/getCaseEmbed.ts | 14 ++-- .../plugins/ModActions/ModActionsPlugin.ts | 3 + .../ModActions/commands/DeleteCaseCmd.ts | 72 +++++++++++++++++++ backend/src/plugins/ModActions/types.ts | 1 + 9 files changed, 146 insertions(+), 6 deletions(-) create mode 100644 backend/src/migrations/1596994103885-AddCaseNotesForeignKey.ts create mode 100644 backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts diff --git a/backend/src/data/CaseTypes.ts b/backend/src/data/CaseTypes.ts index fb1da5ee..6b1cad5b 100644 --- a/backend/src/data/CaseTypes.ts +++ b/backend/src/data/CaseTypes.ts @@ -6,6 +6,6 @@ export enum CaseTypes { Kick, Mute, Unmute, - Expunged, + Deleted, Softban, } diff --git a/backend/src/data/DefaultLogMessages.json b/backend/src/data/DefaultLogMessages.json index 40259ef8..20f957d2 100644 --- a/backend/src/data/DefaultLogMessages.json +++ b/backend/src/data/DefaultLogMessages.json @@ -66,5 +66,7 @@ "AUTOMOD_ACTION": "\uD83E\uDD16 Automod rule **{rule}** triggered by {userMention(users)}\n{matchSummary}\nActions taken: **{actionsTaken}**", "SET_ANTIRAID_USER": "⚔ {userMention(user)} set anti-raid to **{level}**", - "SET_ANTIRAID_AUTO": "⚔ Anti-raid automatically set to **{level}**" + "SET_ANTIRAID_AUTO": "⚔ Anti-raid automatically set to **{level}**", + + "CASE_DELETE": "✂️ **Case #{case.case_number}** was deleted by {userMention(mod)}" } diff --git a/backend/src/data/GuildCases.ts b/backend/src/data/GuildCases.ts index de105b01..8e4c2400 100644 --- a/backend/src/data/GuildCases.ts +++ b/backend/src/data/GuildCases.ts @@ -5,6 +5,7 @@ import { getRepository, In, Repository } from "typeorm"; import { disableLinkPreviews } from "../utils"; import { CaseTypes } from "./CaseTypes"; import moment = require("moment-timezone"); +import { connection } from "./db"; const CASE_SUMMARY_REASON_MAX_LENGTH = 300; @@ -119,6 +120,37 @@ export class GuildCases extends BaseGuildRepository { return this.cases.update(id, data); } + async softDelete(id: number, deletedById: string, deletedByName: string, deletedByText: string) { + return connection.transaction(async entityManager => { + const cases = entityManager.getRepository(Case); + const caseNotes = entityManager.getRepository(CaseNote); + + await Promise.all([ + caseNotes.delete({ + case_id: id, + }), + cases.update(id, { + user_id: "0", + user_name: "Unknown#0000", + mod_id: null, + mod_name: "Unknown#0000", + type: CaseTypes.Deleted, + audit_log_id: null, + is_hidden: false, + pp_id: null, + pp_name: null, + }), + ]); + + await caseNotes.insert({ + case_id: id, + mod_id: deletedById, + mod_name: deletedByName, + body: deletedByText, + }); + }); + } + async createNote(caseId: number, data: any): Promise { await this.caseNotes.insert({ ...data, diff --git a/backend/src/data/LogType.ts b/backend/src/data/LogType.ts index 8ae42cd7..115ec4be 100644 --- a/backend/src/data/LogType.ts +++ b/backend/src/data/LogType.ts @@ -75,4 +75,6 @@ export enum LogType { MASS_UNASSIGN_ROLES, MEMBER_NOTE, + + CASE_DELETE, } diff --git a/backend/src/migrations/1596994103885-AddCaseNotesForeignKey.ts b/backend/src/migrations/1596994103885-AddCaseNotesForeignKey.ts new file mode 100644 index 00000000..e6b6408d --- /dev/null +++ b/backend/src/migrations/1596994103885-AddCaseNotesForeignKey.ts @@ -0,0 +1,22 @@ +import { MigrationInterface, QueryRunner } from "typeorm"; +import { TableForeignKey } from "typeorm/index"; + +export class AddCaseNotesForeignKey1596994103885 implements MigrationInterface { + public async up(queryRunner: QueryRunner): Promise { + await queryRunner.createForeignKey( + "case_notes", + new TableForeignKey({ + name: "case_notes_case_id_fk", + columnNames: ["case_id"], + referencedTableName: "cases", + referencedColumnNames: ["id"], + onDelete: "CASCADE", + onUpdate: "CASCADE", + }), + ); + } + + public async down(queryRunner: QueryRunner): Promise { + await queryRunner.dropForeignKey("case_notes", "case_notes_case_id_fk"); + } +} diff --git a/backend/src/plugins/Cases/functions/getCaseEmbed.ts b/backend/src/plugins/Cases/functions/getCaseEmbed.ts index 8f3e13c3..28afeab6 100644 --- a/backend/src/plugins/Cases/functions/getCaseEmbed.ts +++ b/backend/src/plugins/Cases/functions/getCaseEmbed.ts @@ -1,5 +1,5 @@ import { Case } from "../../../data/entities/Case"; -import { MessageContent } from "eris"; +import { AdvancedMessageContent, MessageContent } from "eris"; import moment from "moment-timezone"; import { CaseTypes } from "../../../data/CaseTypes"; import { PluginData, helpers } from "knub"; @@ -11,13 +11,19 @@ import { chunkLines, chunkMessageLines, emptyEmbedValue } from "../../../utils"; export async function getCaseEmbed( pluginData: PluginData, caseOrCaseId: Case | number, -): Promise { +): Promise { const theCase = await pluginData.state.cases.with("notes").find(resolveCaseId(caseOrCaseId)); if (!theCase) return null; const createdAt = moment(theCase.created_at); const actionTypeStr = CaseTypes[theCase.type].toUpperCase(); + let userName = theCase.user_name; + if (theCase.user_id && theCase.user_id !== "0") userName += `\n<@!${theCase.user_id}>`; + + let modName = theCase.mod_name; + if (theCase.mod_id) modName += `\n<@!${theCase.mod_id}>`; + const embed: any = { title: `${actionTypeStr} - Case #${theCase.case_number}`, footer: { @@ -26,12 +32,12 @@ export async function getCaseEmbed( fields: [ { name: "User", - value: `${theCase.user_name}\n<@!${theCase.user_id}>`, + value: userName, inline: true, }, { name: "Moderator", - value: `${theCase.mod_name}\n<@!${theCase.mod_id}>`, + value: modName, inline: true, }, ], diff --git a/backend/src/plugins/ModActions/ModActionsPlugin.ts b/backend/src/plugins/ModActions/ModActionsPlugin.ts index fea68069..d6abe1f4 100644 --- a/backend/src/plugins/ModActions/ModActionsPlugin.ts +++ b/backend/src/plugins/ModActions/ModActionsPlugin.ts @@ -34,6 +34,7 @@ import { kickMember } from "./functions/kickMember"; import { banUserId } from "./functions/banUserId"; import { MassmuteCmd } from "./commands/MassmuteCmd"; import { trimPluginDescription } from "../../utils"; +import { DeleteCaseCmd } from "./commands/DeleteCaseCmd"; const defaultOptions = { config: { @@ -65,6 +66,7 @@ const defaultOptions = { can_massban: false, can_massmute: false, can_hidecase: false, + can_deletecase: false, can_act_as_other: false, }, overrides: [ @@ -134,6 +136,7 @@ export const ModActionsPlugin = zeppelinPlugin()("mod_acti CasesModCmd, HideCaseCmd, UnhideCaseCmd, + DeleteCaseCmd, ], public: { diff --git a/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts b/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts new file mode 100644 index 00000000..5d6241fb --- /dev/null +++ b/backend/src/plugins/ModActions/commands/DeleteCaseCmd.ts @@ -0,0 +1,72 @@ +import { modActionsCommand } from "../types"; +import { commandTypeHelpers as ct } from "../../../commandTypes"; +import { sendErrorMessage, sendSuccessMessage } from "../../../pluginUtils"; +import { helpers } from "knub"; +import { CasesPlugin } from "../../Cases/CasesPlugin"; +import { TextChannel } from "eris"; +import { SECONDS, stripObjectToScalars, trimLines } from "../../../utils"; +import { LogsPlugin } from "../../Logs/LogsPlugin"; +import { LogType } from "../../../data/LogType"; +import moment from "moment-timezone"; + +export const DeleteCaseCmd = modActionsCommand({ + trigger: ["delete_case", "deletecase"], + permission: "can_deletecase", + description: trimLines(` + Delete the specified case. This operation can *not* be reversed. + It is generally recommended to use \`!hidecase\` instead when possible. + `), + + signature: { + caseNumber: ct.number(), + + force: ct.switchOption({ shortcut: "f" }), + }, + + async run({ pluginData, message, args }) { + const theCase = await pluginData.state.cases.findByCaseNumber(args.caseNumber); + if (!theCase) { + sendErrorMessage(pluginData, message.channel, "Case not found"); + return; + } + + if (!args.force) { + const cases = pluginData.getPlugin(CasesPlugin); + const embedContent = await cases.getCaseEmbed(theCase); + message.channel.createMessage({ + content: "Delete the following case? Answer 'Yes' to continue, 'No' to cancel.", + embed: embedContent.embed, + }); + + const reply = await helpers.waitForReply( + pluginData.client, + message.channel as TextChannel, + message.author.id, + 15 * SECONDS, + ); + const normalizedReply = (reply?.content || "").toLowerCase().trim(); + if (normalizedReply !== "yes" && normalizedReply !== "y") { + message.channel.createMessage("Cancelled. Case was not deleted."); + return; + } + } + + const deletedByName = `${message.author.username}#${message.author.discriminator}`; + const deletedAt = moment().format(`MMM D, YYYY [at] H:mm [UTC]`); + + await pluginData.state.cases.softDelete( + theCase.id, + message.author.id, + deletedByName, + `Case deleted by **${deletedByName}** (\`${message.author.id}\`) on ${deletedAt}`, + ); + + const logs = pluginData.getPlugin(LogsPlugin); + logs.log(LogType.CASE_DELETE, { + mod: stripObjectToScalars(message.member, ["user", "roles"]), + case: stripObjectToScalars(theCase), + }); + + sendSuccessMessage(pluginData, message.channel, `Case #${theCase.case_number} deleted!`); + }, +}); diff --git a/backend/src/plugins/ModActions/types.ts b/backend/src/plugins/ModActions/types.ts index 74546de6..72c60724 100644 --- a/backend/src/plugins/ModActions/types.ts +++ b/backend/src/plugins/ModActions/types.ts @@ -35,6 +35,7 @@ export const ConfigSchema = t.type({ can_massban: t.boolean, can_massmute: t.boolean, can_hidecase: t.boolean, + can_deletecase: t.boolean, can_act_as_other: t.boolean, }); export type TConfigSchema = t.TypeOf;