diff --git a/src/client/DiscordClient.ts b/src/client/DiscordClient.ts index c2a06c6..df78fe1 100644 --- a/src/client/DiscordClient.ts +++ b/src/client/DiscordClient.ts @@ -116,6 +116,15 @@ declare interface DiscordClient extends Client { on(event: K, listener: EventHook): this } +/** + * Client class + * Should not house much functionality itself, + * rather it should mostly house components that implement behaviour. + * + * @class DiscordClient + * @typedef {DiscordClient} + * @extends {Client} + */ class DiscordClient extends Client { #logger: Logger; @@ -204,13 +213,6 @@ class DiscordClient extends Client this.#resolver = new Resolver(this); this.#rateLimiter = new RateLimiter(this); - - // As of d.js v14 these events are emitted from the rest manager, rebinding them to the client - // this.rest.on('request', (...args) => - // { - // this.emit('apiRequest', ...args); - // }); - this.rest.on('response', (...args) => { this.emit('apiResponse', ...args); @@ -228,10 +230,6 @@ class DiscordClient extends Client this.#loadEevents(); - // process.on('uncaughtException', (err) => { - // this.logger.error(`Uncaught exception:\n${err.stack || err}`); - // }); - process.on('unhandledRejection', (err: Error) => { this.#logger.error(`Unhandled rejection:\n${err?.stack || err}`); @@ -314,7 +312,6 @@ class DiscordClient extends Client this.shutdown(); } - // eslint-disable-next-line @typescript-eslint/ban-types async managerEval (script: ((controller: Controller) => Promise | Result) | string, options: ManagerEvalOptions = {}) : Promise { @@ -384,6 +381,7 @@ class DiscordClient extends Client } // Helper function to pass options to the logger in a unified way + // also avoids having to import the logger everywhere createLogger (comp: object, options: LoggerClientOptions = {}) { return new Logger({ name: comp.constructor.name, ...this.#options.logger, ...options }); @@ -445,17 +443,6 @@ class DiscordClient extends Client return this.shard.ids[0]; } - // on(event: K, listener: (...args: unknown[]) => Awaitable): this; - // on (event: K, listener: (...args: unknown[]) => Awaitable) - // { - // // eslint-disable-next-line @typescript-eslint/ban-ts-comment - // // @ts-ignore - // return super.on(event, listener); - // } - - /** - * @private - */ #loadEevents () { this.#eventHooker.hook('ready', () => @@ -518,9 +505,6 @@ class DiscordClient extends Client } - /** - * @private - */ #createWrappers () { this.guilds.cache.forEach((guild) => @@ -567,7 +551,6 @@ class DiscordClient extends Client return wrapper; } - // Signatures for typescript inferral getUserWrapper(resolveable: UserResolveable, fetch?: false): UserWrapper | null; getUserWrapper(resolveable: UserResolveable, fetch?: true): Promise; getUserWrapper (resolveable: UserResolveable, fetch = true) @@ -631,13 +614,6 @@ class DiscordClient extends Client return this.#built; } - // override get user () - // { - // if (!super.user) - // throw new Error('User not set'); - // return super.user; - // } - get prefix () { return this.#options.prefix; @@ -745,6 +721,8 @@ class DiscordClient extends Client } +// This is what actually starts the client. +// Just running this file directly will not start the client and the process will just hang. process.once('message', (msg: IPCMessage) => { if (msg._start) @@ -754,8 +732,4 @@ process.once('message', (msg: IPCMessage) => } }); -export default DiscordClient; - -// process.on("unhandledRejection", (error) => { -// console.error("[DiscordClient.js] Unhandled promise rejection:", error); //eslint-disable-line no-console -// }); \ No newline at end of file +export default DiscordClient; \ No newline at end of file diff --git a/src/client/components/EventHooker.ts b/src/client/components/EventHooker.ts index 438a0ce..665ba5b 100644 --- a/src/client/components/EventHooker.ts +++ b/src/client/components/EventHooker.ts @@ -22,6 +22,15 @@ import { EventHook } from '../../../@types/Client.js'; import { ClientEvents } from '../../../@types/Events.js'; import { Interaction } from 'discord.js'; +/** + * Hooks events to their handlers. + * This is done in order to dispatch events to their handlers sequentially. + * The order of handler execution matters due to priority and to avoid + * race conditions and simultaneous execution in filter hooks. + * + * @class EventHooker + * @typedef {EventHooker} + */ class EventHooker { #target: DiscordClient; @@ -84,9 +93,8 @@ class EventHooker } let guild = null; - // this.logger.debug(`Handler ${eventName}`); - // Should probably move this elsewhere, but testing this out -- or maybe not, might be the most appropriate place for this const eventArgs = []; + // Add guildWrapper property to d.js structures for (const arg of args) { if (arg && typeof arg === 'object' && 'guild' in arg && arg.guild) @@ -102,19 +110,9 @@ class EventHooker if (eventName === 'interactionCreate') { - // const idx = eventArgs.findIndex(val => - // { - // console.log(val!.constructor.name, val instanceof BaseInteraction, val instanceof ChatInputCommandInteraction); - // return val instanceof BaseInteraction; - // }); - // // ChatInputCommandInteraction; - // console.log(idx, args); eventArgs[0] = new InteractionWrapper(this.#target, args[0] as Interaction); } - // async-await does nothing in a .forEach loop, - // the forEach implementation does not await the results of the function before moving onto the next iteration - // which is a problem if we don't want functionality to be overlapping, i.e. different filters trying to delete the same message for (const handler of this.#events.get(eventName) || []) { if (process.env.NODE_ENV === 'development') diff --git a/src/client/components/Intercom.ts b/src/client/components/Intercom.ts index 429d85f..889cd2b 100644 --- a/src/client/components/Intercom.ts +++ b/src/client/components/Intercom.ts @@ -17,6 +17,14 @@ import DiscordClient from '../DiscordClient.js'; import SlashCommand from '../interfaces/commands/SlashCommand.js'; +/** + * IPC module. + * Currently primarily only used to tell the controller to update slash commands. + * Wraps sending messages to the main process. + * + * @class Intercom + * @typedef {Intercom} + */ class Intercom { #client: DiscordClient; diff --git a/src/client/components/LocaleLoader.ts b/src/client/components/LocaleLoader.ts index 3e2bc3a..1b87a94 100644 --- a/src/client/components/LocaleLoader.ts +++ b/src/client/components/LocaleLoader.ts @@ -31,6 +31,16 @@ type Languages = { [key: string]: Language } +/** + * Takes care of localisation. + * Loads languages from files and takes care of formatting the strings. + * + * This class should rarely be called directly from most code, + * rather used through wrappers that fill in options, e.g. GuildWrapper.format + * + * @class LocaleLoader + * @typedef {LocaleLoader} + */ class LocaleLoader { #logger: LoggerClient; diff --git a/src/client/components/RateLimiter.ts b/src/client/components/RateLimiter.ts index 360cf60..a2417b7 100644 --- a/src/client/components/RateLimiter.ts +++ b/src/client/components/RateLimiter.ts @@ -30,6 +30,17 @@ type DeleteQueueEntry = { message: Message, } & QueueEntry +/** + * Utility class used for limiting the amount of messages the bot sends. + * Most useful when filtering and sending certain log messages. + * + * Filters usually notify the user about their message being deleted, but that notice should be sent out infrequently. + * Logs like member logs may sometimes have to send out multiple messages in a short span, and can be batched easily. + * Also used when batching deletions. + * + * @class RateLimiter + * @typedef {RateLimiter} + */ class RateLimiter { #client: DiscordClient; diff --git a/src/client/components/Registry.ts b/src/client/components/Registry.ts index b65695d..9dd5440 100644 --- a/src/client/components/Registry.ts +++ b/src/client/components/Registry.ts @@ -24,6 +24,13 @@ import { Command, Component, Module, Setting } from '../interfaces/index.js'; import { Util } from '../../utilities/index.js'; import { isInitialisable } from '../interfaces/Initialisable.js'; +/** + * Component registry. + * Takes care of instantiating and storing most components. + * + * @class Registry + * @typedef {Registry} + */ class Registry { #client: DiscordClient; diff --git a/src/client/components/Resolver.ts b/src/client/components/Resolver.ts index ddadffe..60541e6 100644 --- a/src/client/components/Resolver.ts +++ b/src/client/components/Resolver.ts @@ -38,17 +38,19 @@ const filterInexact = (search: string) => (comp: T) => comp || ((comp instanceof Command || comp instanceof Setting) && comp.aliases && (comp.aliases.some((ali) => `${comp.type}:${ali}`.toLowerCase().includes(search))) || ((comp instanceof Command || comp instanceof Setting) && comp.aliases.some((ali) => ali.toLowerCase().includes(search)))); +/** + * Resolves various structures. + * E.g. string -> discord user + * + * @class Resolver + * @typedef {Resolver} + */ class Resolver { #client: DiscordClient; #dnsresolver: DNSResolver; #logger: LoggerClient; - /** - * Creates an instance of Resolver. - * @param {DiscordClient} client - * @memberof Resolver - */ constructor (client: DiscordClient) { this.#client = client; diff --git a/src/client/interfaces/Infraction.ts b/src/client/interfaces/Infraction.ts index c5f9e3c..ad39b90 100644 --- a/src/client/interfaces/Infraction.ts +++ b/src/client/interfaces/Infraction.ts @@ -52,6 +52,14 @@ const Constants = { RemovedInfractions: [ 'BAN', 'SOFTBAN', 'KICK' ] }; +/** + * Base Infraction class. + * This class is responsible for logging and storing the infraction. + * Subclasses are responsible for implementing the behaviour of the infraction. + * + * @class Infraction + * @typedef {Infraction} + */ class Infraction { static get Type (): InfractionType diff --git a/src/client/interfaces/Inhibitor.ts b/src/client/interfaces/Inhibitor.ts index 8e2a96f..a238074 100644 --- a/src/client/interfaces/Inhibitor.ts +++ b/src/client/interfaces/Inhibitor.ts @@ -21,6 +21,16 @@ import InvokerWrapper from '../components/wrappers/InvokerWrapper.js'; import Component from './Component.js'; import { Command } from './index.js'; +/** + * Base inhibitor class. + * Inhibitors are what stop commands from executing in the wrong conditions, + * e.g. if a user is missing permissions or the channel is ignored. + * + * @abstract + * @class Inhibitor + * @typedef {Inhibitor} + * @extends {Component} + */ abstract class Inhibitor extends Component { #name: string; diff --git a/src/client/interfaces/Observer.ts b/src/client/interfaces/Observer.ts index e8d289b..2aa8123 100644 --- a/src/client/interfaces/Observer.ts +++ b/src/client/interfaces/Observer.ts @@ -20,6 +20,15 @@ import DiscordClient from '../DiscordClient.js'; import Component from './Component.js'; import { LoggerClient } from '@navy.gif/logger'; +/** + * Base Observer class. + * Observers are what drives the bot's functionality. They listen to the events emitted by Discord. + * Most important ones being the CommandHandler and Automoderation classes. + * + * @class Observer + * @typedef {Observer} + * @extends {Component} + */ class Observer extends Component { #logger: LoggerClient; diff --git a/src/client/interfaces/Setting.ts b/src/client/interfaces/Setting.ts index 56b1639..983267b 100644 --- a/src/client/interfaces/Setting.ts +++ b/src/client/interfaces/Setting.ts @@ -37,40 +37,13 @@ import CommandOption from './CommandOption.js'; import { GuildSettingTypes as GuildSettingType } from '../../../@types/Guild.js'; import { SettingModifyResult, SettingResult } from '../../../@types/Commands/Settings.js'; -// const EMOJIS = { -// 'GUILD_TEXT': { -// name: 'textchannel', -// id: '716414423094525952' -// }, -// 'GUILD_VOICE': { -// name: 'voicechannel', -// id: '716414422662512762' -// }, -// 'GUILD_NEWS': { -// name: 'news', -// id: '741725913171099810' -// }, -// 'GUILD_CATEGORY': { -// name: 'category', -// id: '741731053818871901' -// }, -// 'GUILD_ROLE': { -// name: 'role', -// id: '743563678292639794' -// } -// }; - type HelperResult = { modified: string[] } -// declare interface Setting extends Component { -// add: () => HelperResult; -// remove: () => HelperResult; -// set: () => HelperResult; -// } - /** + * Base setting class. + * * @class Setting * @extends {Component} */ @@ -243,22 +216,6 @@ abstract class Setting extends Component fields.push({ name: `》 ${guild.format('GENERAL_OPTIONS')}`, value: options.map((opt) => opt.usage(guild, true)).map((f) => f.value).join('\n\n') - // value: options.map( - // (opt) => { - // let msg = `**${opt.name} [${opt.type}]:** ${opt.description}`; - // if (opt.choices.length) - // msg += `\n__${guild.format('GENERAL_CHOICES')}__: ${opt.choices.map((choice) => choice.name).join(', ')}`; - // if (opt.dependsOn.length) - // msg += `\n${guild.format('GENERAL_DEPENDSON', { dependencies: opt.dependsOn.join('`, `') })}`; - // if (opt.minimum !== undefined) - // msg += `\nMIN: \`${opt.minimum}\``; - // if (opt.maximum !== undefined) { - // const newline = opt.minimum !== undefined ? ', ' : '\n'; - // msg += `${newline}MAX: \`${opt.maximum}\``; - // } - // return msg; - // } - // ).join('\n\n') }); } diff --git a/src/client/interfaces/commands/Command.ts b/src/client/interfaces/commands/Command.ts index e5903e7..c60976a 100644 --- a/src/client/interfaces/commands/Command.ts +++ b/src/client/interfaces/commands/Command.ts @@ -35,11 +35,19 @@ type CommandThrottle = { timeout: NodeJS.Timeout } -// declare interface Command extends Component -// { -// get type(): 'command' -// } - +/** + * Base class for commands. These can be invoked as text commands (as opposed to slash commands). + * Most commands should not inherit this class directly, rather inherit the SlashCommand class, as most commands + * should work as slash commands. + * + * Note that even though most commands are slash commands, the SlashCommand class inherits this one, + * meaning they can also be invoked as text commands. + * + * @abstract + * @class Command + * @typedef {Command} + * @extends {Component} + */ abstract class Command extends Component { #logger: LoggerClient; @@ -58,6 +66,7 @@ abstract class Command extends Component #clientPermissions: PermissionsString[]; #memberPermissions: PermissionsString[]; + // For statistics purposes #invokes: { success: number, successTime: number, @@ -217,9 +226,6 @@ abstract class Command extends Component abstract execute(invoker: InvokerWrapper, options: CommandParams): Promise; - // { - // throw new Error(`${this.resolveable} is missing an execute function.`); - // } success (when: number) { diff --git a/src/client/interfaces/commands/ModerationCommand.ts b/src/client/interfaces/commands/ModerationCommand.ts index 714ba3a..46136a4 100644 --- a/src/client/interfaces/commands/ModerationCommand.ts +++ b/src/client/interfaces/commands/ModerationCommand.ts @@ -20,6 +20,17 @@ import DiscordClient from '../../DiscordClient.js'; import { ModerationCommandOptions } from '../../../../@types/Commands/Moderation.js'; import { CommandOptionParams, CommandOptionType } from '../../../../@types/Client.js'; +/** + * Base class for moderation commands. + * Mostly a convenience class as most moderation commands share a lot of the options. + * Meaning that strictly speaking a moderation command does not need to inherit this, + * but for consistentcy probably should as long as it doesn't prove inconvenient. + * + * @abstract + * @class ModerationCommand + * @typedef {ModerationCommand} + * @extends {SlashCommand} + */ abstract class ModerationCommand extends SlashCommand { constructor (client: DiscordClient, opts: ModerationCommandOptions) diff --git a/src/client/interfaces/commands/SettingsCommand.ts b/src/client/interfaces/commands/SettingsCommand.ts index 48b2039..865535a 100644 --- a/src/client/interfaces/commands/SettingsCommand.ts +++ b/src/client/interfaces/commands/SettingsCommand.ts @@ -25,6 +25,13 @@ import { CommandOptionType, CommandParams } from '../../../../@types/Client.js'; import InvokerWrapper from '../../components/wrappers/InvokerWrapper.js'; import { APIEmbed } from 'discord.js'; +/** + * Superclass for any settings commands. + * + * @class SettingsCommand + * @typedef {SettingsCommand} + * @extends {SlashCommand} + */ class SettingsCommand extends SlashCommand { constructor (client: DiscordClient, opts: SettingsCommandOptions) @@ -51,10 +58,6 @@ class SettingsCommand extends SlashCommand const settings = this.client.registry .filter((c: Setting) => c.type === 'setting' && c.module.name === this.name); - // const allSettings = this.client.registry.components.filter((c) => c._type ==='setting'); - // Organise modules into subcommand groups - // const modules = new Set(allSettings.map((set) => set.module.name)); - for (const setting of settings.values()) { try @@ -77,28 +80,6 @@ class SettingsCommand extends SlashCommand this.client.logger.error(`Setting ${setting.name} errored during options build:\n${error.stack}`); } } - - // for (const module of modules) { - // const settingsModule = allSettings.filter((s) => s.module.name === module); - // // /settings moderation - // const moduleSubcommand = new CommandOption({ - // name: module, - // description: `Configure ${module} settings`, - // type: 'SUB_COMMAND_GROUP' - // }); - // for (const setting of settingsModule.values()) { - // // Each setting becomes its own subcommand with options defined in the settings - // // /settings moderation mute role:@muted permanent:false default:1h type:1 - // const settingSubcommand = new CommandOption({ - // name: setting.name, - // description: setting.description, - // type: 'SUB_COMMAND', - // options: setting.commandOptions - // }); - // moduleSubcommand.options.push(settingSubcommand); - // } - // this.options.push(moduleSubcommand); - // } } async execute (invoker: InvokerWrapper, opts: CommandParams) @@ -125,7 +106,6 @@ class SettingsCommand extends SlashCommand }); } - // await invoker.deferReply(); const settings = await guild.settings(); if (!Object.keys(opts).length && this.subcommand(subcommand!.name)!.options.length) return this._showSetting(invoker, setting); diff --git a/src/client/interfaces/commands/SlashCommand.ts b/src/client/interfaces/commands/SlashCommand.ts index 740f767..5747a72 100644 --- a/src/client/interfaces/commands/SlashCommand.ts +++ b/src/client/interfaces/commands/SlashCommand.ts @@ -20,6 +20,16 @@ import Command from './Command.js'; import { ApplicationCommandType, PermissionsBitField } from 'discord.js'; +/** + * Base class for most of the commands on the bot. + * Only commands that SHOULD NOT inherit this are ones that shouldn't appear as slash commands, + * e.g. certain developer only commands (ex. eval) + * + * @abstract + * @class SlashCommand + * @typedef {SlashCommand} + * @extends {Command} + */ abstract class SlashCommand extends Command { #type: ApplicationCommandType;