imported localeloader, added some option parsing to command handler. still nowhere near completion.

This commit is contained in:
nolan 2021-08-22 01:34:48 -07:00
parent 1224ac96dd
commit 569a00999a
18 changed files with 379 additions and 100 deletions

View File

@ -4,7 +4,11 @@
"developer": "132620781791346688", "developer": "132620781791346688",
"clientId": "697791541690892369", "clientId": "697791541690892369",
"clientOptions": { "clientOptions": {
"intents": [
"GUILD",
"GUILD_MEMBERS",
"GUILD_MESSAGES"
]
}, },
"shardOptions": { "shardOptions": {
"totalShards": "auto" "totalShards": "auto"

View File

@ -0,0 +1,2 @@
//Mute Command
[C_MUTE_DESCRIPTION]

View File

@ -1,8 +0,0 @@
{
"developer:test": {
"description": "A test command used for testing purposes.",
"responses": {
}
}
}

View File

@ -0,0 +1,3 @@
[O_COMMANDHANDLER_COMMANDNOTSYNCED]
It appears as if the command does not exist on the client.
This is an issue that should be reported to a bot developer.

View File

@ -16,17 +16,17 @@ class SlashCommandManager {
if(message.type === 'global') { if(message.type === 'global') {
await this.global(message.commands); await this.global(message.commands);
} else if(message.type === 'guild') { } else if(message.type === 'guild') {
await this.guild(message.commands, { guilds: message.guilds }); await this.guild(message.commands, { guilds: message.guilds, clientId: message.clientId });
} }
} }
async guild(commands, { guilds = [] }) { async guild(commands, { guilds = [], clientId }) {
if(guilds.length === 0) guilds = this.client._options.discord.slashCommands.developerGuilds; if(guilds.length === 0) guilds = this.client._options.discord.slashCommands.developerGuilds;
const promises = []; const promises = [];
for(const guild of guilds) { for(const guild of guilds) {
promises.push(this.rest.put( promises.push(this.rest.put(
Routes.applicationGuildCommands(this.client._options.discord.clientId, guild), Routes.applicationGuildCommands(clientId, guild),
{ body: commands } { body: commands }
)); ));
} }

View File

@ -1,6 +1,6 @@
const { Client, Intents } = require('discord.js'); const { Client, Intents } = require('discord.js');
const { Logger, Intercom, EventHooker, Registry, Dispatcher, Resolver } = require('./client/'); const { Logger, Intercom, EventHooker, LocaleLoader, Registry, Dispatcher, Resolver } = require('./client/');
const { Observer, Command } = require('./interfaces/'); const { Observer, Command } = require('./interfaces/');
const options = require('../../options.json'); const options = require('../../options.json');
@ -11,13 +11,13 @@ class DiscordClient extends Client {
if(!options) return null; if(!options) return null;
super({ super({
...options.discord.clientOptions, ...options.discord.clientOptions
intents: [Intents.FLAGS.GUILDS, Intents.FLAGS.GUILD_MESSAGES]
}); });
this.eventHooker = new EventHooker(this); this.eventHooker = new EventHooker(this);
this.intercom = new Intercom(this); this.intercom = new Intercom(this);
this.logger = new Logger(this); this.logger = new Logger(this);
this.localeLoader = new LocaleLoader(this);
this.registry = new Registry(this); this.registry = new Registry(this);
this.dispatcher = new Dispatcher(this); this.dispatcher = new Dispatcher(this);
@ -36,6 +36,9 @@ class DiscordClient extends Client {
const beforeTime = Date.now(); const beforeTime = Date.now();
//Initialize components, localization, and observers. //Initialize components, localization, and observers.
await this.localeLoader.loadLanguages();
await this.registry.loadComponents('components/observers', Observer); await this.registry.loadComponents('components/observers', Observer);
await this.registry.loadComponents('components/commands', Command); await this.registry.loadComponents('components/commands', Command);

View File

@ -24,9 +24,9 @@ class Intercom {
const commands = this.client.registry.components const commands = this.client.registry.components
.filter((c) => c._type === 'command' && c.slash) .filter((c) => c._type === 'command' && c.slash)
.map((c) => c.json); .map((c) => c.shape);
this.send('commands', { type: 'guild', commands }); this.send('commands', { type: 'guild', commands, clientId: this.client.application.id });
} }

View File

@ -0,0 +1,82 @@
const path = require('path');
const fs = require('fs');
const chalk = require('chalk');
const Util = require('../../Util.js');
class LocaleLoader {
constructor(client) {
this.client = client;
this.languages = {};
}
async loadLanguages() {
const root = path.join(process.cwd(), "src/localization");
const directories = fs.readdirSync(root); //locale directories (en_us, fi_fi)
for(const directory of directories) this._loadLanguage(root, directory);
}
_loadLanguage(root, language) {
const directory = path.join(root, language);
const files = Util.readdirRecursive(directory);
const combined = {};
for(let file of files) {
file = fs.readFileSync(file, {
encoding: 'utf8'
});
const result = this._loadFile(file);
Object.assign(combined, result);
}
this.languages[language] = combined;
this.client.logger.info(`Language ${chalk.bold(language)} was ${chalk.bold("loaded")}.`);
}
_loadFile(file) {
if(process.platform === 'win32') {
file = file.split('\n').join('');
file = file.replace(/\r/gu, '\n');
}
const lines = file.split('\n');
const parsed = {};
let matched = null,
text = [];
for(const line of lines) {
if(line.startsWith('//') || line.startsWith('#')) continue;
const matches = line.match(/\[([_A-Z0-9]{1,})\]/u);
if(matches) {
if (matched) {
parsed[matched] = text.join('\n').trim();
[, matched] = matches;
text = [];
} else {
[, matched] = matches;
}
} else if (matched) {
text.push(line);
} else {
continue;
}
}
parsed[matched] = text.join('\n').trim();
return parsed;
}
}
module.exports = LocaleLoader;

View File

@ -4,5 +4,6 @@ module.exports = {
Logger: require('./Logger.js'), Logger: require('./Logger.js'),
Dispatcher: require('./Dispatcher.js'), Dispatcher: require('./Dispatcher.js'),
Registry: require('./Registry.js'), Registry: require('./Registry.js'),
Resolver: require('./Resolver.js') Resolver: require('./Resolver.js'),
LocaleLoader: require('./LocaleLoader.js')
}; };

View File

@ -1,4 +1,4 @@
const { SlashCommand } = require('../../../interfaces/'); const { SlashCommand, CommandOption } = require('../../../interfaces/');
class MuteCommand extends SlashCommand { class MuteCommand extends SlashCommand {
@ -7,12 +7,36 @@ class MuteCommand extends SlashCommand {
name: 'mute', name: 'mute',
description: "Silence people.", description: "Silence people.",
module: 'moderation', module: 'moderation',
arguments: [ options: [
] new CommandOption({
name: 'targets',
description: "Provide users to mute.",
type: 'MEMBER'
}),
new CommandOption({
name: 'reason',
description: "Provide a reason.",
type: 'STRING'
}),
new CommandOption({
name: 'points',
description: "Assign points to the infraction.",
type: 'INTEGER',
minimum: 0, maximum: 100
}),
new CommandOption({
name: 'channel',
type: 'TEXT_CHANNEL'
})
],
guildOnly: true
}); });
} }
async execute(thing) { async execute(interaction) {
// console.log(interaction, interaction.options);
interaction.reply(this.resolveable);
} }

