-
-
Notifications
You must be signed in to change notification settings - Fork 0
Commit
This commit does not belong to any branch on this repository, and may belong to a fork outside of the repository.
feat: implement delete note command (#72)
- Loading branch information
Showing
3 changed files
with
231 additions
and
1 deletion.
There are no files selected for viewing
102 changes: 102 additions & 0 deletions
102
apps/barry/src/modules/moderation/commands/chatinput/cases/notes/delete/index.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,102 @@ | ||
import { | ||
type ApplicationCommandInteraction, | ||
SlashCommand, | ||
SlashCommandOptionBuilder | ||
} from "@barry/core"; | ||
import type ModerationModule from "../../../../../index.js"; | ||
|
||
import { MessageFlags, PermissionFlagsBits } from "@discordjs/core"; | ||
import config from "../../../../../../../config.js"; | ||
|
||
/** | ||
* Options for the delete note options. | ||
*/ | ||
export interface DeleteNoteOptions { | ||
/** | ||
* The ID of the case. | ||
*/ | ||
case: number; | ||
|
||
/** | ||
* The ID of the note. | ||
*/ | ||
note: number; | ||
} | ||
|
||
/** | ||
* The permissions needed to delete someone else's note. | ||
*/ | ||
const BYPASS_PERMISSIONS = PermissionFlagsBits.Administrator | PermissionFlagsBits.ManageGuild; | ||
|
||
/** | ||
* Represents a slash command that a note from a case. | ||
*/ | ||
export default class extends SlashCommand<ModerationModule> { | ||
/** | ||
* Represents a slash command that a note from a case. | ||
* | ||
* @param module The module this command belongs to. | ||
*/ | ||
constructor(module: ModerationModule) { | ||
super(module, { | ||
name: "delete", | ||
description: "Removes a note from a case.", | ||
defaultMemberPermissions: PermissionFlagsBits.ModerateMembers, | ||
guildOnly: true, | ||
options: { | ||
case: SlashCommandOptionBuilder.integer({ | ||
description: "The ID of the case.", | ||
minimum: 1, | ||
required: true | ||
}), | ||
note: SlashCommandOptionBuilder.integer({ | ||
description: "The ID of the note.", | ||
minimum: 1, | ||
required: true | ||
}) | ||
} | ||
}); | ||
} | ||
|
||
/** | ||
* Remove a note from a case. | ||
* | ||
* @param interaction The interaction that triggered the command. | ||
* @param options The options for the command. | ||
*/ | ||
async execute(interaction: ApplicationCommandInteraction, options: DeleteNoteOptions): Promise<void> { | ||
if (!interaction.isInvokedInGuild()) { | ||
return; | ||
} | ||
|
||
const entity = await this.module.cases.get(interaction.guildID, options.case, true); | ||
if (entity === null) { | ||
return interaction.createMessage({ | ||
content: `${config.emotes.error} That case does not exist.`, | ||
flags: MessageFlags.Ephemeral | ||
}); | ||
} | ||
|
||
const note = entity.notes.find((n) => n.id === options.note); | ||
if (note === undefined) { | ||
return interaction.createMessage({ | ||
content: `${config.emotes.error} That note does not exist.`, | ||
flags: MessageFlags.Ephemeral | ||
}); | ||
} | ||
|
||
const hasPermissions = (BigInt(interaction.member.permissions) & BYPASS_PERMISSIONS) !== 0n; | ||
if (!hasPermissions && note.creatorID !== interaction.user.id) { | ||
return interaction.createMessage({ | ||
content: `${config.emotes.error} That is not your note.`, | ||
flags: MessageFlags.Ephemeral | ||
}); | ||
} | ||
|
||
await this.module.caseNotes.delete(interaction.guildID, options.case, options.note); | ||
await interaction.createMessage({ | ||
content: `${config.emotes.check} Successfully deleted note \`${options.note}\` from case \`${options.case}\`.`, | ||
flags: MessageFlags.Ephemeral | ||
}); | ||
} | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
127 changes: 127 additions & 0 deletions
127
apps/barry/tests/modules/moderation/commands/chatinput/cases/notes/delete/index.test.ts
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,127 @@ | ||
import type { CaseWithNotes } from "../../../../../../../../src/modules/moderation/database.js"; | ||
|
||
import { MessageFlags, PermissionFlagsBits } from "@discordjs/core"; | ||
import { | ||
createMockApplicationCommandInteraction, | ||
mockMember, | ||
mockUser | ||
} from "@barry/testing"; | ||
import { mockCase, mockCaseNote } from "../../../../../mocks/case.js"; | ||
import { ApplicationCommandInteraction } from "@barry/core"; | ||
import { createMockApplication } from "../../../../../../../mocks/application.js"; | ||
|
||
import DeleteNoteCommand, { type DeleteNoteOptions } from "../../../../../../../../src/modules/moderation/commands/chatinput/cases/notes/delete/index.js"; | ||
import ModerationModule from "../../../../../../../../src/modules/moderation/index.js"; | ||
|
||
describe("/cases notes delete", () => { | ||
let command: DeleteNoteCommand; | ||
let interaction: ApplicationCommandInteraction; | ||
let options: DeleteNoteOptions; | ||
|
||
beforeEach(() => { | ||
const client = createMockApplication(); | ||
const module = new ModerationModule(client); | ||
command = new DeleteNoteCommand(module); | ||
|
||
const data = createMockApplicationCommandInteraction(); | ||
interaction = new ApplicationCommandInteraction(data, client, vi.fn()); | ||
|
||
options = { | ||
case: 34, | ||
note: 1 | ||
}; | ||
|
||
vi.spyOn(module.cases, "get").mockResolvedValue({ | ||
...mockCase, | ||
notes: [mockCaseNote] | ||
} as CaseWithNotes); | ||
vi.spyOn(module.caseNotes, "delete").mockResolvedValue(mockCaseNote); | ||
}); | ||
|
||
describe("execute", () => { | ||
it("should ignore if the interaction was invoked outside a guild", async () => { | ||
delete interaction.guildID; | ||
|
||
await command.execute(interaction, options); | ||
|
||
expect(interaction.acknowledged).toBe(false); | ||
}); | ||
|
||
it("should show an error message if the provided case does not exist", async () => { | ||
vi.spyOn(command.module.cases, "get").mockResolvedValue(null); | ||
const createSpy = vi.spyOn(interaction, "createMessage"); | ||
|
||
await command.execute(interaction, options); | ||
|
||
expect(createSpy).toHaveBeenCalledOnce(); | ||
expect(createSpy).toHaveBeenCalledWith({ | ||
content: expect.stringContaining("That case does not exist."), | ||
flags: MessageFlags.Ephemeral | ||
}); | ||
}); | ||
|
||
it("should show an error message if the provided note does not exist", async () => { | ||
vi.spyOn(command.module.cases, "get").mockResolvedValue({ ...mockCase, notes: [] } as CaseWithNotes); | ||
const createSpy = vi.spyOn(interaction, "createMessage"); | ||
|
||
await command.execute(interaction, options); | ||
|
||
expect(createSpy).toHaveBeenCalledOnce(); | ||
expect(createSpy).toHaveBeenCalledWith({ | ||
content: expect.stringContaining("That note does not exist."), | ||
flags: MessageFlags.Ephemeral | ||
}); | ||
}); | ||
|
||
it("should show an error message if the user tries deleting a note that is not theirs", async () => { | ||
interaction.user = { ...mockUser, id: mockCase.userID }; | ||
const createSpy = vi.spyOn(interaction, "createMessage"); | ||
|
||
await command.execute(interaction, options); | ||
|
||
expect(createSpy).toHaveBeenCalledOnce(); | ||
expect(createSpy).toHaveBeenCalledWith({ | ||
content: expect.stringContaining("That is not your note."), | ||
flags: MessageFlags.Ephemeral | ||
}); | ||
}); | ||
|
||
it("should delete the provided note", async () => { | ||
const deleteSpy = vi.spyOn(command.module.caseNotes, "delete"); | ||
|
||
await command.execute(interaction, options); | ||
|
||
expect(deleteSpy).toHaveBeenCalledOnce(); | ||
expect(deleteSpy).toHaveBeenCalledWith(interaction.guildID, options.case, options.note); | ||
}); | ||
|
||
it("should bypass if the user has administrator permissions", async () => { | ||
interaction.user = { ...mockUser, id: mockCase.userID }; | ||
interaction.member = { | ||
...mockMember, | ||
permissions: PermissionFlagsBits.ManageGuild.toString() | ||
}; | ||
const createSpy = vi.spyOn(interaction, "createMessage"); | ||
|
||
await command.execute(interaction, options); | ||
|
||
expect(createSpy).toHaveBeenCalledOnce(); | ||
expect(createSpy).toHaveBeenCalledWith({ | ||
content: expect.stringContaining("Successfully deleted note `1` from case `34`."), | ||
flags: MessageFlags.Ephemeral | ||
}); | ||
}); | ||
|
||
it("should show a success message if the note is successfully deleted", async () => { | ||
const createSpy = vi.spyOn(interaction, "createMessage"); | ||
|
||
await command.execute(interaction, options); | ||
|
||
expect(createSpy).toHaveBeenCalledOnce(); | ||
expect(createSpy).toHaveBeenCalledWith({ | ||
content: expect.stringContaining("Successfully deleted note `1` from case `34`."), | ||
flags: MessageFlags.Ephemeral | ||
}); | ||
}); | ||
}); | ||
}); |