forked from Galactic/galactic-bot
Fixing warn command to use MemberWrapper
This commit is contained in:
parent
a637f1df22
commit
f65604ec2b
@ -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
@ -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;
|
@ -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
@ -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;
|
@ -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;
|
@ -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;
|
Loading…
Reference in New Issue
Block a user