/
Command.ts
220 lines (195 loc) · 7.65 KB
/
Command.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
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
/** @module Yuuko */
import * as Eris from 'eris';
import {EventContext} from './Yuuko';
import {makeArray} from './util';
/** Check if requirements are met. */
// TODO this interface is ugly
async function fulfillsRequirements (requirements: CommandRequirements, msg: Eris.Message, args: string[], ctx: CommandContext) {
const {owner, guildOnly, dmOnly, permissions, custom} = requirements;
const {client} = ctx;
// Owner checking
if (owner) {
// If the bot's application info isn't loaded, we can't confirm anything
if (!client.app) return false;
if (client.app.team) {
// If the bot is owned by a team, we check their ID and team role
// (as of 2020-09-29, Admin/2 is the only role/membership_state)
// TODO: Remove type assertion after abalabahaha/eris#1171
if (!(client.app.team.members as unknown as Eris.OAuthTeamMember[]).some(member => member.membership_state === 2 && member.user.id === msg.author.id)) {
return false;
}
} else if (client.app.owner.id !== msg.author.id) {
// If the bot is owned by a single user, we check their ID directly
return false;
}
}
// Guild-only commands
if (guildOnly) {
if (!msg.guildID) {
return false;
}
}
// DM-only commands
if (dmOnly) {
if (msg.guildID) {
return false;
}
}
// Permissions
if (permissions && permissions.length > 0) {
// Permission checks only make sense in guild channels
if (!(msg.channel instanceof Eris.GuildChannel)) {
return false;
}
// Calculate permissions of the user and check all we need
const memberPerms = msg.channel.permissionsOf(msg.author.id);
for (const permission of permissions) {
if (!memberPerms.has(permission)) {
return false;
}
}
}
// Custom requirement function
if (custom && !await custom(msg, args, ctx)) {
return false;
}
// If we haven't returned yet, all requirements are met
return true;
}
/** An object of requirements a user must meet to use the command. */
export interface CommandRequirements {
/** If true, the user must be the bot's owner. */
owner?: boolean;
/** If true, the message must be sent in a server channel. */
guildOnly?: boolean;
/** If true, the message must be sent in a DM channel. */
dmOnly?: boolean;
/**
* A list of permission strings the user must have. If set, the `guildOnly`
* option is implied.
*/
permissions?: (keyof Eris.Constants['Permissions'])[];
/** A custom function that must return true to enable the command. */
custom?(msg: Eris.Message, args: string[], ctx: CommandContext): boolean | Promise<boolean>;
}
/** An object containing context information for a command's execution. */
export interface CommandContext extends EventContext {
/** The prefix used to call the command. */
prefix: string;
/** The name or alias used to call the command. */
commandName?: string;
}
/** The function to be called when a command is executed. */
export interface CommandProcess<T extends Eris.Textable = Eris.TextableChannel> {
(
/** The message object from Eris. */
msg: Eris.Message<T>,
/** A space-separated list of arguments to the command. */
args: string[],
/** An object containing additional context information. */
ctx: CommandContext,
): void;
}
export type GuildCommandProcess = CommandProcess<Eris.GuildTextableChannel>;
export type PrivateCommandProcess = CommandProcess<Eris.PrivateChannel>;
/** Class representing a command. */
export class Command {
/**
* A list of the command's names. The first should be considered the
* command's canonical or display name. All characters must be lowercase if
* the client option `caseSensitiveCommands` is false or unset.
*/
names: string[];
/** The function executed when the command is triggered. */
process: CommandProcess | GuildCommandProcess | PrivateCommandProcess
/** The requirements for the command being triggered. */
requirements: CommandRequirements;
/** The name of the file the command was loaded from, if any. */
filename?: string;
/** Subcommands of this command. */
subcommands: Command[] = [];
// For some reason, I cannot get TS to recognize that `CommandProcess` is a
// superset of `GuildCommandProcess` and `PrivateCommandProcess`, so for
// now we have one more override than we should really need. Oh well.
// TODO: Does microsoft/typescript#31023 fix this?
constructor(names: string | string[], process: CommandProcess, requirements?: CommandRequirements);
constructor(names: string | string[], process: GuildCommandProcess, requirements: CommandRequirements & { guildOnly: true; dmOnly?: false });
constructor(names: string | string[], process: PrivateCommandProcess, requirements: CommandRequirements & { dmOnly: true; guildOnly?: false })
constructor (names: string | string[], process: CommandProcess | GuildCommandProcess | PrivateCommandProcess, requirements?: CommandRequirements) {
if (Array.isArray(names)) {
this.names = names;
} else {
this.names = [names];
}
if (!this.names[0]) throw new TypeError('At least one name is required');
this.process = process;
if (!this.process) throw new TypeError('Process is required');
this.requirements = {};
if (requirements) {
if (requirements.owner) {
this.requirements.owner = true;
}
if (requirements.permissions) {
this.requirements.permissions = makeArray(requirements.permissions);
}
if (requirements.custom) {
this.requirements.custom = requirements.custom;
}
}
}
/** Checks whether or not a command can be executed. */
async checkPermissions (msg: Eris.Message, args: string[], ctx: CommandContext): Promise<boolean> {
if (!ctx.client.ignoreGlobalRequirements) {
if (!await fulfillsRequirements(ctx.client.globalCommandRequirements, msg, args, ctx)) {
return false;
}
}
return fulfillsRequirements(this.requirements, msg, args, ctx);
}
/**
* Adds a subcommand to this command.
* @param command The subcommand to add
*/
addSubcommand (command: Command): this {
for (const name of command.names) {
for (const otherCommand of this.subcommands) {
if (otherCommand.names.includes(name)) {
throw new TypeError(`Two commands have the same name: ${name}`);
}
}
}
this.subcommands.push(command);
return this;
}
/**
* Checks the list of subcommands and returns one whch is known by a given
* name. Passing an empty string will return the default command, if any.
*/
subcommandForName (name: string, caseSensitive: boolean): Command | null {
if (caseSensitive) return this.subcommands.find(c => c.names.includes(name)) || null;
return this.subcommands.find(c => c.names.some(n => n.toLowerCase() === name.toLowerCase())) || null;
}
/** Executes the command process if the permission checks pass. */
async execute (msg: Eris.Message, args: string[], ctx: CommandContext): Promise<boolean> {
if (!await this.checkPermissions(msg, args, ctx)) return false;
// Check if we have a subcommand, and if so, execute that command
if (args.length) {
const subcommand = this.subcommandForName(args[0], ctx.client.caseSensitiveCommands);
if (subcommand) {
// TODO: Might want to handle this as an array instead, but doing it
// this way for now for backwards compatibility
ctx.commandName += ` ${args.shift()}`;
return subcommand.execute(msg, args, ctx);
}
}
// We have no subcommand, so call this command's process
// NOTE: By calling checkPermissions and returning early if it returns
// false, we guarantee that messages will be the correct type for
// the stored process, so this call is always safe. Restructuring
// this to properly use TS type guards would be very messy and
// would result in duplicate safety checks that we want to avoid.
// @ts-ignore
this.process(msg, args, ctx);
return true;
}
}