Skip to content
This repository was archived by the owner on Jan 8, 2022. It is now read-only.

Commit f0641e5

Browse files
Khasmsn1ckoatesiCrawl
authored
feat(ContextMenus): add context menu command builder (#29)
Co-authored-by: Nicholas Christopher <nicholaschristopher@protonmail.com> Co-authored-by: Keenser1 <36800359+Keenser1@users.noreply.github.com> Co-authored-by: Noel <buechler.noel@outlook.com>
1 parent 5f16430 commit f0641e5

File tree

5 files changed

+205
-2
lines changed

5 files changed

+205
-2
lines changed

src/index.ts

+3
Original file line numberDiff line numberDiff line change
@@ -13,3 +13,6 @@ export * from './interactions/slashCommands/options/number';
1313
export * from './interactions/slashCommands/options/role';
1414
export * from './interactions/slashCommands/options/string';
1515
export * from './interactions/slashCommands/options/user';
16+
17+
export * as ContextMenuCommandAssertions from './interactions/contextMenuCommands/Assertions';
18+
export * from './interactions/contextMenuCommands/ContextMenuCommandBuilder';
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,32 @@
1+
import ow from 'ow';
2+
import { ApplicationCommandType } from 'discord-api-types/v9';
3+
import type { ContextMenuCommandType } from './ContextMenuCommandBuilder';
4+
5+
export function validateRequiredParameters(name: string, type: number) {
6+
// Assert name matches all conditions
7+
validateName(name);
8+
9+
// Assert type is valid
10+
validateType(type);
11+
}
12+
13+
const namePredicate = ow.string
14+
.minLength(1)
15+
.maxLength(32)
16+
.matches(/^( *[\p{L}\p{N}_-]+ *)+$/u);
17+
18+
export function validateName(name: unknown): asserts name is string {
19+
ow(name, 'name', namePredicate);
20+
}
21+
22+
const typePredicate = ow.number.oneOf([ApplicationCommandType.User, ApplicationCommandType.Message]);
23+
24+
export function validateType(type: unknown): asserts type is ContextMenuCommandType {
25+
ow(type, 'type', typePredicate);
26+
}
27+
28+
const defaultPermissionPredicate = ow.boolean;
29+
30+
export function validateDefaultPermission(value: unknown): asserts value is boolean {
31+
ow(value, 'default_permission', defaultPermissionPredicate);
32+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,79 @@
1+
import { validateRequiredParameters, validateName, validateType, validateDefaultPermission } from './Assertions';
2+
import type { ApplicationCommandType, RESTPostAPIApplicationCommandsJSONBody } from 'discord-api-types/v9';
3+
4+
export class ContextMenuCommandBuilder {
5+
/**
6+
* The name of this context menu command
7+
*/
8+
public readonly name: string = undefined!;
9+
10+
/**
11+
* The type of this context menu command
12+
*/
13+
public readonly type: ContextMenuCommandType = undefined!;
14+
15+
/**
16+
* Whether the command is enabled by default when the app is added to a guild
17+
* @default true
18+
*/
19+
public readonly defaultPermission: boolean | undefined = undefined;
20+
21+
/**
22+
* Sets the name
23+
* @param name The name
24+
*/
25+
public setName(name: string) {
26+
// Assert the name matches the conditions
27+
validateName(name);
28+
29+
Reflect.set(this, 'name', name);
30+
31+
return this;
32+
}
33+
34+
/**
35+
* Sets the type
36+
* @param type The type
37+
*/
38+
public setType(type: ContextMenuCommandType) {
39+
// Assert the type is valid
40+
validateType(type);
41+
42+
Reflect.set(this, 'type', type);
43+
44+
return this;
45+
}
46+
47+
/**
48+
* Sets whether the command is enabled by default when the application is added to a guild.
49+
*
50+
* **Note**: If set to `false`, you will have to later `PUT` the permissions for this command.
51+
* @param value Whether or not to enable this command by default
52+
*
53+
* @see https://discord.com/developers/docs/interactions/application-commands#permissions
54+
*/
55+
public setDefaultPermission(value: boolean) {
56+
// Assert the value matches the conditions
57+
validateDefaultPermission(value);
58+
59+
Reflect.set(this, 'defaultPermission', value);
60+
61+
return this;
62+
}
63+
64+
/**
65+
* Returns the final data that should be sent to Discord.
66+
*
67+
* **Note:** Calling this function will validate required properties based on their conditions.
68+
*/
69+
public toJSON(): RESTPostAPIApplicationCommandsJSONBody {
70+
validateRequiredParameters(this.name, this.type);
71+
return {
72+
name: this.name,
73+
type: this.type,
74+
default_permission: this.defaultPermission,
75+
};
76+
}
77+
}
78+
79+
export type ContextMenuCommandType = ApplicationCommandType.User | ApplicationCommandType.Message;

src/interactions/slashCommands/SlashCommandBuilder.ts

+2-2
Original file line numberDiff line numberDiff line change
@@ -51,10 +51,10 @@ export class SlashCommandBuilder {
5151
/**
5252
* Sets whether the command is enabled by default when the application is added to a guild.
5353
*
54-
* **Note**: If set to `false`, you will have to later have to `PUT` the permissions for this command.
54+
* **Note**: If set to `false`, you will have to later `PUT` the permissions for this command.
5555
* @param value Whether or not to enable this command by default
5656
*
57-
* @see https://discord.com/developers/docs/interactions/slash-commands#permissions
57+
* @see https://discord.com/developers/docs/interactions/application-commands#permissions
5858
*/
5959
public setDefaultPermission(value: boolean) {
6060
// Assert the value matches the conditions

tests/ContextMenuCommands.test.ts

+89
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,89 @@
1+
import { ContextMenuCommandAssertions, ContextMenuCommandBuilder } from '../src/index';
2+
3+
const getBuilder = () => new ContextMenuCommandBuilder();
4+
5+
describe('Context Menu Commands', () => {
6+
describe('Assertions tests', () => {
7+
test('GIVEN valid name THEN does not throw error', () => {
8+
expect(() => ContextMenuCommandAssertions.validateName('ping')).not.toThrowError();
9+
});
10+
11+
test('GIVEN invalid name THEN throw error', () => {
12+
expect(() => ContextMenuCommandAssertions.validateName(null)).toThrowError();
13+
14+
// Too short of a name
15+
expect(() => ContextMenuCommandAssertions.validateName('')).toThrowError();
16+
17+
// Invalid characters used
18+
expect(() => ContextMenuCommandAssertions.validateName('ABC123$%^&')).toThrowError();
19+
20+
// Too long of a name
21+
expect(() =>
22+
ContextMenuCommandAssertions.validateName('qwertyuiopasdfghjklzxcvbnmqwertyuiopasdfghjklzxcvbnm'),
23+
).toThrowError();
24+
});
25+
26+
test('GIVEN valid type THEN does not throw error', () => {
27+
expect(() => ContextMenuCommandAssertions.validateType(3)).not.toThrowError();
28+
});
29+
30+
test('GIVEN invalid type THEN throw error', () => {
31+
expect(() => ContextMenuCommandAssertions.validateType(null)).toThrowError();
32+
33+
// Out of range
34+
expect(() => ContextMenuCommandAssertions.validateType(1)).toThrowError();
35+
});
36+
37+
test('GIVEN valid required parameters THEN does not throw error', () => {
38+
expect(() => ContextMenuCommandAssertions.validateRequiredParameters('owo', 2)).not.toThrowError();
39+
});
40+
41+
test('GIVEN valid default_permission THEN does not throw error', () => {
42+
expect(() => ContextMenuCommandAssertions.validateDefaultPermission(true)).not.toThrowError();
43+
});
44+
45+
test('GIVEN invalid default_permission THEN throw error', () => {
46+
expect(() => ContextMenuCommandAssertions.validateDefaultPermission(null)).toThrowError();
47+
});
48+
});
49+
50+
describe('ContextMenuCommandBuilder', () => {
51+
describe('Builder tests', () => {
52+
test('GIVEN empty builder THEN throw error when calling toJSON', () => {
53+
expect(() => getBuilder().toJSON()).toThrowError();
54+
});
55+
56+
test('GIVEN valid builder THEN does not throw error', () => {
57+
expect(() => getBuilder().setName('example').setType(3).toJSON()).not.toThrowError();
58+
});
59+
60+
test('GIVEN invalid name THEN throw error', () => {
61+
expect(() => getBuilder().setName('$$$')).toThrowError();
62+
63+
expect(() => getBuilder().setName(' ')).toThrowError();
64+
});
65+
66+
test('GIVEN valid names THEN does not throw error', () => {
67+
expect(() => getBuilder().setName('hi_there')).not.toThrowError();
68+
69+
expect(() => getBuilder().setName('A COMMAND')).not.toThrowError();
70+
71+
// Translation: a_command
72+
expect(() => getBuilder().setName('o_comandă')).not.toThrowError();
73+
74+
// Translation: thx (according to GTranslate)
75+
expect(() => getBuilder().setName('どうも')).not.toThrowError();
76+
});
77+
78+
test('GIVEN valid types THEN does not throw error', () => {
79+
expect(() => getBuilder().setType(2)).not.toThrowError();
80+
81+
expect(() => getBuilder().setType(3)).not.toThrowError();
82+
});
83+
84+
test('GIVEN valid builder with defaultPermission false THEN does not throw error', () => {
85+
expect(() => getBuilder().setName('foo').setDefaultPermission(false)).not.toThrowError();
86+
});
87+
});
88+
});
89+
});

0 commit comments

Comments
 (0)