Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Basic /help functionality #23

Merged
merged 5 commits into from Sep 29, 2022
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
7 changes: 5 additions & 2 deletions README.md
Expand Up @@ -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:
Expand Down Expand Up @@ -112,7 +113,9 @@ $ pm2 start .

## Commands

\[TBD\]
### /help

Prints the list of commands.

## Contributing

Expand Down
2 changes: 1 addition & 1 deletion package.json
Expand Up @@ -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"
},
Expand Down
25 changes: 25 additions & 0 deletions src/commands/__snapshots__/help.test.ts.snap
@@ -0,0 +1,25 @@
// Jest Snapshot v1, https://goo.gl/fbAQLP
JstnMcBrd marked this conversation as resolved.
Show resolved Hide resolved

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",
}
`;
73 changes: 69 additions & 4 deletions src/commands/help.test.ts
@@ -1,15 +1,80 @@
import type { EmbedBuilder } from 'discord.js';
import { help } from './help';

const { allCommands: realAllCommands } = jest.requireActual<typeof import('./index')>('./index');
const mockAllCommands = new Map<string, Command>();

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);
});
});
40 changes: 37 additions & 3 deletions 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;
}
gmacgre marked this conversation as resolved.
Show resolved Hide resolved

// 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);
AverageHelper marked this conversation as resolved.
Show resolved Hide resolved
}

await reply({
embeds: [embed],
ephemeral: true,
});
},
};