Update main branch #15

Merged
Navy.gif merged 21 commits from beta into main 2024-07-26 16:40:55 +02:00
15 changed files with 137 additions and 136 deletions
Showing only changes of commit ccb5ce3000 - Show all commits

View File

@ -116,6 +116,15 @@ declare interface DiscordClient extends Client {
on<K extends keyof ClientEvents>(event: K, listener: EventHook<K>): 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<Result> (script: ((controller: Controller) => Promise<Result> | Result) | string, options: ManagerEvalOptions = {})
: Promise<Result>
{
@ -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<K extends keyof ClientEvents>(event: K, listener: (...args: unknown[]) => Awaitable<void>): this;
// on<K extends keyof ClientEvents> (event: K, listener: (...args: unknown[]) => Awaitable<void>)
// {
// // 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<UserWrapper | null>;
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
// });
export default DiscordClient;

View File

@ -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')

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -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;

View File

@ -38,17 +38,19 @@ const filterInexact = <T extends Component>(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;

View File

@ -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

View File

@ -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;

View File

@ -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;

View File

@ -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<IsGuildSetting extends boolean = true> 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')
});
}

View File

@ -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<string | Message | null | ReplyOptions | void | EmbedBuilder>;
// {
// throw new Error(`${this.resolveable} is missing an execute function.`);
// }
success (when: number)
{

View File

@ -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)

View File

@ -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<true>, 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);

View File

@ -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;