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",
"clientId": "697791541690892369",
"clientOptions": {
"intents": [
"GUILD",
"GUILD_MEMBERS",
"GUILD_MESSAGES"
]
},
"shardOptions": {
"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') {
await this.global(message.commands);
} 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;
const promises = [];
for(const guild of guilds) {
promises.push(this.rest.put(
Routes.applicationGuildCommands(this.client._options.discord.clientId, guild),
Routes.applicationGuildCommands(clientId, guild),
{ body: commands }
));
}

View File

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

View File

@ -24,9 +24,9 @@ class Intercom {
const commands = this.client.registry.components
.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'),
Dispatcher: require('./Dispatcher.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 {
@ -7,12 +7,36 @@ class MuteCommand extends SlashCommand {
name: 'mute',
description: "Silence people.",
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 {
@ -21,8 +21,6 @@ class CommandHandler extends Observer {
async messageCreate(message) {
const { prefix } = this.client._options.discord;
if(!this.client._built
|| message.webhookId
|| message.author.bot
@ -40,17 +38,122 @@ class CommandHandler extends Observer {
}
async interactionCreate(interaction) {
if(!interaction.isCommand()) return undefined;
if(!interaction.isCommand()
&& !interaction.isContextMenu()) return undefined;
// if(!this.client._built
// || message.guild && !message.guild.available) return undefined;
if(!this.client._built
|| !interaction?.guild?.available) return undefined;
const command = this._matchCommand(interaction.commandName);
console.log(interaction.commandName)
if(!command) return interaction.reply('Command is not synced with client instance.');
const thing = new Thing(this.client, command, interaction);
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) {
@ -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.
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,
parameters = [];
if(start === prefix) {
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)) {
if(mentionPattern.test(message.content)) {
const [ , commandName, ...rest] = message.content.split(" ");
command = this._matchCommand(message, commandName);
parameters = rest.join(" ");
@ -79,11 +174,11 @@ class CommandHandler extends Observer {
}
_matchCommand(commandName) {
const [ command ] = this.client.resolver.components(commandName, 'command', true);
if(!command) return null;
return command;
return command || null;
}
_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 {
constructor(client, options = {}) {
if(!options) return null;
constructor(client, command, interaction) {
this.message = options.message || null;
this.interaction = options.interaction || null;
this.client = client;
this.interaction = interaction;
this.command = command;
this.command = options.command; //Should always be provided.
this.arguments = options.arguments;
this.options = [];
this.parameters = options.parameters || [];
this._resolved = false;
this._guild = null;
this._channel = null;
this._pending = null;
}
async resolve() {
async reply(options = {}) {
if(this.command.showUsage && !this.parameters.length && !this.arguments.length) {
console.log('Show usage embed'); //eslint-disable-line no-console
return undefined;
if(options.locale) {
options.content = this.format(options.locale);
// delete options.locale;
}
try {
const response = this.command.execute(this);
if(response instanceof Promise) await response;
this.command._invokes.successes++;
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 };
}
}
async respond(content, options = {}) {
if(typeof content === 'string') {
if(options.emoji && Emojis[options.emoji]) {
content = `${Emojis[options.emoji]} ${content}`;
}
if(options.reply) content = `<@${this.message.author.id}> ${content}`;
if(options.emoji) {
if(!Emojis[options.emoji]) this.client.logger.warn(`Invalid emoji provided to command ${this.command.resolveable}: "${options.emoji}"`);
options.content = `${Emojis[options.emoji]}${options.content}`;
// delete options.emoji;
}
this._pending = this.interaction.reply(options);
return this._pending;
}
async send(options) {
if(this.type === 'MESSAGE') {
this.message.channel.send({
});
} else {
this.interaction.reply({
});
}
format(locale) {
const language = 'en_us'; //Default language.
//TODO: Fetch guild/user settings and switch localization.
return this.client.localeLoader.languages[language][locale];
}
get type() {
if(this.message) return 'MESSAGE';
return 'INTERACTION';
get guild() {
return this.interaction.guild || null;
}
get channel() {
return this.interaction.channel || null;
}
// async guild() {
// if(!this.interaction.guild) return null;
// 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;

View File

@ -1,4 +1,4 @@
const Component = require('./Component.js');
const Component = require('../Component.js');
class Command extends Component {
@ -25,7 +25,8 @@ class Command extends Component {
this.guildOnly = Boolean(options?.guildOnly);
this.archivable = options.archivable === undefined ? true : Boolean(options.archivable);
this.slash = Boolean(options?.slash);
this.slash = Boolean(options.slash);
this._invokes = {
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 { Commands: CommandsConstant } = require('../../constants/');
const { Commands: CommandsConstant } = require('../../../constants/');
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.options = options.options || [];
this.defaultPermission = options.defaultPermission || {};
this.defaultPermission = options.defaultPermission || true;
}
get json() {
get shape() {
return {
name: this.name,
description: this.description,
type: CommandsConstant.ApplicationCommandTypes[this.type],
options: this.options,
options: this.options.map((o) => o.shape),
defaultPermission: this.defaultPermission
};
}

View File

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