-
Notifications
You must be signed in to change notification settings - Fork 13
/
CommandRegistry.ts
177 lines (160 loc) · 4.68 KB
/
CommandRegistry.ts
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
import { Logger, logger } from '../util/logger/Logger';
import { Client } from '../client/Client';
import { Collection } from 'discord.js';
import { Command } from './Command';
/**
* @classdesc Stores loaded Commands in a Collection keyed by each Command's `name` property
* @class CommandRegistry
* @extends {external:Collection}
*/
export class CommandRegistry<
T extends Client,
K extends string = string,
V extends Command<T> = Command<T>>
extends Collection<K, V>
{
@logger('CommandRegistry')
private readonly _logger!: Logger;
private readonly _client!: T;
private readonly _reserved!: ((() => string) | string)[];
public constructor(client: T)
{
super();
Object.defineProperty(this, '_client', { value: client });
Object.defineProperty(this, '_reserved', {
value: [
() => this.has('limit' as K) ? 'clear' : null
]
});
}
public static get [Symbol.species](): any
{
return Collection as any;
}
/**
* Contains all [Command groups]{@link Command#group}
* @readonly
* @type {string[]}
*/
public get groups(): string[]
{
return Array.from(new Set(this.map(c => c.group)));
}
/**
* Register an external command and add it to the `<Client>.commands`
* [collection]{@link external:Collection}, erroring on duplicate
* aliases
*
* >**Note:** This is intended for Plugins to use to register external
* commands with the Client instance. Under normal circumstances
* commands should be added by placing them in the directory passed
* to the `commandsDir` YAMDBF Client option
* @param {Command} command The Command instance to be registered
* @returns {void}
*/
public registerExternal(command: Command<any>): void
{
this._logger.info(`External command loaded: ${command.name}`);
this.registerInternal(command as V, true);
}
/**
* Resolve the given Command name or alias to a registered Command
* @param {string} input Command name or alias
* @returns {Command | undefined}
*/
public resolve(input: string): V | undefined
{
// eslint-disable-next-line no-param-reassign
input = input ? input.toLowerCase() : input;
return this.find(c => c.name.toLowerCase() === input
|| Boolean(c.aliases.find(a => a.toLowerCase() === input)));
}
/**
* Complete registration of a command and add to the parent collection.
*
* This is an internal method and should not be used. Use
* `registerExternal()` instead
* @private
*/
public registerInternal(command: V, external: boolean = false): void
{
if (this.has(command.name as K))
{
if (!this.get(command.name as K)!.external)
this._logger.info(`Replacing previously loaded command: ${command.name}`);
else
this._logger.info(`Replacing externally loaded command: ${command.name}`);
this.delete(command.name as K);
}
this.set(command.name as K, command);
command.register(this._client);
if (external) command.external = true;
}
/**
* Check for duplicate aliases, erroring on any. Used internally
* @private
*/
public checkDuplicateAliases(): void
{
for (const command of this.values())
for (const alias of command.aliases)
{
const duplicate: V = this.filter(c => c !== command).find(c => c.aliases.includes(alias))!;
const name: string = command.name;
if (!duplicate)
continue;
if (!command.external)
throw new Error(
`Commands may not share aliases: ${name}, ${duplicate.name} (shared alias: "${alias}")`
);
else
throw new Error([
`External command "${duplicate.name}" has conflicting alias`,
`with "${name}" (shared alias: "${alias}")`
].join(' '));
}
}
/**
* Check for commands with reserved names. Used internally
* @private
*/
public checkReservedCommandNames(): void
{
const reserved: string[] = this._reserved.map(r => typeof r !== 'string' ? r() : r);
for (const name of reserved)
{
if (!name) continue;
const command: Command = this.resolve(name)!;
if (command)
throw new Error(`Command '${command.name}' is using reserved name or alias: '${name}'`);
}
}
/**
* Run the `init()` method of all loaded commands.
* This is an internal method and should not be used
* @private
*/
public async initCommands(): Promise<boolean>
{
let success: boolean = true;
for (const command of this.values())
{
if (command.initialized) continue;
try
{
// eslint-disable-next-line no-await-in-loop, @typescript-eslint/await-thenable
await command.init();
command.initialized = true;
}
catch (err)
{
success = false;
this._logger.error(
`Command "${command.name}" errored during initialization: \n\n${err.stack}`,
command.external ? '\n\nPlease report this error to the command author.\n' : '\n'
);
}
}
return success;
}
}