View File

@ -1,4 +1,4 @@
const { Observer } = require('../../interfaces/'); const { Observer, Thing, CommandOption } = require('../../interfaces/');
class CommandHandler extends Observer { class CommandHandler extends Observer {
@ -21,8 +21,6 @@ class CommandHandler extends Observer {
async messageCreate(message) { async messageCreate(message) {
const { prefix } = this.client._options.discord;
if(!this.client._built if(!this.client._built
|| message.webhookId || message.webhookId
|| message.author.bot || message.author.bot
@ -40,17 +38,122 @@ class CommandHandler extends Observer {
} }
async interactionCreate(interaction) { async interactionCreate(interaction) {
if(!interaction.isCommand()) return undefined; if(!interaction.isCommand()
&& !interaction.isContextMenu()) return undefined;
// if(!this.client._built if(!this.client._built
// || message.guild && !message.guild.available) return undefined; || !interaction?.guild?.available) return undefined;
const command = this._matchCommand(interaction.commandName); const command = this._matchCommand(interaction.commandName);
console.log(interaction.commandName) const thing = new Thing(this.client, command, interaction);
if(!command) return interaction.reply('Command is not synced with client instance.');
interaction.reply(command.resolveable); if(!command) return thing.reply({ locale: 'O_COMMANDHANDLER_COMMANDNOTSYNCED', emoji: 'failure', ephemeral: true });
const response = await this._parseInteraction(thing);
}
async _parseInteraction(thing) {
const { command, interaction } = thing;
if(!interaction.guild && command.guildOnly) {
return thing.reply({ locale: 'O_COMMANDHANDLER_GUILDONLY', emoji: 'failure', ephemeral: true });
}
const options = [];
for(const option of interaction.options._hoistedOptions) {
const matched = command.options.find((o) => o.name === option.name);
const newOption = new CommandOption({ name: matched.name, type: matched.type, _rawValue: option.value });
const parsed = await this._parseOption(thing, newOption);
if(parsed.error) {
//uhh
}
newOption.value = parsed.value;
options.push(newOption);
}
console.log(options);
}
async _parseOption(thing, option) {
const types = {
ROLES: (string) => {
},
MEMBERS: (string) => {
},
USERS: (string) => {
},
CHANNELS: (string) => {
},
TEXT_CHANNELS: (string) => {
},
VOICE_CHANNELS: (string) => {
},
STRING: (string) => {
return { error: false, value: string };
},
INTEGER: (integer) => {
if(option.minimum !== undefined && integer < option.minimum) return { error: true };
if(option.maximum !== undefined && integer > option.maximum) return { error: true };
return { error: false, value: parseInt(integer) };
},
BOOLEAN: (boolean) => {
return { error: false, value: boolean };
},
MEMBER: async (user) => {
let member = null;
try {
member = await thing.guild.members.fetch(user);
} catch(error) {} //eslint-disable-line no-empty
if(!member) return { error: true };
return { error: false, value: member };
},
USER: (user) => {
return { error: false, value: user };
},
TEXT_CHANNEL: (channel) => {
if(channel.type !== 'GUILD_TEXT') return { error: true };
return { error: false, value: channel };
},
VOICE_CHANNEL: (channel) => {
if(channel.type !== 'GUILD_VOICE') return { error: true };
return { error: false, value: channel };
},
CHANNEL: (channel) => {
return { error: false, value: channel };
},
ROLE: (role) => {
return { error: false, value: role };
},
MENTIONABLE: (mentionable) => {
return { error: false, value: mentionable };
},
NUMBER: (number) => {
if(option.minimum !== undefined && number < option.minimum) return { error: true };
if(option.maximum !== undefined && number > option.maximum) return { error: true };
return { error: false, value: number };
},
FLOAT: (float) => {
if(option.minimum !== undefined && float < option.minimum) return { error: true };
if(option.maximum !== undefined && float > option.maximum) return { error: true };
return { error: false, value: parseFloat(float) };
}
};
const result = types[option.type](option._rawValue);
if(result instanceof Promise) await result;
return result;
} }
async _getCommand(message) { async _getCommand(message) {
@ -58,17 +161,9 @@ class CommandHandler extends Observer {
//TODO: Move this somewhere else. RegExp should not be created every method call, but it requires the client user to be loaded. //TODO: Move this somewhere else. RegExp should not be created every method call, but it requires the client user to be loaded.
const mentionPattern = new RegExp(`^(<@!?${this.client.user.id}>)`, 'iu'); const mentionPattern = new RegExp(`^(<@!?${this.client.user.id}>)`, 'iu');
const { prefix } = this.client._options.discord;
const start = message.content.slice(0, prefix.length);
let command = null, let command = null,
parameters = []; parameters = [];
if(start === prefix) { if(mentionPattern.test(message.content)) {
const remaining = message.content.slice(prefix.length);
const [ commandName, ...rest ] = remaining.split(" ");
command = this._matchCommand(message, commandName);
parameters = rest.join(" ");
} else if (mentionPattern.test(message.content)) {
const [ , commandName, ...rest] = message.content.split(" "); const [ , commandName, ...rest] = message.content.split(" ");
command = this._matchCommand(message, commandName); command = this._matchCommand(message, commandName);
parameters = rest.join(" "); parameters = rest.join(" ");
@ -79,11 +174,11 @@ class CommandHandler extends Observer {
} }
_matchCommand(commandName) { _matchCommand(commandName) {
const [ command ] = this.client.resolver.components(commandName, 'command', true); const [ command ] = this.client.resolver.components(commandName, 'command', true);
if(!command) return null; return command || null;
}
return command; _generateError() {
} }

View File

@ -0,0 +1,60 @@
const Constants = {
CommandOptionTypes: {
SUB_COMMAND: 1,
SUB_COMMAND_GROUP: 2,
ROLES: 3, //Note plurality, strings can parse users, roles, and channels.
MEMBERS: 3,
USERS: 3,
CHANNELS: 3,
TEXT_CHANNELS: 3,
VOICE_CHANNELS: 3,
STRING: 3,
INTEGER: 4,
BOOLEAN: 5,
MEMBER: 6,
USER: 6,
TEXT_CHANNEL: 7,
VOICE_CHANNEL: 7,
CHANNEL: 7,
ROLE: 8,
MENTIONABLE: 9,
NUMBER: 10,
FLOAT: 10
}
};
class CommandOption {
constructor(options = {}) {
this.name = options.name;
this.description = options.description || "A missing description, let a bot developer know.";
this.type = Object.keys(Constants.CommandOptionTypes).includes(options.type) ? options.type : 'STRING';
this.required = Boolean(options.required);
this.choices = options.choices || []; //Used for STRING/INTEGER/NUMBER types.
this.options = options.options || []; //Used for SUB_COMMAND/SUB_COMMAND_GROUP types.
this.minimum = options.minimum || undefined; //Used for INTEGER/NUMBER/FLOAT types.
this.maxiumum = options.maximum || undefined;
this.value = undefined;
this._rawValue = options._rawValue || null; //Raw value input from Discord.
}
get shape() {
return {
name: this.name,
description: this.description,
type: Constants.CommandOptionTypes[this.type],
required: this.required,
choices: this.choices,
options: this.options.map((o) => o.shape)
};
}
}
module.exports = CommandOption;

View File

@ -1,71 +1,74 @@
const { Emojis } = require('../../constants'); const { Emojis } = require('../../constants/');
class Thing { class Thing {
constructor(client, options = {}) { constructor(client, command, interaction) {
if(!options) return null;
this.message = options.message || null; this.client = client;
this.interaction = options.interaction || null; this.interaction = interaction;
this.command = command;
this.command = options.command; //Should always be provided. this.options = [];
this.arguments = options.arguments;
this.parameters = options.parameters || []; this._guild = null;
this._channel = null;
this._resolved = false; this._pending = null;
} }
async resolve() { async reply(options = {}) {
if(this.command.showUsage && !this.parameters.length && !this.arguments.length) { if(options.locale) {
console.log('Show usage embed'); //eslint-disable-line no-console options.content = this.format(options.locale);
return undefined; // delete options.locale;
} }
try { if(options.emoji) {
const response = this.command.execute(this); if(!Emojis[options.emoji]) this.client.logger.warn(`Invalid emoji provided to command ${this.command.resolveable}: "${options.emoji}"`);
if(response instanceof Promise) await response; options.content = `${Emojis[options.emoji]}${options.content}`;
this.command._invokes.successes++; // delete options.emoji;
this.client.emit('commandExecute', { instance: this, type: 'SUCCESS' });
return { error: false };
} catch(error) {
this.command._invokes.failures++;
this.client.emit('commandExecute', { instance: this, type: 'FAILURE' });
return { error: true, message: error };
} }
this._pending = this.interaction.reply(options);
return this._pending;
} }
format(locale) {
async respond(content, options = {}) { const language = 'en_us'; //Default language.
if(typeof content === 'string') { //TODO: Fetch guild/user settings and switch localization.
if(options.emoji && Emojis[options.emoji]) { return this.client.localeLoader.languages[language][locale];
content = `${Emojis[options.emoji]} ${content}`;
}
if(options.reply) content = `<@${this.message.author.id}> ${content}`;
} }
get guild() {
return this.interaction.guild || null;
} }
async send(options) { get channel() {
if(this.type === 'MESSAGE') { return this.interaction.channel || null;
this.message.channel.send({
});
} else {
this.interaction.reply({
});
}
} }
get type() { // async guild() {
if(this.message) return 'MESSAGE'; // if(!this.interaction.guild) return null;
return 'INTERACTION'; // if(this._guild) return this._guild;
// this._guild = await this.client.guilds.fetch(this.interaction.guildId);
// return this._guild;
// }
// async channel() {
// if(!this.interaction.channel) return null;
// if(this._channel) return this._channel;
// this._channel = await this.client.channels.fetch(this.interaction.channelId);
// return this._channel;
// }
get user() {
return this.interaction.user || null;
} }
get member() {
return this.interaction.member || null;
}
} }
module.exports = Thing; module.exports = Thing;

View File

@ -1,4 +1,4 @@
const Component = require('./Component.js'); const Component = require('../Component.js');
class Command extends Component { class Command extends Component {
@ -25,7 +25,8 @@ class Command extends Component {
this.guildOnly = Boolean(options?.guildOnly); this.guildOnly = Boolean(options?.guildOnly);
this.archivable = options.archivable === undefined ? true : Boolean(options.archivable); this.archivable = options.archivable === undefined ? true : Boolean(options.archivable);
this.slash = Boolean(options?.slash);
this.slash = Boolean(options.slash);
this._invokes = { this._invokes = {
success: 0, success: 0,

View File

@ -0,0 +1,8 @@
const { Command } = require('./Command.js');
class LegacyCommand extends Command {
}
module.exports = LegacyCommand;

View File

@ -1,5 +1,5 @@
const Command = require('./Command.js'); const Command = require('./Command.js');
const { Commands: CommandsConstant } = require('../../constants/'); const { Commands: CommandsConstant } = require('../../../constants/');
class SlashCommand extends Command { class SlashCommand extends Command {
@ -21,16 +21,16 @@ class SlashCommand extends Command {
this.type = Object.keys(CommandsConstant.ApplicationCommandTypes).includes(options.type) ? options.type : 'CHAT_INPUT'; this.type = Object.keys(CommandsConstant.ApplicationCommandTypes).includes(options.type) ? options.type : 'CHAT_INPUT';
this.options = options.options || []; this.options = options.options || [];
this.defaultPermission = options.defaultPermission || {}; this.defaultPermission = options.defaultPermission || true;
} }
get json() { get shape() {
return { return {
name: this.name, name: this.name,
description: this.description, description: this.description,
type: CommandsConstant.ApplicationCommandTypes[this.type], type: CommandsConstant.ApplicationCommandTypes[this.type],
options: this.options, options: this.options.map((o) => o.shape),
defaultPermission: this.defaultPermission defaultPermission: this.defaultPermission
}; };
} }

View File

@ -2,7 +2,8 @@ module.exports = {
Component: require('./Component.js'), Component: require('./Component.js'),
Observer: require('./Observer.js'), Observer: require('./Observer.js'),
Module: require('./Module.js'), Module: require('./Module.js'),
SlashCommand: require('./SlashCommand.js'), SlashCommand: require('./commands/SlashCommand.js'),
Command: require('./Command.js'), Command: require('./commands/Command.js'),
CommandOption: require('./CommandOption.js'),
Thing: require('./Thing.js') Thing: require('./Thing.js')
}; };