diff --git a/README.md b/README.md index ed1bfb9..2eb698f 100644 --- a/README.md +++ b/README.md @@ -76,10 +76,11 @@ Go to https://discordapi.com/permissions.html#378091424832 and paste in your bot ### Build the bot server -Be sure to install dependencies: +Be sure to install dependencies, and run a quick lint to generate needed files: ```sh $ npm ci +$ npm run lint ``` The first time you download the source, and each time the source code changes, you'll need to run this command before you run the bot: @@ -112,7 +113,9 @@ $ pm2 start . ## Commands -\[TBD\] +### /help + +Prints the list of commands. ## Contributing diff --git a/package.json b/package.json index c217802..e62c38d 100644 --- a/package.json +++ b/package.json @@ -19,7 +19,7 @@ "prebuild": "npm run lint", "build": "npm run build:only", "build:only": "rm -rf dist && ./node_modules/.bin/tsc -p tsconfig.prod.json", - "export-version": "./node_modules/.bin/genversion ./src/version.ts -esd", + "export-version": "./node_modules/.bin/genversion ./src/version.ts -es", "test": "./node_modules/.bin/jest", "test:watch": "npm run test -- --watch --coverage=false" }, diff --git a/src/commands/__snapshots__/help.test.ts.snap b/src/commands/__snapshots__/help.test.ts.snap new file mode 100644 index 0000000..f9e707c --- /dev/null +++ b/src/commands/__snapshots__/help.test.ts.snap @@ -0,0 +1,25 @@ +// Jest Snapshot v1, https://goo.gl/fbAQLP + +exports[`help presents an ephemeral embed with all available global and guild-bound commands 1`] = ` +Object { + "fields": Array [ + Object { + "name": "\`/help\`", + "value": "Prints the list of commands", + }, + ], + "title": "All commands", +} +`; + +exports[`help presents an ephemeral embed with all available global commands 1`] = ` +Object { + "fields": Array [ + Object { + "name": "\`/help\`", + "value": "Prints the list of commands", + }, + ], + "title": "All commands", +} +`; diff --git a/src/commands/help.test.ts b/src/commands/help.test.ts index 2165b19..3559c17 100644 --- a/src/commands/help.test.ts +++ b/src/commands/help.test.ts @@ -1,15 +1,80 @@ +import type { EmbedBuilder } from 'discord.js'; import { help } from './help'; +const { allCommands: realAllCommands } = jest.requireActual('./index'); +const mockAllCommands = new Map(); + +jest.mock('./index', () => ({ + allCommands: mockAllCommands, +})); + describe('help', () => { - test("it's just a Hello World", async () => { - const mockReply = jest.fn(); - const context = { + const mockReply = jest.fn(); + let context: CommandContext; + + beforeEach(() => { + mockAllCommands.clear(); + realAllCommands.forEach((value, key) => mockAllCommands.set(key, value)); + + context = { + guild: null, reply: mockReply, } as unknown as CommandContext; + }); + + test('presents an ephemeral embed with all available global commands', async () => { + context = { ...context, guild: null }; + + await expect(help.execute(context)).resolves.toBeUndefined(); + expect(mockReply).toHaveBeenCalledOnce(); + expect(mockReply).toHaveBeenCalledWith({ + embeds: [expect.toBeObject()], + ephemeral: true, + }); + const call = mockReply.mock.calls[0] as [{ embeds: [EmbedBuilder] }]; + const embed = call[0].embeds[0]; + expect(embed.data.fields).toBeArrayOfSize(mockAllCommands.size); + expect(embed.data).toMatchSnapshot(); + }); + + test('presents an ephemeral embed with all available global and guild-bound commands', async () => { + context = { + ...context, + guild: { id: 'the-guild-1234' }, + } as unknown as CommandContext; await expect(help.execute(context)).resolves.toBeUndefined(); + expect(mockReply).toHaveBeenCalledOnce(); + expect(mockReply).toHaveBeenCalledWith({ + embeds: [expect.toBeObject()], + ephemeral: true, + }); + const call = mockReply.mock.calls[0] as [{ embeds: [EmbedBuilder] }]; + const embed = call[0].embeds[0]; + expect(embed.data.fields).toBeArrayOfSize(mockAllCommands.size); + expect(embed.data).toMatchSnapshot(); + }); + test("doesn't show a guild-only command when in DMs", async () => { + const cmd: GuildedCommand = { + name: 'canttouchthis-do-do-do-do', // this test will break if we ever have a command with this name + description: "Can't touch this. (This is a test.)", + requiresGuild: true, + dmPermission: false, + execute() { + // nop + }, + }; + mockAllCommands.set(cmd.name, cmd); + + await expect(help.execute(context)).resolves.toBeUndefined(); expect(mockReply).toHaveBeenCalledOnce(); - expect(mockReply).toHaveBeenCalledWith('Hello, world!'); + expect(mockReply).toHaveBeenCalledWith({ + embeds: [expect.toBeObject()], + ephemeral: true, + }); + const call = mockReply.mock.calls[0] as [{ embeds: [EmbedBuilder] }]; + const embed = call[0].embeds[0]; + expect(embed.data.fields).toBeArrayOfSize(mockAllCommands.size - 1); }); }); diff --git a/src/commands/help.ts b/src/commands/help.ts index 5c81027..eb63686 100644 --- a/src/commands/help.ts +++ b/src/commands/help.ts @@ -1,9 +1,43 @@ +import { EmbedBuilder } from 'discord.js'; + export const help: GlobalCommand = { name: 'help', description: 'Prints the list of commands', requiresGuild: false, - async execute({ reply }) { - // TODO: finish this command - await reply('Hello, world!'); + async execute({ guild, reply }) { + // Dynamic import here b/c ./index depends on this file + const { allCommands } = await import('./index'); + + const embed = new EmbedBuilder() // + .setTitle('All commands'); + + function embedCommand(command: Command): void { + embed.addFields({ + name: `\`/${command.name}\``, // i.e. `/help` + value: command.description, + }); + } + + for (const command of allCommands.values()) { + if (guild) { + // We're in a guild. We should check the user's permissions, + // and skip this command if they aren't allowed to use it. + // TODO: Grab the guild's configured permissions for this channel + embedCommand(command); + continue; + } + + // We're in DMs; command should have dmPermission if we're to print it. + // Discord defaults dmPermission to `true`, so `undefined` should behave that way. + // See https://discordjs.guide/interactions/slash-commands.html#dm-permission + if (command.dmPermission === false) continue; + + embedCommand(command); + } + + await reply({ + embeds: [embed], + ephemeral: true, + }); }, };