/
SapphireClient.ts
396 lines (348 loc) · 12.3 KB
/
SapphireClient.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
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
import { container, Store, StoreRegistry } from '@sapphire/pieces';
import type { Awaitable } from '@sapphire/utilities';
import { Client, ClientOptions, Message, Snowflake } from 'discord.js';
import { join } from 'path';
import type { Plugin } from './plugins/Plugin';
import { PluginManager } from './plugins/PluginManager';
import { ArgumentStore } from './structures/ArgumentStore';
import { CommandStore } from './structures/CommandStore';
import { InteractionHandlerStore } from './structures/InteractionHandlerStore';
import { ListenerStore } from './structures/ListenerStore';
import { PreconditionStore } from './structures/PreconditionStore';
import { BucketScope, PluginHook } from './types/Enums';
import { Events } from './types/Events';
import { acquire } from './utils/application-commands/ApplicationCommandRegistries';
import { ILogger, LogLevel } from './utils/logger/ILogger';
import { Logger } from './utils/logger/Logger';
container.applicationCommandRegistries = { acquire };
/**
* A valid prefix in Sapphire.
* * `string`: a single prefix, e.g. `'!'`.
* * `string[]`: an array of prefixes, e.g. `['!', '.']`.
* * `null`: disabled prefix, locks the bot's command usage to mentions only.
*/
export type SapphirePrefix = string | readonly string[] | null;
export interface SapphirePrefixHook {
(message: Message): Awaitable<SapphirePrefix>;
}
export interface SapphireClientOptions {
/**
* The base user directory, if set to `null`, Sapphire will not call {@link StoreRegistry.registerPath},
* meaning that you will need to manually set each folder for each store. Please read the aforementioned method's
* documentation for more information.
* @since 1.0.0
* @default undefined
*/
baseUserDirectory?: URL | string | null;
/**
* Whether commands can be case insensitive
* @since 1.0.0
* @default false
*/
caseInsensitiveCommands?: boolean | null;
/**
* Whether prefixes can be case insensitive
* @since 1.0.0
* @default false
*/
caseInsensitivePrefixes?: boolean | null;
/**
* The default prefix, in case of `null`, only mention prefix will trigger the bot's commands.
* @since 1.0.0
* @default null
*/
defaultPrefix?: SapphirePrefix;
/**
* The regex prefix, an alternative to a mention or regular prefix to allow creating natural language command messages
* @since 1.0.0
* @example
* ```typescript
* /^(hey +)?bot[,! ]/i
*
* // Matches:
* // - hey bot,
* // - hey bot!
* // - hey bot
* // - bot,
* // - bot!
* // - bot
* ```
*/
regexPrefix?: RegExp;
/**
* The prefix hook, by default it is a callback function that returns {@link SapphireClientOptions.defaultPrefix}.
* @since 1.0.0
* @default () => client.options.defaultPrefix
*/
fetchPrefix?: SapphirePrefixHook;
/**
* The client's ID, this is automatically set by the CoreReady event.
* @since 1.0.0
* @default this.client.user?.id ?? null
*/
id?: Snowflake;
/**
* The logger options, defaults to an instance of {@link Logger} when {@link ClientLoggerOptions.instance} is not specified.
* @since 1.0.0
* @default { instance: new Logger(LogLevel.Info) }
*/
logger?: ClientLoggerOptions;
/**
* Whether or not trace logging should be enabled.
* @since 2.0.0
* @default container.logger.has(LogLevel.Trace)
*/
enableLoaderTraceLoggings?: boolean;
/**
* If Sapphire should load the pre-included error event listeners that log any encountered errors to the {@link SapphireClient.logger} instance
* @since 1.0.0
* @default true
*/
loadDefaultErrorListeners?: boolean;
/**
* If Sapphire should load the pre-included message command listeners that are used to process incoming messages for commands.
* @since 3.0.0
* @default false
*/
loadMessageCommandListeners?: boolean;
/**
* Controls whether the bot will automatically appear to be typing when a command is accepted.
* @default false
*/
typing?: boolean;
/**
* Sets the default cooldown time for all commands.
* @default "No cooldown options"
*/
defaultCooldown?: CooldownOptions;
/**
* Controls whether the bot has mention as a prefix disabled
* @default false
*/
disableMentionPrefix?: boolean;
/**
* Whenever starting the bot process Sapphire may report errors when failing to fetch guild commands.
* One of the causes for this can be when a bot was invited to a server without the `application.commands` scope.
*
* Normally this produce a log in the console at the WARN level, however because bot lists have a tendency to invite your
* bot specifically without the scope to ensure that your Chat Input and Context Menu commands do not show up as usable commands
* in that server, you may want to include their guild ids in this list.
*
* By adding ids to this list, whenever a guild id matches one of the ids in the list no warning log message will be emitted for that guild.
*
* By setting this value to `true`, no warning log message will be emitted for any guilds we couldn't fetch the commands from.
*
* Note that this specifically applies to the warning log:
*
* > ApplicationCommandRegistries: Failed to fetch guild commands for guild \<guild name\> (\<guild id\>). Make sure to authorize your application with the "applications.commands" scope in that guild.
*/
preventFailedToFetchLogForGuilds?: string[] | true;
}
/**
* The base {@link Client} extension that makes Sapphire work. When building a Discord bot with the framework, the developer
* must either use this class, or extend it.
*
* Sapphire also automatically detects the folders to scan for pieces, please read {@link StoreRegistry.registerPath}
* for reference. This method is called at the start of the {@link SapphireClient.login} method.
*
* @see {@link SapphireClientOptions} for all options available to the Sapphire Client. You can also provide all of discord.js' [ClientOptions](https://discord.js.org/#/docs/main/stable/typedef/ClientOptions)
*
* @since 1.0.0
* @example
* ```typescript
* const client = new SapphireClient({
* presence: {
* activity: {
* name: 'for commands!',
* type: 'LISTENING'
* }
* }
* });
*
* client.login(process.env.DISCORD_TOKEN)
* .catch(console.error);
* ```
*
* @example
* ```typescript
* // Automatically scan from a specific directory, e.g. the main
* // file is at `/home/me/bot/index.js` and all your pieces are at
* // `/home/me/bot/pieces` (e.g. `/home/me/bot/pieces/commands/MyCommand.js`):
* const client = new SapphireClient({
* baseUserDirectory: join(__dirname, 'pieces'),
* // More options...
* });
* ```
*
* @example
* ```typescript
* // Opt-out automatic scanning:
* const client = new SapphireClient({
* baseUserDirectory: null,
* // More options...
* });
* ```
*/
export class SapphireClient<Ready extends boolean = boolean> extends Client<Ready> {
/**
* The client's ID, used for the user prefix.
* @since 1.0.0
*/
public id: Snowflake | null = null;
/**
* The method to be overridden by the developer.
* @since 1.0.0
* @return A string for a single prefix, an array of strings for matching multiple, or null for no match (mention prefix only).
* @example
* ```typescript
* // Return always the same prefix (unconfigurable):
* client.fetchPrefix = () => '!';
* ```
* @example
* ```typescript
* // Retrieving the prefix from a SQL database:
* client.fetchPrefix = async (message) => {
* // note: driver is something generic and depends on how you connect to your database
* const guild = await driver.getOne('SELECT prefix FROM public.guild WHERE id = $1', [message.guild.id]);
* return guild?.prefix ?? '!';
* };
* ```
* @example
* ```typescript
* // Retrieving the prefix from an ORM:
* client.fetchPrefix = async (message) => {
* // note: driver is something generic and depends on how you connect to your database
* const guild = await driver.getRepository(GuildEntity).findOne({ id: message.guild.id });
* return guild?.prefix ?? '!';
* };
* ```
*/
public fetchPrefix: SapphirePrefixHook;
/**
* The logger to be used by the framework and plugins. By default, a {@link Logger} instance is used, which emits the
* messages to the console.
* @since 1.0.0
*/
public logger: ILogger;
/**
* Whether the bot has mention as a prefix disabled
* @default false
* @example
* ```typescript
* client.disableMentionPrefix = false;
* ```
*/
public disableMentionPrefix?: boolean;
/**
* The registered stores.
* @since 1.0.0
*/
public stores: StoreRegistry;
public constructor(options: ClientOptions) {
super(options);
container.client = this;
for (const plugin of SapphireClient.plugins.values(PluginHook.PreGenericsInitialization)) {
plugin.hook.call(this, options);
this.emit(Events.PluginLoaded, plugin.type, plugin.name);
}
this.logger = options.logger?.instance ?? new Logger(options.logger?.level ?? LogLevel.Info);
container.logger = this.logger;
if (options.enableLoaderTraceLoggings ?? container.logger.has(LogLevel.Trace)) {
Store.logger = container.logger.trace.bind(container.logger);
}
this.stores = new StoreRegistry();
container.stores = this.stores;
this.fetchPrefix = options.fetchPrefix ?? (() => this.options.defaultPrefix ?? null);
this.disableMentionPrefix = options.disableMentionPrefix;
for (const plugin of SapphireClient.plugins.values(PluginHook.PreInitialization)) {
plugin.hook.call(this, options);
this.emit(Events.PluginLoaded, plugin.type, plugin.name);
}
this.id = options.id ?? null;
this.stores
.register(new ArgumentStore().registerPath(join(__dirname, '..', 'arguments'))) //
.register(new CommandStore())
.register(new InteractionHandlerStore())
.register(new ListenerStore().registerPath(join(__dirname, '..', 'listeners')))
.register(new PreconditionStore().registerPath(join(__dirname, '..', 'preconditions')));
const optionalListenersPath = join(__dirname, '..', 'optional-listeners');
if (options.loadDefaultErrorListeners !== false) {
this.stores.get('listeners').registerPath(join(optionalListenersPath, 'error-listeners'));
}
if (options.loadMessageCommandListeners === true) {
this.stores.get('listeners').registerPath(join(optionalListenersPath, 'message-command-listeners'));
}
for (const plugin of SapphireClient.plugins.values(PluginHook.PostInitialization)) {
plugin.hook.call(this, options);
this.emit(Events.PluginLoaded, plugin.type, plugin.name);
}
}
/**
* Loads all pieces, then logs the client in, establishing a websocket connection to Discord.
* @since 1.0.0
* @param token Token of the account to log in with.
* @return Token of the account used.
*/
public async login(token?: string) {
// Register the user directory if not null:
if (this.options.baseUserDirectory !== null) {
this.stores.registerPath(this.options.baseUserDirectory);
}
// Call pre-login plugins:
for (const plugin of SapphireClient.plugins.values(PluginHook.PreLogin)) {
await plugin.hook.call(this, this.options);
this.emit(Events.PluginLoaded, plugin.type, plugin.name);
}
// Loads all stores, then call login:
await Promise.all([...this.stores.values()].map((store) => store.loadAll()));
const login = await super.login(token);
// Call post-login plugins:
for (const plugin of SapphireClient.plugins.values(PluginHook.PostLogin)) {
await plugin.hook.call(this, this.options);
this.emit(Events.PluginLoaded, plugin.type, plugin.name);
}
return login;
}
public static plugins = new PluginManager();
public static use(plugin: typeof Plugin) {
this.plugins.use(plugin);
return this;
}
}
export interface ClientLoggerOptions {
level?: LogLevel;
instance?: ILogger;
}
export interface CooldownOptions {
scope?: BucketScope;
delay?: number;
limit?: number;
filteredUsers?: Snowflake[];
filteredCommands?: string[];
}
declare module 'discord.js' {
interface Client {
id: Snowflake | null;
logger: ILogger;
stores: StoreRegistry;
fetchPrefix: SapphirePrefixHook;
}
interface ClientOptions extends SapphireClientOptions {}
}
declare module '@sapphire/pieces' {
interface Container {
client: SapphireClient;
logger: ILogger;
stores: StoreRegistry;
applicationCommandRegistries: {
acquire: typeof acquire;
};
}
interface StoreRegistryEntries {
arguments: ArgumentStore;
commands: CommandStore;
'interaction-handlers': InteractionHandlerStore;
listeners: ListenerStore;
preconditions: PreconditionStore;
}
}