Fixing warn command to use MemberWrapper

This commit is contained in:
D3vision 2023-12-05 22:00:49 +01:00
parent a637f1df22
commit f65604ec2b
9 changed files with 3245 additions and 3243 deletions

View File

@ -7,7 +7,8 @@
"discord": {
"prefix": "!",
"developers": [
"132777808362471424"
"132777808362471424",
"187613017733726210"
],
"developmentMode": true,
"libraryOptions": {
@ -39,7 +40,8 @@
"264527028751958016",
"207880433432657920",
"992757341848080486",
"1069272779100266598"
"1069272779100266598",
"1086433147073331340"
]
}
},

File diff suppressed because it is too large Load Diff

View File

@ -1,51 +1,51 @@
import DiscordClient from '../DiscordClient.js';
import SlashCommand from '../interfaces/commands/SlashCommand.js';
class Intercom
{
#client: DiscordClient;
constructor (client: DiscordClient)
{
this.#client = client;
if (client.singleton || client!.shard?.ids[0] === 0)
{
this.#client.eventHooker.hook('built', () =>
{
this._transportCommands();
});
}
}
send (type: string, message = {})
{
if (typeof message !== 'object')
throw new Error('Invalid message object');
if (!process.send)
return; // Nowhere to send, the client was not spawned as a shard
return process.send({
[`_${type}`]: true,
...message
});
}
_transportCommands ()
{
if (!this.#client.application)
throw new Error('Missing client application');
const clientId = this.#client.application.id;
const commands = this.#client.registry
.filter((c: SlashCommand) => c.type === 'command' && c.slash)
.map((c) => c.shape);
// console.log(inspect(commands, { depth: 25 }));
if (process.env.NODE_ENV === 'development')
return this.send('commands', { type: 'guild', commands, clientId });
this.send('commands', { type: 'global', commands, clientId });
// this.send('commands', { type: 'guild', commands, clientId });
}
}
import DiscordClient from '../DiscordClient.js';
import SlashCommand from '../interfaces/commands/SlashCommand.js';
class Intercom
{
#client: DiscordClient;
constructor (client: DiscordClient)
{
this.#client = client;
if (client.singleton || client!.shard?.ids[0] === 0)
{
this.#client.eventHooker.hook('built', () =>
{
this._transportCommands();
});
}
}
send (type: string, message = {})
{
if (typeof message !== 'object')
throw new Error('Invalid message object');
if (!process.send)
return; // Nowhere to send, the client was not spawned as a shard
return process.send({
[`_${type}`]: true,
...message
});
}
_transportCommands ()
{
if (!this.#client.application)
throw new Error('Missing client application');
const clientId = this.#client.application.id;
const commands = this.#client.registry
.filter((c: SlashCommand) => c.type === 'command' && c.slash)
.map((c) => c.shape);
// console.log(inspect(commands, { depth: 25 }));
if (process.env.NODE_ENV === 'development')
return this.send('commands', { type: 'guild', commands, clientId });
this.send('commands', { type: 'global', commands, clientId });
// this.send('commands', { type: 'guild', commands, clientId });
}
}
export default Intercom;

View File

@ -3,7 +3,7 @@ import DiscordClient from '../../../DiscordClient.js';
import { Warn } from '../../../infractions/index.js';
import { ModerationCommand } from '../../../interfaces/index.js';
import InvokerWrapper from '../../wrappers/InvokerWrapper.js';
import UserWrapper from '../../wrappers/UserWrapper.js';
import MemberWrapper from '../../wrappers/MemberWrapper.js';
class WarnCommand extends ModerationCommand
{
@ -28,11 +28,11 @@ class WarnCommand extends ModerationCommand
});
}
async execute (invoker: InvokerWrapper, { users, ...args }: CommandParams)
async execute (invoker: InvokerWrapper<true>, { users, ...args }: CommandParams)
{
const wrappers = await Promise.all(users!.asUsers.map(user => this.client.getUserWrapper(user)));
const wrappers = await Promise.all(users!.asUsers.map(user => invoker.guild.memberWrapper(user)));
return this.client.moderation.handleInfraction(Warn, invoker, {
targets: wrappers.filter(Boolean) as UserWrapper[],
targets: wrappers.filter(Boolean) as MemberWrapper[],
args
});
}

File diff suppressed because it is too large Load Diff

File diff suppressed because it is too large Load Diff

View File

