Skip to content

Commit

Permalink
feat: implement delete note command (#72)
Browse files Browse the repository at this point in the history
  • Loading branch information
HeadTriXz authored Sep 12, 2023
1 parent 4f9c056 commit ebc3999
Show file tree
Hide file tree
Showing 3 changed files with 231 additions and 1 deletion.
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
});
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,7 @@ import type ModerationModule from "../../../../index.js";
import { PermissionFlagsBits } from "@discordjs/core";
import { SlashCommand } from "@barry/core";
import AddNoteCommand from "./add/index.js";
import DeleteNoteCommand from "./delete/index.js";

/**
* Represents a slash command to manage notes on a case.
Expand All @@ -19,7 +20,7 @@ export default class extends SlashCommand<ModerationModule> {
description: "Modify or add notes to a case.",
defaultMemberPermissions: PermissionFlagsBits.ModerateMembers,
guildOnly: true,
children: [AddNoteCommand]
children: [AddNoteCommand, DeleteNoteCommand]
});
}

Expand Down
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
});
});
});
});

0 comments on commit ebc3999

Please # to comment.