@ -1,402 +1,402 @@
import { LoggerClient } from '@navy.gif/logger';
import { EmbedBuilder, Message, PermissionsString, Snowflake } from 'discord.js';
import Component from '../Component.js';
import CommandOption from '../CommandOption.js';
import { Util } from '../../../utilities/index.js';
import DiscordClient from '../../DiscordClient.js';
import { InvokerWrapper } from '../../components/wrappers/index.js';
import { CommandOptionParams, CommandOptionType, CommandOptions, CommandParams } from '../../../../@types/Client.js';
import { ReplyOptions } from '../../../../@types/Wrappers.js';
type CommandUsageLimits = {
usages: number,
duration: number
}
type CommandThrottle = {
usages: number,
start: number,
timeout: NodeJS.Timeout
}
// declare interface Command extends Component
// {
// get type(): 'command'
// }
abstract class Command extends Component
{
#logger: LoggerClient;
#name: string;
#description: string;
#tags: string[];
#aliases: string[];
#restricted: boolean;
#showUsage: boolean;
#guildOnly: boolean;
#archivable: boolean;
#slash: boolean;
#clientPermissions: PermissionsString[];
#memberPermissions: PermissionsString[];
#invokes: {
success: number,
successTime: number,
fail: number,
failTime: number
};
#options: CommandOption[];
#throttling?: CommandUsageLimits;
#throttles: Map<Snowflake, CommandThrottle>;
/**
* Creates an instance of Command.
* @param {DiscordClient} client
* @param {Object} [options={}]
* @memberof Command
*/
constructor (client: DiscordClient, options: CommandOptions)
{
if (!options)
throw Util.fatal(new Error('Missing command options'));
if (!options.name)
throw Util.fatal(new Error('Missing name'));
super(client, {
id: options.name,
type: 'command',
disabled: options.disabled,
guarded: options.guarded,
moduleName: options.moduleName
});
this.#name = options.name;
this.#logger = client.createLogger(this);
if (!options.moduleName)
this.logger.warn(`Command ${this.#name} is missing module information.`);
this.#description = options.description || '';
this.#tags = options.tags || [];
this.#aliases = options.aliases || [];
this.#restricted = Boolean(options?.restricted);
this.#showUsage = Boolean(options.showUsage);
this.#guildOnly = Boolean(options?.guildOnly);
this.#archivable = typeof options.archivable === 'undefined' ? true : Boolean(options.archivable);
this.#slash = Boolean(options.slash);
// Convers permissions to PascalCase from snake case bc for some reason d.js decided it was a good change
this.#clientPermissions = [ ...new Set<PermissionsString>([ 'SendMessages', ...options.clientPermissions || [] ]) ]; // .map(Util.pascalConverter);
this.#memberPermissions = options.memberPermissions || []; // .map(Util.pascalConverter);
this.#invokes = {
success: 0,
successTime: 0,
fail: 0,
failTime: 0
};
this.#options = [];
if (options.options)
this.#parseOptions(options.options);
this.#options.sort((a, b) =>
{
if (a.required)
return -1;
if (b.required)
return 1;
return 0;
});
this.#throttles = new Map();
}
get name ()
{
return this.#name;
}
get aliases ()
{
return this.#aliases;
}
get description ()
{
return this.#description;
}
get tags ()
{
return this.#tags;
}
get restricted ()
{
return this.#restricted;
}
get showUsage ()
{
return this.#showUsage;
}
get archivable ()
{
return this.#archivable;
}
get slash ()
{
return this.#slash;
}
get memberPermissions ()
{
return this.#memberPermissions;
}
get clientPermissions ()
{
return this.#clientPermissions;
}
get options ()
{
return this.#options;
}
get guildOnly ()
{
return this.#guildOnly;
}
protected get logger ()
{
return this.#logger;
}
get throttling ()
{
return this.#throttling;
}
get throttles ()
{
return this.#throttles;
}
get invokes ()
{
return this.#invokes;
}
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)
{
const now = Date.now();
const execTime = now - when;
// Calculate new average
if (this.#invokes.successTime)
{
this.#invokes.successTime = (this.#invokes.successTime * this.#invokes.success + execTime) / ++this.#invokes.success;
}
else
{
this.#invokes.successTime = execTime;
this.#invokes.success++;
}
}
error (when: number)
{
const now = Date.now();
const execTime = now - when;
// Calculate new average
if (this.#invokes.failTime)
{
this.#invokes.failTime = (this.#invokes.failTime * this.#invokes.fail + execTime) / ++this.#invokes.fail;
}
else
{
this.#invokes.failTime = execTime;
this.#invokes.fail++;
}
}
async usageEmbed (invoker: InvokerWrapper, verbose = false)
{
const fields = [];
const { guild, subcommand, subcommandGroup } = invoker;
let type = null;
const format = (index: string) => guild
? guild.format(index)
: this.client.format(index);
if (guild)
({ permissions: { type } } = await guild.settings());
if (this.#options.length)
{
if (verbose)
fields.push(...this.#options.map((opt) => opt.usage(guild)));
else if (subcommand)
{
const opt = this.subcommand(subcommand.name) as CommandOption;
fields.push(opt.usage(guild));
}
else if (subcommandGroup)
{
const opt = this.subcommandGroup(subcommandGroup.name) as CommandOption;
fields.push(opt.usage(guild));
}
}
if (this.memberPermissions.length)
{
let required = [];
if (type === 'discord')
required = this.memberPermissions;
else if (type === 'grant')
required = [ this.resolveable ];
else
required = [ this.resolveable, ...this.memberPermissions ];
fields.push({
name: `${format('GENERAL_PERMISSIONS')}`,
value: `\`${required.join('`, `')}\``
});
}
return new EmbedBuilder({
author: {
name: `${this.name} [module:${this.module?.name}]`
},
description: format(`COMMAND_${this.name.toUpperCase()}_HELP`),
fields
});
}
subcommandGroup (name: string)
{
if (!name)
return null;
name = name.toLowerCase();
return this.subcommandGroups.find((group) => group.name === name) ?? null;
}
get subcommandGroups ()
{
return this.#options.filter((opt) => opt.type === CommandOptionType.SUB_COMMAND_GROUP);
}
subcommand (name?: string)
{
if (!name)
return null;
name = name.toLowerCase();
return this.subcommands.find((cmd) => cmd.name === name) ?? null;
}
get subcommands ()
{
return this.#subcommands(this.#options);
}
/**
* @private
*/
#subcommands (opts: CommandOption[]): CommandOption[]
{
const subcommands = [];
for (const opt of opts)
{
if (opt.type === CommandOptionType.SUB_COMMAND)
subcommands.push(opt);
else if (opt.type === CommandOptionType.SUB_COMMAND_GROUP)
subcommands.push(...this.#subcommands(opt.options));
}
return subcommands;
}
// probably not a final name -- flattenedOptions maybe?
get actualOptions ()
{
return this.#actualOptions(this.#options);
}
/**
* @private
*/
#actualOptions (opts: CommandOption[]): CommandOption[]
{
const options: CommandOption[] = [];
for (const opt of opts)
{
if ([ CommandOptionType.SUB_COMMAND_GROUP, CommandOptionType.SUB_COMMAND ].includes(opt.type))
options.push(...this.#actualOptions(opt.options));
else
options.push(opt);
}
return options;
}
#parseOptions (options: CommandOptionParams[])
{
for (const opt of options)
{
if (opt instanceof CommandOption)
{
opt.client = this.client;
this.#options.push(opt);
continue;
}
if (!(opt.name instanceof Array))
{
this.#options.push(new CommandOption({ ...opt, client: this.client }));
continue;
}
// Allows easy templating of subcommands that share arguments
const { name: names, description, type, ...opts } = opt;
for (const name of names)
{
const index = names.indexOf(name);
let desc = description,
_type = type;
if (description instanceof Array)
desc = description[index] || 'Missing description';
if (type instanceof Array)
_type = type[index];
if (!_type)
{
_type = CommandOptionType.STRING;
this.logger.warn(`Missing option type for ${this.resolveable}.${name}, defaulting to string`);
}
// throw new Error(`Missing type for option ${name} in command ${this.name}`);
this.#options.push(new CommandOption({
...opts,
name,
type: _type,
description: desc,
client: this.client
}));
}
}
}
}
import { LoggerClient } from '@navy.gif/logger';
import { EmbedBuilder, Message, PermissionsString, Snowflake } from 'discord.js';
import Component from '../Component.js';
import CommandOption from '../CommandOption.js';
import { Util } from '../../../utilities/index.js';
import DiscordClient from '../../DiscordClient.js';
import { InvokerWrapper } from '../../components/wrappers/index.js';
import { CommandOptionParams, CommandOptionType, CommandOptions, CommandParams } from '../../../../@types/Client.js';
import { ReplyOptions } from '../../../../@types/Wrappers.js';
type CommandUsageLimits = {
usages: number,
duration: number
}
type CommandThrottle = {
usages: number,
start: number,
timeout: NodeJS.Timeout
}
// declare interface Command extends Component
// {
// get type(): 'command'
// }
abstract class Command extends Component
{
#logger: LoggerClient;
#name: string;
#description: string;
#tags: string[];
#aliases: string[];
#restricted: boolean;
#showUsage: boolean;
#guildOnly: boolean;
#archivable: boolean;
#slash: boolean;
#clientPermissions: PermissionsString[];
#memberPermissions: PermissionsString[];
#invokes: {
success: number,
successTime: number,
fail: number,
failTime: number
};
#options: CommandOption[];
#throttling?: CommandUsageLimits;
#throttles: Map<Snowflake, CommandThrottle>;
/**
* Creates an instance of Command.
* @param {DiscordClient} client
* @param {Object} [options={}]
* @memberof Command
*/
constructor (client: DiscordClient, options: CommandOptions)
{
if (!options)
throw Util.fatal(new Error('Missing command options'));
if (!options.name)
throw Util.fatal(new Error('Missing name'));
super(client, {
id: options.name,
type: 'command',
disabled: options.disabled,
guarded: options.guarded,
moduleName: options.moduleName
});
this.#name = options.name;
this.#logger = client.createLogger(this);
if (!options.moduleName)
this.logger.warn(`Command ${this.#name} is missing module information.`);
this.#description = options.description || '';
this.#tags = options.tags || [];
this.#aliases = options.aliases || [];
this.#restricted = Boolean(options?.restricted);
this.#showUsage = Boolean(options.showUsage);
this.#guildOnly = Boolean(options?.guildOnly);
this.#archivable = typeof options.archivable === 'undefined' ? true : Boolean(options.archivable);
this.#slash = Boolean(options.slash);
// Convers permissions to PascalCase from snake case bc for some reason d.js decided it was a good change
this.#clientPermissions = [ ...new Set<PermissionsString>([ 'SendMessages', ...options.clientPermissions || [] ]) ]; // .map(Util.pascalConverter);
this.#memberPermissions = options.memberPermissions || []; // .map(Util.pascalConverter);
this.#invokes = {
success: 0,
successTime: 0,
fail: 0,
failTime: 0
};
this.#options = [];
if (options.options)
this.#parseOptions(options.options);
this.#options.sort((a, b) =>
{
if (a.required)
return -1;
if (b.required)
return 1;
return 0;
});
this.#throttles = new Map();
}
get name ()
{
return this.#name;
}
get aliases ()
{
return this.#aliases;
}
get description ()
{
return this.#description;
}
get tags ()
{
return this.#tags;
}
get restricted ()
{
return this.#restricted;
}
get showUsage ()
{
return this.#showUsage;
}
get archivable ()
{
return this.#archivable;
}
get slash ()
{
return this.#slash;
}
get memberPermissions ()
{
return this.#memberPermissions;
}
get clientPermissions ()
{
return this.#clientPermissions;
}
get options ()
{
return this.#options;
}
get guildOnly ()
{
return this.#guildOnly;
}
protected get logger ()
{
return this.#logger;
}
get throttling ()
{
return this.#throttling;
}
get throttles ()
{
return this.#throttles;
}
get invokes ()
{
return this.#invokes;
}
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)
{
const now = Date.now();
const execTime = now - when;
// Calculate new average
if (this.#invokes.successTime)
{
this.#invokes.successTime = (this.#invokes.successTime * this.#invokes.success + execTime) / ++this.#invokes.success;
}
else
{
this.#invokes.successTime = execTime;
this.#invokes.success++;
}
}
error (when: number)
{
const now = Date.now();
const execTime = now - when;
// Calculate new average
if (this.#invokes.failTime)
{
this.#invokes.failTime = (this.#invokes.failTime * this.#invokes.fail + execTime) / ++this.#invokes.fail;
}
else
{
this.#invokes.failTime = execTime;
this.#invokes.fail++;
}
}
async usageEmbed (invoker: InvokerWrapper, verbose = false)
{
const fields = [];
const { guild, subcommand, subcommandGroup } = invoker;
let type = null;
const format = (index: string) => guild
? guild.format(index)
: this.client.format(index);
if (guild)
({ permissions: { type } } = await guild.settings());
if (this.#options.length)
{
if (verbose)
fields.push(...this.#options.map((opt) => opt.usage(guild)));
else if (subcommand)
{
const opt = this.subcommand(subcommand.name) as CommandOption;
fields.push(opt.usage(guild));
}
else if (subcommandGroup)
{
const opt = this.subcommandGroup(subcommandGroup.name) as CommandOption;
fields.push(opt.usage(guild));
}
}
if (this.memberPermissions.length)
{
let required = [];
if (type === 'discord')
required = this.memberPermissions;
else if (type === 'grant')
required = [ this.resolveable ];
else
required = [ this.resolveable, ...this.memberPermissions ];
fields.push({
name: `${format('GENERAL_PERMISSIONS')}`,
value: `\`${required.join('`, `')}\``
});
}
return new EmbedBuilder({
author: {
name: `${this.name} [module:${this.module?.name}]`
},
description: format(`COMMAND_${this.name.toUpperCase()}_HELP`),
fields
});
}
subcommandGroup (name: string)
{
if (!name)
return null;
name = name.toLowerCase();
return this.subcommandGroups.find((group) => group.name === name) ?? null;
}
get subcommandGroups ()
{
return this.#options.filter((opt) => opt.type === CommandOptionType.SUB_COMMAND_GROUP);
}
subcommand (name?: string)
{
if (!name)
return null;
name = name.toLowerCase();
return this.subcommands.find((cmd) => cmd.name === name) ?? null;
}
get subcommands ()
{
return this.#subcommands(this.#options);
}
/**
* @private
*/
#subcommands (opts: CommandOption[]): CommandOption[]
{
const subcommands = [];
for (const opt of opts)
{
if (opt.type === CommandOptionType.SUB_COMMAND)
subcommands.push(opt);
else if (opt.type === CommandOptionType.SUB_COMMAND_GROUP)
subcommands.push(...this.#subcommands(opt.options));
}
return subcommands;
}
// probably not a final name -- flattenedOptions maybe?
get actualOptions ()
{
return this.#actualOptions(this.#options);
}
/**
* @private
*/
#actualOptions (opts: CommandOption[]): CommandOption[]
{
const options: CommandOption[] = [];
for (const opt of opts)
{
if ([ CommandOptionType.SUB_COMMAND_GROUP, CommandOptionType.SUB_COMMAND ].includes(opt.type))
options.push(...this.#actualOptions(opt.options));
else
options.push(opt);
}
return options;
}
#parseOptions (options: CommandOptionParams[])
{
for (const opt of options)
{
if (opt instanceof CommandOption)
{
opt.client = this.client;
this.#options.push(opt);
continue;
}
if (!(opt.name instanceof Array))
{
this.#options.push(new CommandOption({ ...opt, client: this.client }));
continue;
}
// Allows easy templating of subcommands that share arguments
const { name: names, description, type, ...opts } = opt;
for (const name of names)
{
const index = names.indexOf(name);
let desc = description,
_type = type;
if (description instanceof Array)
desc = description[index] || 'Missing description';
if (type instanceof Array)
_type = type[index];
if (!_type)
{
_type = CommandOptionType.STRING;
this.logger.warn(`Missing option type for ${this.resolveable}.${name}, defaulting to string`);
}
// throw new Error(`Missing type for option ${name} in command ${this.name}`);
this.#options.push(new CommandOption({
...opts,
name,
type: _type,
description: desc,
client: this.client
}));
}
}
}
}
export default Command;

View File

@ -1,375 +1,375 @@
import { EventEmitter } from 'node:events';
import { inspect } from 'node:util';
import path from 'node:path';
import { CommandsDef, IPCMessage } from '../../@types/Shared.js';
import { BroadcastEvalOptions, ShardMethod, ShardingOptions } from '../../@types/Shard.js';
import { ControllerOptions } from '../../@types/Controller.js';
import { MasterLogger } from '@navy.gif/logger';
import { Collection } from 'discord.js';
// Available for evals
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import ClientUtils from './ClientUtils.js';
import Metrics from './Metrics.js';
// import ApiClientUtil from './ApiClientUtil.js';
import SlashCommandManager from './rest/SlashCommandManager.js';
import { Shard } from './shard/index.js';
import { existsSync } from 'node:fs';
import Util from '../utilities/Util.js';
// Placeholder
type GalacticAPI = {
init: () => Promise<void>
}
class Controller extends EventEmitter
{
// #shardingManager: ShardingManager;
#slashCommandManager: SlashCommandManager;
#logger: MasterLogger;
#metrics: Metrics;
#options: ControllerOptions;
#shardingOptions: ShardingOptions;
// #apiClientUtil: ApiClientUtil;
#shards: Collection<number, Shard>;
#version: string;
#readyAt: number | null;
#built: boolean;
#api?: GalacticAPI;
constructor (options: ControllerOptions, version: string)
{
super();
// Sharding
const respawn = process.env.NODE_ENV !== 'development';
const clientPath = path.join(options.rootDir, 'client/DiscordClient.js');
if (!existsSync(clientPath))
throw new Error(`Client path does not seem to exist: ${clientPath}`);
this.#options = options;
const { shardList, totalShards } = Controller.parseShardOptions(options.shardOptions);
options.discord.rootDir = options.rootDir;
options.discord.logger = options.logger;
options.discord.storage = options.storage;
options.discord.version = version;
this.#shardingOptions = {
path: clientPath,
totalShards,
shardList,
respawn,
shardArgs: [],
execArgv: [],
token: process.env.DISCORD_TOKEN,
clientOptions: options.discord,
};
// Other
this.#slashCommandManager = new SlashCommandManager(this);
this.#logger = new MasterLogger(options.logger);
this.#metrics = new Metrics(this);
// this.#apiClientUtil = new ApiClientUtil(this);
this.#version = version;
this.#readyAt = null;
this.#built = false;
this.#shards = new Collection();
// this.#shardingManager.on('message', this._handleMessage.bind(this));
}
get version ()
{
return this.#version;
}
get ready ()
{
return this.#built;
}
get readyAt ()
{
return this.#readyAt || -1;
}
get totalShards ()
{
return this.#shardingOptions.totalShards as number;
}
get developerGuilds ()
{
return this.#options.discord.slashCommands?.developerGuilds;
}
get logger ()
{
return this.#logger;
}
get api ()
{
return this.#api;
}
get shards ()
{
return this.#shards.clone();
}
async build ()
{
const start = Date.now();
// const API = this._options.api.load ? await import('/Documents/My programs/GBot/api/index.js')
// .catch(() => this.logger.warn(`Error importing API files, continuing without`)) : null;
// let API = null;
// if (this.#options.api.load)
// API = await import('../../api/index.js').catch(() => this.#logger.warn(`Error importing API files, continuing without`));
// if (API) {
// // TODO: this needs to be fixed up
// this.#logger.info('Booting up API');
// const { default: APIManager } = API;
// this.#api = new APIManager(this, this.#options.api) as GalacticAPI;
// await this.#api.init();
// const now = Date.now();
// this.#logger.info(`API ready. Took ${now - start} ms`);
// start = now;
// }
this.#logger.status('Starting bot shards');
// await this.shardingManager.spawn().catch((error) => {
// this.#logger.error(`Fatal error during shard spawning:\n${error.stack || inspect(error)}`);
// // eslint-disable-next-line no-process-exit
// process.exit(); // Prevent a boot loop when shards die due to an error in the client
// });
const { totalShards, token } = this.#shardingOptions;
let shardCount = 0;
if (totalShards === 'auto')
{
if (!token)
throw new Error('Missing token');
shardCount = await Util.fetchRecommendedShards(token);
}
else
{
if (typeof shardCount !== 'number' || isNaN(shardCount))
throw new TypeError('Amount of shards must be a number.');
if (shardCount < 1)
throw new RangeError('Amount of shards must be at least one.');
if (!Number.isInteger(shardCount))
throw new TypeError('Amount of shards must be an integer.');
}
const promises = [];
for (let i = 0; i < shardCount; i++)
{
const shard = this.createShard(shardCount);
promises.push(shard.spawn());
}
await Promise.all(promises);
this.#logger.status(`Shards spawned, spawned ${this.#shards.size} shards. Took ${Date.now() - start} ms`);
this.#built = true;
this.#readyAt = Date.now();
}
createShard (totalShards: number)
{
const ids = this.#shards.map(s => s.id);
const id = ids.length ? Math.max(...ids) + 1 : 0;
const { path: file, token, respawn, execArgv, shardArgs: args, clientOptions: discordOptions } = this.#shardingOptions;
if (!file)
throw new Error('File seems to be missing');
if (!discordOptions)
throw new Error('Missing discord options');
const shard = new Shard(this, id, {
file,
token,
respawn,
args,
execArgv,
totalShards,
clientOptions: discordOptions
});
this.#shards.set(shard.id, shard);
this.#logger.attach(shard);
this.#setListeners(shard);
return shard;
}
#setListeners (shard: Shard)
{
shard.on('death', () => this.#logger.info(`Shard ${shard.id} has died`));
shard.on('fatal', ({ error }) => this.#logger.warn(`Shard ${shard.id} has died fatally: ${inspect(error) ?? ''}`));
shard.on('shutdown', () => this.#logger.info(`Shard ${shard.id} is shutting down gracefully`));
shard.on('ready', () => this.#logger.info(`Shard ${shard.id} is ready`));
shard.on('disconnect', () => this.#logger.warn(`Shard ${shard.id} has disconnected`));
shard.on('processDisconnect', () => this.#logger.warn(`Process for ${shard.id} has disconnected`));
shard.on('spawn', () => this.#logger.info(`Shard ${shard.id} spawned`));
shard.on('error', (err) => this.#logger.error(`Shard ${shard.id} ran into an error:\n${err.stack}`));
shard.on('warn', (msg) => this.#logger.warn(`Warning from shard ${shard.id}: ${msg}`, { broadcast: true }));
shard.on('message', (msg) => this.#handleMessage(shard, msg));
}
#handleMessage (shard: Shard, message: IPCMessage)
{
if (message._logger)
return;
// this.logger.debug(`New message from ${shard ? `${message._api ? 'api-' : ''}shard ${shard.id}`: 'manager'}: ${inspect(message)}`);
if (message._mEval)
return this.eval(shard, { script: message._mEval, debug: message.debug || false });
if (message._commands)
return this.#slashCommandManager._handleMessage(message as CommandsDef);
if (message._api)
return this.apiRequest(shard, message);
}
apiRequest (shard: Shard, message: IPCMessage)
{
const { type } = message;
switch (type)
{
case 'stats':
this.#metrics.aggregateStatistics(shard, message);
break;
default:
// this.#apiClientUtil.handleMessage(shard, message);
}
}
/**
* @param {*} shard The shard from which the eval came and to which it will be returned
* @param {*} script The script to be executed
* @memberof Manager
* @private
*/
async eval (shard: Shard, { script, debug }: {script: string, debug: boolean})
{
this.#logger.info(`Incoming manager eval from shard ${shard.id}:\n${script}`);
let result = null,
error = null;
const response: IPCMessage = {
script, _mEvalResult: true
};
try
{
// eslint-disable-next-line no-eval
result = await eval(script);
response._result = result;
// if(typeof result !== 'string') result = inspect(result);
if (debug)
this.#logger.debug(`Eval result: ${inspect(result)}`);
}
catch (e)
{
const err = e as Error;
error = Util.makePlainError(err);
response._error = error;
}
return shard.send(response);
}
// eslint-disable-next-line @typescript-eslint/ban-types
broadcastEval (script: string | Function, options: BroadcastEvalOptions = {})
{
if (typeof script !== 'function')
return Promise.reject(new TypeError('[shardmanager] Provided eval must be a function.'));
return this._performOnShards('eval', [ `(${script})(this, ${JSON.stringify(options.context)})` ], options.shard);
}
fetchClientValues (prop: string, shard?: number)
{
return this._performOnShards('fetchClientValue', [ prop ], shard);
}
_performOnShards (method: ShardMethod, args: [string, object?], shard?: number): Promise<unknown>
{
if (this.#shards.size === 0)
return Promise.reject(new Error('No shards available.'));
if (!this.ready)
return Promise.reject(new Error('Controller not ready'));
if (typeof shard === 'number')
{
if (!this.#shards.has(shard))
Promise.reject(new Error('Shard not found.'));
const s = this.#shards.get(shard) as Shard;
if (method === 'eval')
return s.eval(...args);
else if (method === 'fetchClientValue')
return s.eval(args[0]);
}
const promises = [];
for (const sh of this.#shards.values())
{
if (method === 'eval')
promises.push(sh.eval(...args));
else if (method === 'fetchClientValue')
promises.push(sh.eval(args[0]));
}
return Promise.all(promises);
}
async respawnAll ({ shardDelay = 5000, respawnDelay = 500, timeout = 30000 } = {})
{
let s = 0;
for (const shard of this.#shards.values())
{
const promises: Promise<unknown>[] = [ shard.respawn({ delay: respawnDelay, timeout }) ];
if (++s < this.#shards.size && shardDelay > 0)
promises.push(Util.delayFor(shardDelay));
await Promise.all(promises); // eslint-disable-line no-await-in-loop
}
return this.#shards;
}
static parseShardOptions (options: ShardingOptions)
{
let shardList = options.shardList ?? 'auto';
if (shardList !== 'auto')
{
if (!Array.isArray(shardList))
throw new Error('ShardList must be an array.');
shardList = [ ...new Set(shardList) ];
if (shardList.length < 1)
throw new Error('ShardList must have at least one ID.');
if (shardList.some((shardId) => typeof shardId !== 'number' || isNaN(shardId) || !Number.isInteger(shardId) || shardId < 0))
throw new Error('ShardList must be an array of positive integers.');
}
const totalShards = options.totalShards || 'auto';
if (totalShards !== 'auto')
{
if (typeof totalShards !== 'number' || isNaN(totalShards))
throw new Error('TotalShards must be an integer.');
if (totalShards < 1)
throw new Error('TotalShards must be at least one.');
if (!Number.isInteger(totalShards))
throw new Error('TotalShards must be an integer.');
}
return { shardList, totalShards };
}
}
import { EventEmitter } from 'node:events';
import { inspect } from 'node:util';
import path from 'node:path';
import { CommandsDef, IPCMessage } from '../../@types/Shared.js';
import { BroadcastEvalOptions, ShardMethod, ShardingOptions } from '../../@types/Shard.js';
import { ControllerOptions } from '../../@types/Controller.js';
import { MasterLogger } from '@navy.gif/logger';
import { Collection } from 'discord.js';
// Available for evals
// eslint-disable-next-line @typescript-eslint/ban-ts-comment
// @ts-ignore
import ClientUtils from './ClientUtils.js';
import Metrics from './Metrics.js';
// import ApiClientUtil from './ApiClientUtil.js';
import SlashCommandManager from './rest/SlashCommandManager.js';
import { Shard } from './shard/index.js';
import { existsSync } from 'node:fs';
import Util from '../utilities/Util.js';
// Placeholder
type GalacticAPI = {
init: () => Promise<void>
}
class Controller extends EventEmitter
{
// #shardingManager: ShardingManager;
#slashCommandManager: SlashCommandManager;
#logger: MasterLogger;
#metrics: Metrics;
#options: ControllerOptions;
#shardingOptions: ShardingOptions;
// #apiClientUtil: ApiClientUtil;
#shards: Collection<number, Shard>;
#version: string;
#readyAt: number | null;
#built: boolean;
#api?: GalacticAPI;
constructor (options: ControllerOptions, version: string)
{
super();
// Sharding
const respawn = process.env.NODE_ENV !== 'development';
const clientPath = path.join(options.rootDir, 'client/DiscordClient.js');
if (!existsSync(clientPath))
throw new Error(`Client path does not seem to exist: ${clientPath}`);
this.#options = options;
const { shardList, totalShards } = Controller.parseShardOptions(options.shardOptions);
options.discord.rootDir = options.rootDir;
options.discord.logger = options.logger;
options.discord.storage = options.storage;
options.discord.version = version;
this.#shardingOptions = {
path: clientPath,
totalShards,
shardList,
respawn,
shardArgs: [],
execArgv: [],
token: process.env.DISCORD_TOKEN,
clientOptions: options.discord,
};
// Other
this.#slashCommandManager = new SlashCommandManager(this);
this.#logger = new MasterLogger(options.logger);
this.#metrics = new Metrics(this);
// this.#apiClientUtil = new ApiClientUtil(this);
this.#version = version;
this.#readyAt = null;
this.#built = false;
this.#shards = new Collection();
// this.#shardingManager.on('message', this._handleMessage.bind(this));
}
get version ()
{
return this.#version;
}
get ready ()
{
return this.#built;
}
get readyAt ()
{
return this.#readyAt || -1;
}
get totalShards ()
{
return this.#shardingOptions.totalShards as number;
}
get developerGuilds ()
{
return this.#options.discord.slashCommands?.developerGuilds;
}
get logger ()
{
return this.#logger;
}
get api ()
{
return this.#api;
}
get shards ()
{
return this.#shards.clone();
}
async build ()
{
const start = Date.now();
// const API = this._options.api.load ? await import('/Documents/My programs/GBot/api/index.js')
// .catch(() => this.logger.warn(`Error importing API files, continuing without`)) : null;
// let API = null;
// if (this.#options.api.load)
// API = await import('../../api/index.js').catch(() => this.#logger.warn(`Error importing API files, continuing without`));
// if (API) {
// // TODO: this needs to be fixed up
// this.#logger.info('Booting up API');
// const { default: APIManager } = API;
// this.#api = new APIManager(this, this.#options.api) as GalacticAPI;
// await this.#api.init();
// const now = Date.now();
// this.#logger.info(`API ready. Took ${now - start} ms`);
// start = now;
// }
this.#logger.status('Starting bot shards');
// await this.shardingManager.spawn().catch((error) => {
// this.#logger.error(`Fatal error during shard spawning:\n${error.stack || inspect(error)}`);
// // eslint-disable-next-line no-process-exit
// process.exit(); // Prevent a boot loop when shards die due to an error in the client
// });
const { totalShards, token } = this.#shardingOptions;
let shardCount = 0;
if (totalShards === 'auto')
{
if (!token)
throw new Error('Missing token');
shardCount = await Util.fetchRecommendedShards(token);
}
else
{
if (typeof shardCount !== 'number' || isNaN(shardCount))
throw new TypeError('Amount of shards must be a number.');
if (shardCount < 1)
throw new RangeError('Amount of shards must be at least one.');
if (!Number.isInteger(shardCount))
throw new TypeError('Amount of shards must be an integer.');
}
const promises = [];
for (let i = 0; i < shardCount; i++)
{
const shard = this.createShard(shardCount);
promises.push(shard.spawn());
}
await Promise.all(promises);
this.#logger.status(`Shards spawned, spawned ${this.#shards.size} shards. Took ${Date.now() - start} ms`);
this.#built = true;
this.#readyAt = Date.now();
}
createShard (totalShards: number)
{
const ids = this.#shards.map(s => s.id);
const id = ids.length ? Math.max(...ids) + 1 : 0;
const { path: file, token, respawn, execArgv, shardArgs: args, clientOptions: discordOptions } = this.#shardingOptions;
if (!file)
throw new Error('File seems to be missing');
if (!discordOptions)
throw new Error('Missing discord options');
const shard = new Shard(this, id, {
file,
token,
respawn,
args,
execArgv,
totalShards,
clientOptions: discordOptions
});
this.#shards.set(shard.id, shard);
this.#logger.attach(shard);
this.#setListeners(shard);
return shard;
}
#setListeners (shard: Shard)
{
shard.on('death', () => this.#logger.info(`Shard ${shard.id} has died`));
shard.on('fatal', ({ error }) => this.#logger.warn(`Shard ${shard.id} has died fatally: ${inspect(error) ?? ''}`));
shard.on('shutdown', () => this.#logger.info(`Shard ${shard.id} is shutting down gracefully`));
shard.on('ready', () => this.#logger.info(`Shard ${shard.id} is ready`));
shard.on('disconnect', () => this.#logger.warn(`Shard ${shard.id} has disconnected`));
shard.on('processDisconnect', () => this.#logger.warn(`Process for ${shard.id} has disconnected`));
shard.on('spawn', () => this.#logger.info(`Shard ${shard.id} spawned`));
shard.on('error', (err) => this.#logger.error(`Shard ${shard.id} ran into an error:\n${err.stack}`));
shard.on('warn', (msg) => this.#logger.warn(`Warning from shard ${shard.id}: ${msg}`, { broadcast: true }));
shard.on('message', (msg) => this.#handleMessage(shard, msg));
}
#handleMessage (shard: Shard, message: IPCMessage)
{
if (message._logger)
return;
// this.logger.debug(`New message from ${shard ? `${message._api ? 'api-' : ''}shard ${shard.id}`: 'manager'}: ${inspect(message)}`);
if (message._mEval)
return this.eval(shard, { script: message._mEval, debug: message.debug || false });
if (message._commands)
return this.#slashCommandManager._handleMessage(message as CommandsDef);
if (message._api)
return this.apiRequest(shard, message);
}
apiRequest (shard: Shard, message: IPCMessage)
{
const { type } = message;
switch (type)
{
case 'stats':
this.#metrics.aggregateStatistics(shard, message);
break;
default:
// this.#apiClientUtil.handleMessage(shard, message);
}
}
/**
* @param {*} shard The shard from which the eval came and to which it will be returned
* @param {*} script The script to be executed
* @memberof Manager
* @private
*/
async eval (shard: Shard, { script, debug }: {script: string, debug: boolean})
{
this.#logger.info(`Incoming manager eval from shard ${shard.id}:\n${script}`);
let result = null,
error = null;
const response: IPCMessage = {
script, _mEvalResult: true
};
try
{
// eslint-disable-next-line no-eval
result = await eval(script);
response._result = result;
// if(typeof result !== 'string') result = inspect(result);
if (debug)
this.#logger.debug(`Eval result: ${inspect(result)}`);
}
catch (e)
{
const err = e as Error;
error = Util.makePlainError(err);
response._error = error;
}
return shard.send(response);
}
// eslint-disable-next-line @typescript-eslint/ban-types
broadcastEval (script: string | Function, options: BroadcastEvalOptions = {})
{
if (typeof script !== 'function')
return Promise.reject(new TypeError('[shardmanager] Provided eval must be a function.'));
return this._performOnShards('eval', [ `(${script})(this, ${JSON.stringify(options.context)})` ], options.shard);
}
fetchClientValues (prop: string, shard?: number)
{
return this._performOnShards('fetchClientValue', [ prop ], shard);
}
_performOnShards (method: ShardMethod, args: [string, object?], shard?: number): Promise<unknown>
{
if (this.#shards.size === 0)
return Promise.reject(new Error('No shards available.'));
if (!this.ready)
return Promise.reject(new Error('Controller not ready'));
if (typeof shard === 'number')
{
if (!this.#shards.has(shard))
Promise.reject(new Error('Shard not found.'));
const s = this.#shards.get(shard) as Shard;
if (method === 'eval')
return s.eval(...args);
else if (method === 'fetchClientValue')
return s.eval(args[0]);
}
const promises = [];
for (const sh of this.#shards.values())
{
if (method === 'eval')
promises.push(sh.eval(...args));
else if (method === 'fetchClientValue')
promises.push(sh.eval(args[0]));
}
return Promise.all(promises);
}
async respawnAll ({ shardDelay = 5000, respawnDelay = 500, timeout = 30000 } = {})
{
let s = 0;
for (const shard of this.#shards.values())
{
const promises: Promise<unknown>[] = [ shard.respawn({ delay: respawnDelay, timeout }) ];
if (++s < this.#shards.size && shardDelay > 0)
promises.push(Util.delayFor(shardDelay));
await Promise.all(promises); // eslint-disable-line no-await-in-loop
}
return this.#shards;
}
static parseShardOptions (options: ShardingOptions)
{
let shardList = options.shardList ?? 'auto';
if (shardList !== 'auto')
{
if (!Array.isArray(shardList))
throw new Error('ShardList must be an array.');
shardList = [ ...new Set(shardList) ];
if (shardList.length < 1)
throw new Error('ShardList must have at least one ID.');
if (shardList.some((shardId) => typeof shardId !== 'number' || isNaN(shardId) || !Number.isInteger(shardId) || shardId < 0))
throw new Error('ShardList must be an array of positive integers.');
}
const totalShards = options.totalShards || 'auto';
if (totalShards !== 'auto')
{
if (typeof totalShards !== 'number' || isNaN(totalShards))
throw new Error('TotalShards must be an integer.');
if (totalShards < 1)
throw new Error('TotalShards must be at least one.');
if (!Number.isInteger(totalShards))
throw new Error('TotalShards must be an integer.');
}
return { shardList, totalShards };
}
}
export default Controller;

View File

@ -1,185 +1,185 @@
import { REST } from '@discordjs/rest';
import { Routes } from 'discord-api-types/v9';
import hash from 'object-hash';
import fs from 'node:fs';
import path from 'node:path';
import { inspect } from 'node:util';
import BaseClient from '../Controller.js';
import { Command, CommandOption, CommandsDef } from '../../../@types/Shared.js';
type CommandHashTable = {
global: string | null,
guilds: {
[key: string]: string
}
}
type ClusterFuck = {
rawError: {
errors: {
[key: string]: {
options: {
[key: string]: CommandOption
}
}
}
}
} & Error
class SlashCommandManager
{
#client: BaseClient;
#rest: REST;
#hashPath: string;
#hash: CommandHashTable;
#developerGuilds: string[];
constructor (client: BaseClient)
{
this.#client = client;
this.#rest = new REST({ version: '9' })
.setToken(process.env.DISCORD_TOKEN as string);
this.#developerGuilds = client.developerGuilds ?? [];
this.#hashPath = path.join(process.cwd(), '/commandHash.json');
this.#hash = fs.existsSync(this.#hashPath)
? JSON.parse(fs.readFileSync(this.#hashPath, { encoding: 'utf-8' }))
: { global: null, guilds: {} };
}
async _handleMessage ({ commands, guilds, clientId, type }: CommandsDef)
{
fs.writeFileSync('./commands.json', JSON.stringify(commands, null, 4), { encoding: 'utf-8' });
if (type === 'global')
await this.#global(commands, clientId);
else if (type === 'guild')
await this.#guild(commands, { guilds, clientId });
}
async #guild (commands: Command[], { guilds = [], clientId }: { guilds: string[], clientId: string })
{
if (!guilds.length)
guilds = this.#developerGuilds;
const cmdHash = hash(commands);
for (const guild of [ ...guilds ])
{
// Skip guild if unavailable
const res = await this.#rest.get(Routes.guild(guild)).catch(() =>
{
return null;
});
if (!res)
{
guilds.splice(guilds.indexOf(guild), 1);
continue;
}
// Skip guild update if hash is already up to date
if (this.#hash.guilds[guild] === cmdHash)
guilds.splice(guilds.indexOf(guild), 1);
// else update hash
else
this.#hash.guilds[guild] = cmdHash;
}
this.#client.logger.write('info', `Commands hash: ${cmdHash}, ${guilds.length} out of date`);
if (!guilds.length)
return;
const promises = [];
// fs.writeFileSync(path.join(process.cwd(), 'commands.json'), JSON.stringify(commands));
for (const guild of guilds)
{
promises.push(this.#rest.put(
Routes.applicationGuildCommands(clientId, guild),
{ body: commands }
));
}
let result = null;
try
{
result = await Promise.all(promises);
}
catch (err)
{
this.#parseError(err as ClusterFuck, commands);
}
if (!result)
return null;
this.#saveHash();
this.#client.logger.debug(`Refreshed guild slash commands for guild${guilds.length === 1 ? '' : 's'}: ${guilds.join(' ')}`);
return result;
}
#parseError (error: ClusterFuck, commands: Command[])
{
// console.log(inspect(error, { depth: 25 }));
this.#client.logger.error(`An issue has occured while updating guild commands. Guild command refresh aborted.\n${error.stack || error}`);
// Figures out which command and option ran into issues
const invalid = error.rawError.errors;
const keys = Object.keys(invalid);
let str = '';
for (const key of keys)
{
const i = parseInt(key);
const command = commands[i];
if (!command)
{
this.#client.logger.warn(`Unable to select command for index ${i} (${key})`);
continue;
}
str += `${command.name}: `;
const options = Object.keys(invalid[key].options);
for (const optKey of options)
{
if (!command.options[optKey])
{
this.#client.logger.warn(`Missing properties for ${command.name}: ${optKey}\nOptions: ${inspect(command.options)}`);
continue;
}
str += `${command.options[optKey].name}\t`;
}
str += '\n\n';
}
this.#client.logger.error(`Failed commands:\n${str}`);
}
async #global (commands: Command[], clientId: string)
{
const cmdHash = hash(commands);
const upToDate = this.#hash.global === cmdHash;
this.#client.logger.info(`Commands hash: ${cmdHash}, ${upToDate ? 'not ' : ''}updating`);
if (upToDate)
return;
this.#hash.global = cmdHash;
try
{
this.#client.logger.debug('Starting global refresh for slash commands.');
await this.#rest.put(
Routes.applicationCommands(clientId),
{ body: commands }
);
this.#client.logger.debug('Finished global refresh for slash commands.');
}
catch (err)
{
const error = err as Error;
return this.#client.logger.error(`Failed to refresh slash commands globally.\n${error.stack || error}`);
}
this.#saveHash();
}
#saveHash ()
{
fs.writeFileSync(this.#hashPath, JSON.stringify(this.#hash), { encoding: 'utf-8' });
}
}
import { REST } from '@discordjs/rest';
import { Routes } from 'discord-api-types/v9';
import hash from 'object-hash';
import fs from 'node:fs';
import path from 'node:path';
import { inspect } from 'node:util';
import BaseClient from '../Controller.js';
import { Command, CommandOption, CommandsDef } from '../../../@types/Shared.js';
type CommandHashTable = {
global: string | null,
guilds: {
[key: string]: string
}
}
type ClusterFuck = {
rawError: {
errors: {
[key: string]: {
options: {
[key: string]: CommandOption
}
}
}
}
} & Error
class SlashCommandManager
{
#client: BaseClient;
#rest: REST;
#hashPath: string;
#hash: CommandHashTable;
#developerGuilds: string[];
constructor (client: BaseClient)
{
this.#client = client;
this.#rest = new REST({ version: '9' })
.setToken(process.env.DISCORD_TOKEN as string);
this.#developerGuilds = client.developerGuilds ?? [];
this.#hashPath = path.join(process.cwd(), '/commandHash.json');
this.#hash = fs.existsSync(this.#hashPath)
? JSON.parse(fs.readFileSync(this.#hashPath, { encoding: 'utf-8' }))
: { global: null, guilds: {} };
}
async _handleMessage ({ commands, guilds, clientId, type }: CommandsDef)
{
fs.writeFileSync('./commands.json', JSON.stringify(commands, null, 4), { encoding: 'utf-8' });
if (type === 'global')
await this.#global(commands, clientId);
else if (type === 'guild')
await this.#guild(commands, { guilds, clientId });
}
async #guild (commands: Command[], { guilds = [], clientId }: { guilds: string[], clientId: string })
{
if (!guilds.length)
guilds = this.#developerGuilds;
const cmdHash = hash(commands);
for (const guild of [ ...guilds ])
{
// Skip guild if unavailable
const res = await this.#rest.get(Routes.guild(guild)).catch(() =>
{
return null;
});
if (!res)
{
guilds.splice(guilds.indexOf(guild), 1);
continue;
}
// Skip guild update if hash is already up to date
if (this.#hash.guilds[guild] === cmdHash)
guilds.splice(guilds.indexOf(guild), 1);
// else update hash
else
this.#hash.guilds[guild] = cmdHash;
}
this.#client.logger.write('info', `Commands hash: ${cmdHash}, ${guilds.length} out of date`);
if (!guilds.length)
return;
const promises = [];
// fs.writeFileSync(path.join(process.cwd(), 'commands.json'), JSON.stringify(commands));
for (const guild of guilds)
{
promises.push(this.#rest.put(
Routes.applicationGuildCommands(clientId, guild),
{ body: commands }
));
}
let result = null;
try
{
result = await Promise.all(promises);
}
catch (err)
{
this.#parseError(err as ClusterFuck, commands);
}
if (!result)
return null;
this.#saveHash();
this.#client.logger.debug(`Refreshed guild slash commands for guild${guilds.length === 1 ? '' : 's'}: ${guilds.join(' ')}`);
return result;
}
#parseError (error: ClusterFuck, commands: Command[])
{
// console.log(inspect(error, { depth: 25 }));
this.#client.logger.error(`An issue has occured while updating guild commands. Guild command refresh aborted.\n${error.stack || error}`);
// Figures out which command and option ran into issues
const invalid = error.rawError.errors;
const keys = Object.keys(invalid);
let str = '';
for (const key of keys)
{
const i = parseInt(key);
const command = commands[i];
if (!command)
{
this.#client.logger.warn(`Unable to select command for index ${i} (${key})`);
continue;
}
str += `${command.name}: `;
const options = Object.keys(invalid[key].options);
for (const optKey of options)
{
if (!command.options[optKey])
{
this.#client.logger.warn(`Missing properties for ${command.name}: ${optKey}\nOptions: ${inspect(command.options)}`);
continue;
}
str += `${command.options[optKey].name}\t`;
}
str += '\n\n';
}
this.#client.logger.error(`Failed commands:\n${str}`);
}
async #global (commands: Command[], clientId: string)
{
const cmdHash = hash(commands);
const upToDate = this.#hash.global === cmdHash;
this.#client.logger.info(`Commands hash: ${cmdHash}, ${upToDate ? 'not ' : ''}updating`);
if (upToDate)
return;
this.#hash.global = cmdHash;
try
{
this.#client.logger.debug('Starting global refresh for slash commands.');
await this.#rest.put(
Routes.applicationCommands(clientId),
{ body: commands }
);
this.#client.logger.debug('Finished global refresh for slash commands.');
}
catch (err)
{
const error = err as Error;
return this.#client.logger.error(`Failed to refresh slash commands globally.\n${error.stack || error}`);
}
this.#saveHash();
}
#saveHash ()
{
fs.writeFileSync(this.#hashPath, JSON.stringify(this.#hash), { encoding: 'utf-8' });
}
}
export default SlashCommandManager;