Framework Rewrite

This commit is contained in:
Dragon Fire
2021-06-05 12:17:33 -04:00
parent 5c9f237321
commit 7917766ce3
48 changed files with 1101 additions and 294 deletions
+20
View File
@@ -0,0 +1,20 @@
const UnionType = require('./UnionType');
module.exports = class Argument {
constructor(client, options) {
Object.defineProperty(this, 'client', { value: client });
this.key = options.key.toLowerCase();
this.typeID = options.type.toLowerCase();
this.min = options.min;
this.max = options.max;
this.oneOf = options.oneOf;
this.default = options.default;
this.avatarSize = options.avatarSize || 2048;
}
get type() {
if (this.typeID.includes('|')) return new UnionType(this.client, this.typeID);
return this.client.registry.types.get(this.typeID);
}
};
+17
View File
@@ -0,0 +1,17 @@
module.exports = class ArgumentType {
constructor(id) {
this.id = id.toLowerCase();
}
validate(val) {
return Boolean(val);
}
parse(val) {
return val;
}
isEmpty(val) {
return !val;
}
};
+228
View File
@@ -0,0 +1,228 @@
const { Client } = require('discord.js');
const fs = require('fs');
const { stripIndents } = require('common-tags');
const Registry = require('./Registry');
const Dispatcher = require('./Dispatcher');
const Patreon = require('../structures/Patreon');
require('./Extensions');
module.exports = class CommandClient extends Client {
constructor(options) {
super(options);
this.commandPrefix = options.commandPrefix;
this.owner = typeof options.owner === 'string' ? [options.owner] : options.owner;
this.invite = options.invite || null;
this.registry = new Registry(this);
this.dispatcher = new Dispatcher(this);
this.patreon = new Patreon();
this.blacklist = { user: [], guild: [] };
this._throttlingTimeouts = new Map();
this.once('ready', this.onceReady);
this.on('message', this.onMessage);
}
isOwner(user) {
return this.owners.includes(user.id);
}
async onceReady() {
for (const owner of this.owner) {
await this.users.fetch(owner);
}
}
async onMessage(msg) {
if (!msg.author) return;
if (msg.channel.partial) msg.channel = await this.channels.fetch(msg.channel.id);
if (msg.partial) msg = await msg.channel.messages.fetch(msg.id);
if (msg.member.partial || !msg.guild.members.cache.has(msg.author.id)) {
await msg.guild.members.fetch(msg.author.id);
}
if (msg.author.bot) return;
if (this.blacklist.user.includes(msg.author.id)) return;
if (msg.guild && this.blacklist.guild.includes(msg.guild.id)) return;
if (!msg.channel.permissionsFor(this.user).has('SEND_MESSAGES')) return;
if (!this.dispatcher.isCommand(msg)) return;
const parsed = await this.dispatcher.parseMessage(msg);
if (typeof parsed === 'string') {
const helpUsage = this.registry.commands.get('help').usage();
await msg.reply(`${parsed} Use ${helpUsage} for more information.`);
return;
}
const { command, args } = parsed;
if (!command._enabled) {
await msg.reply(`The \`${command.name}\` command is disabled.`);
return;
}
if (command.ownerOnly && !this.isOwner(msg.author)) {
await msg.reply(`The \`${command.name}\` command can only be used by the bot owner.`);
return;
}
if (command.guildOnly && !msg.guild) {
await msg.reply(`The \`${command.name}\` command can only be used in a server channel.`);
return;
}
if (command.nsfw && !msg.channel.nsfw) {
await msg.reply(`The \`${command.name}\` command can only be used in NSFW channels.`);
return;
}
if (command.patronOnly && !this.patreon.isPatron(msg.author.id)) {
await msg.reply(stripIndents`
The \`${command.name}\` command can only be used by Patrons.
Visit <https://www.patreon.com/xiaodiscord> to sign-up!
`);
return;
}
if (command.clientPermissions.length) {
for (const permission of command.clientPermissions) {
if (msg.channel.permissionsFor(this.user).has(permission)) continue;
await msg.reply(`The \`${command.name}\` command requires me to have the "${permission}" permission.`);
return;
}
}
if (command.userPermissions.length) {
for (const permission of command.userPermissions) {
if (msg.channel.permissionsFor(msg.author).has(permission)) continue;
await msg.reply(`You need the "${permission}" permission to use the \`${command.name}\` command.`);
return;
}
}
const throttleAmount = command.throttles.get(msg.author.id) || 0;
if (throttleAmount >= command.throttling.uses) {
const timeout = command._timeouts.get(msg.author.id);
await msg.reply(`Please wait ${getTimeLeft(timeout)} seconds before using the \`${command.name}\` command again.`);
return;
}
command.throttles.set(msg.author.id, throttleAmount + 1);
if (!throttleAmount) {
const timeout = setTimeout(() => command.throttles.delete(msg.author.id), command.throttling.duration);
command._timeouts.set(msg.author.id, timeout);
}
try {
const result = await command.run(msg, args);
command.uses++;
command.lastRun = new Date();
this.emit('commandRun', command, result, msg, args);
} catch (err) {
this.emit('commandError', command, err, msg, args);
await msg.reply(stripIndents`
An error occurred while running this command: \`${err.message}\`.
You shouldn't ever recieve an error like this.
${this.invite ? `Please visit ${this.invite} for support.` : ''}
`);
}
}
importBlacklist() {
const read = fs.readFileSync(path.join(__dirname, '..', 'blacklist.json'), { encoding: 'utf8' });
const file = JSON.parse(read);
if (typeof file !== 'object' || Array.isArray(file)) return null;
if (!file.guild || !file.user) return null;
for (const id of file.guild) {
if (typeof id !== 'string') continue;
if (this.blacklist.guild.includes(id)) continue;
this.blacklist.guild.push(id);
}
for (const id of file.user) {
if (typeof id !== 'string') continue;
if (this.blacklist.user.includes(id)) continue;
this.blacklist.user.push(id);
}
return file;
}
exportBlacklist() {
let text = '{\n "guild": [\n ';
if (this.blacklist.guild.length) {
for (const id of this.blacklist.guild) {
text += `"${id}",\n `;
}
text = text.slice(0, -4);
}
text += '\n ],\n "user": [\n ';
if (this.blacklist.user.length) {
for (const id of this.blacklist.user) {
text += `"${id}",\n `;
}
text = text.slice(0, -4);
}
text += '\n ]\n}\n';
const buf = Buffer.from(text);
fs.writeFileSync(path.join(__dirname, '..', 'blacklist.json'), buf, { encoding: 'utf8' });
return buf;
}
importCommandLeaderboard(add = false) {
const read = fs.readFileSync(path.join(__dirname, '..', 'command-leaderboard.json'), {
encoding: 'utf8'
});
const file = JSON.parse(read);
if (typeof file !== 'object' || Array.isArray(file)) return null;
for (const [id, value] of Object.entries(file)) {
if (typeof value !== 'number') continue;
const found = this.registry.commands.get(id);
if (!found || found.uses === undefined) continue;
if (add) found.uses += value;
else found.uses = value;
}
return file;
}
exportCommandLeaderboard() {
let text = '{';
for (const command of this.registry.commands.values()) {
if (command.unknown) continue;
if (command.uses === undefined) continue;
text += `\n "${command.name}": ${command.uses},`;
}
text = text.slice(0, -1);
text += '\n}\n';
const buf = Buffer.from(text);
fs.writeFileSync(path.join(__dirname, '..', 'command-leaderboard.json'), buf, {
encoding: 'utf8'
});
return buf;
}
importLastRun() {
const read = fs.readFileSync(path.join(__dirname, '..', 'command-last-run.json'), {
encoding: 'utf8'
});
const file = JSON.parse(read);
if (typeof file !== 'object' || Array.isArray(file)) return null;
for (const [id, value] of Object.entries(file)) {
if (!value) continue;
const date = new Date(value);
if (date.toString() === 'Invalid Date') continue;
const found = this.registry.commands.get(id);
if (!found || found.lastRun === undefined) continue;
found.lastRun = date;
}
return file;
}
exportLastRun() {
let text = '{';
for (const command of this.registry.commands.values()) {
if (command.unknown) continue;
if (command.lastRun === undefined) continue;
text += `\n "${command.name}": ${command.lastRun ? `"${command.lastRun.toISOString()}"` : null},`;
}
text = text.slice(0, -1);
text += '\n}\n';
const buf = Buffer.from(text);
fs.writeFileSync(path.join(__dirname, '..', 'command-last-run.json'), buf, {
encoding: 'utf8'
});
return buf;
}
};
function getTimeLeft(timeout) {
return Math.ceil((timeout._idleStart + timeout._idleTimeout - Date.now()) / 1000);
}
+52
View File
@@ -0,0 +1,52 @@
const Argument = require('./Argument');
module.exports = class Command {
constructor(client, options) {
Object.defineProperty(this, 'client', { value: client });
this.name = options.name.toLowerCase();
this.aliases = options.aliases ? options.aliases.map(alias => alias.toLowerCase()) : [];
this.groupID = options.group.toLowerCase();
this.memberName = options.memberName.toLowerCase();
this.description = options.description;
this.details = options.details || null;
this.args = options.args ? options.args.map(arg => new Argument(arg)) : [];
this.clientPermissions = options.clientPermissions || [];
this.userPermissions = options.userPermissions || [];
this.ownerOnly = options.ownerOnly || false;
this.nsfw = options.nsfw || false;
this.guildOnly = options.guildOnly || false;
this.patronOnly = options.patronOnly || false;
this.guarded = options.guarded || false;
this.throttling = options.throttling || { usages: 2, duration: 5 };
this.credit = options.credit || [];
this.credit.push({
name: 'Dragon Fire',
url: 'https://github.com/dragonfire535',
reason: 'Code'
});
this.uses = 0;
this.lastRun = null;
this.throttles = new Map();
this._timeouts = new Map();
this._enabled = true;
}
get group() {
return this.client.registry.groups.get(this.groupID);
}
usage() {
const args = this.args
.map(arg => `${arg.default ? '[' : '<'}${arg.label || arg.name}${arg.default ? ']' : '>'}`).join(' ');
return `\`${this.client.commandPrefix}${this.name} ${args}\` or \`@${this.client.user.tag} ${this.name} ${args}\``;
}
disable() {
this._enabled = false;
}
enable() {
this._enabled = true;
}
};
+58
View File
@@ -0,0 +1,58 @@
const minimist = require('minimist');
const argRegex = /"([^"]*)"|(\S+)/g;
module.exports = class CommandDispatcher {
constructor(client) {
Object.defineProperty(this, 'client', { value: client });
this._commandPattern = null;
}
get commandPattern() {
if (this._commandPattern) return this._commandPattern;
const prefix = this.client.commandPrefix;
this._commandPattern = new RegExp(
`^(<@!?${this.client.user.id}>\\s+(?:${prefix}}\\s*)?|${prefix}\\s*)([^\\s]+)`, 'i'
);
return this._commandPattern;
}
isCommand(msg) {
const command = msg.content.match(this.commandPattern);
return Boolean(command);
}
async parseMessage(msg) {
const command = this.resolveCommand(command[2].toLowerCase());
if (!command) {
return {
command: this.registry.commands.find(cmd => cmd.unknown),
args: { command: command[2].toLowerCase() }
};
}
const content = msg.content.replace(this.commandPattern, '').trim();
const result = (content.match(argRegex) || []).map(m => m.replace(argRegex, '$1$2'));
const parsed = minimist(result);
const result = { flags: [...parsed] };
for (let i = 0; i > command.args.length; i++) {
const arg = command.args[i];
const parsedArg = result._[i];
if (arg.isEmpty(parsedArg, msg, arg)) {
if (arg.default) {
result[arg.name] = typeof arg.default === 'function' ? arg.default(msg) : arg.default;
continue;
} else {
return `The "${arg.label || arg.name}" argument is required.`;
}
}
const valid = await arg.validate(parsedArg, msg, arg);
if (!valid) return `An invalid value was provided for the "${arg.label || arg.name}" argument.`;
result[arg.name] = await arg.parse(parsedArg, msg, arg);
}
return { command, args: result };
}
resolveCommand(command) {
return this.registry.commands.find(cmd => cmd.name === command || cmd.aliases.includes(command));
}
};
+21
View File
@@ -0,0 +1,21 @@
const { Structures } = require('discord.js');
module.exports = Structures.extend('Message', Message => {
return class CommandMessage extends Message {
constructor(...args) {
super(...args);
}
say(content, options) {
return this.channel.send(content, options);
}
embed(embed, options) {
return this.channel.send('', { embed, ...options });
}
code(lang, content, options) {
return this.channel.send(content, { code: lang, ...options });
}
}
});
+12
View File
@@ -0,0 +1,12 @@
module.exports = class Group {
constructor(client, id, name) {
Object.defineProperty(this, 'client', { value: client });
this.id = id.toLowerCase();
this.name = name;
}
get commands() {
return this.client.registry.commands.filter(command => command.groupID === this.id);
}
};
+74
View File
@@ -0,0 +1,74 @@
const Collection = require('@discordjs/collection');
const fs = require('fs');
const path = require('path');
const Group = require('./Group');
module.exports = class Registry {
constructor(client) {
Object.defineProperty(this, 'client', { value: client });
this.commands = new Collection();
this.groups = new Collection();
this.types = new Collection();
}
findCommands(query) {
query = query.toLowerCase();
return this.commands.filter(command => command.name === query || command.aliases.includes(query));
}
registerCommand(command) {
this.commands.set(command.name, command);
return this;
}
registerCommandsIn(dir) {
const groups = fs.readdirSync(dir);
for (const group of groups) {
const commands = fs.readdirSync(path.join(dir, group));
for (const command of commands) {
if (!command.endsWith('.js')) continue;
const required = require(path.join(dir, group, command));
this.registerCommand(new required(this.client));
}
}
return this;
}
findGroups(query) {
query = query.toLowerCase();
return this.groups.filter(group => group.id === query || group.name.toLowerCase() === query);
}
registerGroup(group) {
this.groups.set(group.id, group);
return this;
}
registerGroups(groups) {
for (const [id, name] of groups) {
const group = new Group(this.client, id, name);
this.registerGroup(group);
}
return this;
}
registerType(type) {
this.types.set(type.id, type);
return this;
}
registerTypesIn(dir) {
const types = fs.readdirSync(dir);
for (const type of types) {
if (!type.endsWith('.js')) continue;
const required = require(path.join(dir, type));
this.registerType(new required(this.client));
}
return this;
}
registerDefaultTypes() {
return this.registerTypesIn(path.join(__dirname, 'types'));
}
};
+37
View File
@@ -0,0 +1,37 @@
const ArgumentType = require('./ArgumentType');
module.exports = class ArgumentUnionType extends ArgumentType {
constructor(client, id) {
super(client, id);
this.types = [];
const typeIDs = id.split('|');
for (const typeID of typeIDs) {
const type = client.registry.types.get(typeID);
if (!type) throw new Error(`Argument type "${typeID}" is not registered.`);
this.types.push(type);
}
}
async validate(val, msg, arg) {
let results = this.types.map(type => !type.isEmpty(val, msg, arg) && type.validate(val, msg, arg));
results = await Promise.all(results);
if (results.some(valid => valid && typeof valid !== 'string')) return true;
const errors = results.filter(valid => typeof valid === 'string');
if (errors.length > 0) return errors.join('\n');
return false;
}
async parse(val, msg, arg) {
let results = this.types.map(type => !type.isEmpty(val, msg, arg) && type.validate(val, msg, arg));
results = await Promise.all(results);
for (let i = 0; i < results.length; i++) {
if (results[i] && typeof results[i] !== 'string') return this.types[i].parse(val, msg, arg);
}
throw new Error(`Couldn't parse value "${val}" with union type ${this.id}.`);
}
isEmpty(val, msg, arg) {
return !this.types.some(type => !type.isEmpty(val, msg, arg));
}
};
+21
View File
@@ -0,0 +1,21 @@
const ArgumentType = require('../ArgumentType');
module.exports = class BooleanArgumentType extends ArgumentType {
constructor(client) {
super(client, 'boolean');
this.truthy = new Set(['true', 't', 'yes', 'y', 'on', 'enable', 'enabled', '1', '+']);
this.falsy = new Set(['false', 'f', 'no', 'n', 'off', 'disable', 'disabled', '0', '-']);
}
validate(val) {
const lc = val.toLowerCase();
return this.truthy.has(lc) || this.falsy.has(lc);
}
parse(val) {
const lc = val.toLowerCase();
if (this.truthy.has(lc)) return true;
if (this.falsy.has(lc)) return false;
throw new RangeError('Unknown boolean value.');
}
}
+39
View File
@@ -0,0 +1,39 @@
const ArgumentType = require('../ArgumentType');
module.exports = class ChannelArgumentType extends ArgumentType {
constructor(client) {
super(client, 'channel');
}
validate(val, msg) {
const matches = val.match(/^(?:<#)?([0-9]+)>?$/);
if (matches) return msg.guild.channels.cache.has(matches[1]);
const search = val.toLowerCase();
const channels = msg.guild.channels.cache.filter(nameFilterInexact(search));
if (channels.size === 0) return false;
if (channels.size === 1) return true;
const exactChannels = channels.filter(nameFilterExact(search));
if (exactChannels.size === 1) return true;
return false;
}
parse(val, msg) {
const matches = val.match(/^(?:<#)?([0-9]+)>?$/);
if (matches) return msg.guild.channels.cache.get(matches[1]) || null;
const search = val.toLowerCase();
const channels = msg.guild.channels.cache.filter(nameFilterInexact(search));
if (channels.size === 0) return null;
if (channels.size === 1) return channels.first();
const exactChannels = channels.filter(nameFilterExact(search));
if (exactChannels.size === 1) return exactChannels.first();
return null;
}
}
function nameFilterExact(search) {
return thing => thing.name.toLowerCase() === search;
}
function nameFilterInexact(search) {
return thing => thing.name.toLowerCase().includes(search);
}
+17
View File
@@ -0,0 +1,17 @@
const ArgumentType = require('../ArgumentType');
module.exports = class CommandArgumentType extends ArgumentType {
constructor(client) {
super(client, 'command');
}
validate(val) {
const commands = this.client.registry.findCommands(val);
if (commands.size === 1) return true;
return false;
}
parse(val) {
return this.client.registry.findCommands(val).first();
}
}
+40
View File
@@ -0,0 +1,40 @@
const ArgumentType = require('../ArgumentType');
module.exports = class CustomEmojiArgumentType extends ArgumentType {
constructor(client) {
super(client, 'custom-emoji');
}
validate(value, msg) {
const matches = value.match(/^(?:<a?:([a-zA-Z0-9_]+):)?([0-9]+)>?$/);
if (matches && msg.client.emojis.cache.has(matches[2])) return true;
if (!msg.guild) return false;
const search = value.toLowerCase();
const emojis = msg.guild.emojis.cache.filter(nameFilterInexact(search));
if (!emojis.size) return false;
if (emojis.size === 1) return true;
const exactEmojis = emojis.filter(nameFilterExact(search));
if (exactEmojis.size === 1) return true;
return false;
}
parse(value, msg) {
const matches = value.match(/^(?:<a?:([a-zA-Z0-9_]+):)?([0-9]+)>?$/);
if (matches) return msg.client.emojis.cache.get(matches[2]) || null;
const search = value.toLowerCase();
const emojis = msg.guild.emojis.cache.filter(nameFilterInexact(search));
if (!emojis.size) return null;
if (emojis.size === 1) return emojis.first();
const exactEmojis = emojis.filter(nameFilterExact(search));
if (exactEmojis.size === 1) return exactEmojis.first();
return null;
}
}
function nameFilterExact(search) {
return emoji => emoji.name.toLowerCase() === search;
}
function nameFilterInexact(search) {
return emoji => emoji.name.toLowerCase().includes(search);
}
+18
View File
@@ -0,0 +1,18 @@
const ArgumentType = require('../ArgumentType');
const emojiRegex = require('emoji-regex/RGI_Emoji.js');
module.exports = class DefaultEmojiArgumentType extends ArgumentType {
constructor(client) {
super(client, 'default-emoji');
this.regex = new RegExp(`^(?:${emojiRegex().source})$`);
}
validate(value) {
if (!this.regex.test(value)) return false;
return true;
}
parse(value) {
return value;
}
}
+20
View File
@@ -0,0 +1,20 @@
const ArgumentType = require('../ArgumentType');
module.exports = class FloatArgumentType extends ArgumentType {
constructor(client) {
super(client, 'float');
}
validate(val, msg, arg) {
const float = Number.parseFloat(val);
if (Number.isNaN(float)) return false;
if (arg.oneOf && !arg.oneOf.includes(float)) return false;
if (arg.min !== null && typeof arg.min !== 'undefined' && float < arg.min) return false;
if (arg.max !== null && typeof arg.max !== 'undefined' && float > arg.max) return false;
return true;
}
parse(val) {
return Number.parseFloat(val);
}
}
+17
View File
@@ -0,0 +1,17 @@
const ArgumentType = require('../ArgumentType');
module.exports = class GroupArgumentType extends ArgumentType {
constructor(client) {
super(client, 'group');
}
validate(val) {
const groups = this.client.registry.findGroups(val);
if (groups.size === 1) return true;
return false;
}
parse(val) {
return this.client.registry.findGroups(val).first();
}
}
+20
View File
@@ -0,0 +1,20 @@
const ArgumentType = require('../ArgumentType');
module.exports = class IntegerArgumentType extends ArgumentType {
constructor(client) {
super(client, 'integer');
}
validate(val, msg, arg) {
const int = Number.parseInt(val);
if (Number.isNaN(int)) return false;
if (arg.oneOf && !arg.oneOf.includes(int)) return false;
if (arg.min !== null && typeof arg.min !== 'undefined' && int < arg.min) return false;
if (arg.max !== null && typeof arg.max !== 'undefined' && int > arg.max) return false;
return true;
}
parse(val) {
return Number.parseInt(val);
}
}
+51
View File
@@ -0,0 +1,51 @@
const ArgumentType = require('../ArgumentType');
module.exports = class MemberArgumentType extends ArgumentType {
constructor(client) {
super(client, 'member');
}
async validate(val, msg) {
const matches = val.match(/^(?:<@!?)?([0-9]+)>?$/);
if (matches) {
try {
const member = await msg.guild.members.fetch(await this.client.users.fetch(matches[1]));
if (!member) return false;
return true;
} catch (err) {
return false;
}
}
const search = val.toLowerCase();
const members = msg.guild.members.cache.filter(memberFilterInexact(search));
if (members.size === 0) return false;
if (members.size === 1) return true;
const exactMembers = members.filter(memberFilterExact(search));
if (exactMembers.size === 1) return true;
return false;
}
parse(val, msg) {
const matches = val.match(/^(?:<@!?)?([0-9]+)>?$/);
if (matches) return msg.guild.members.resolve(matches[1]) || null;
const search = val.toLowerCase();
const members = msg.guild.members.cache.filter(memberFilterInexact(search));
if (members.size === 0) return null;
if (members.size === 1) return members.first();
const exactMembers = members.filter(memberFilterExact(search));
if (exactMembers.size === 1) return exactMembers.first();
return null;
}
}
function memberFilterExact(search) {
return mem => mem.user.username.toLowerCase() === search ||
(mem.nickname && mem.nickname.toLowerCase() === search) ||
mem.tag.toLowerCase() === search;
}
function memberFilterInexact(search) {
return mem => mem.user.username.toLowerCase().includes(search) ||
(mem.nickname && mem.nickname.toLowerCase().includes(search)) ||
mem.tag.toLowerCase().includes(search);
}
+16
View File
@@ -0,0 +1,16 @@
const ArgumentType = require('../ArgumentType');
module.exports = class MessageArgumentType extends ArgumentType {
constructor(client) {
super(client, 'message');
}
async validate(val, msg) {
if (!/^[0-9]+$/.test(val)) return false;
return Boolean(await msg.channel.messages.fetch(val).catch(() => null));
}
parse(val, msg) {
return msg.channel.messages.cache.get(val);
}
}
+39
View File
@@ -0,0 +1,39 @@
const ArgumentType = require('../ArgumentType');
module.exports = class RoleArgumentType extends ArgumentType {
constructor(client) {
super(client, 'role');
}
validate(val, msg) {
const matches = val.match(/^(?:<@&)?([0-9]+)>?$/);
if (matches) return msg.guild.roles.cache.has(matches[1]);
const search = val.toLowerCase();
const roles = msg.guild.roles.cache.filter(nameFilterInexact(search));
if (roles.size === 0) return false;
if (roles.size === 1) return true;
const exactRoles = roles.filter(nameFilterExact(search));
if (exactRoles.size === 1) return true;
return false;
}
parse(val, msg) {
const matches = val.match(/^(?:<@&)?([0-9]+)>?$/);
if (matches) return msg.guild.roles.cache.get(matches[1]) || null;
const search = val.toLowerCase();
const roles = msg.guild.roles.cache.filter(nameFilterInexact(search));
if (roles.size === 0) return null;
if (roles.size === 1) return roles.first();
const exactRoles = roles.filter(nameFilterExact(search));
if (exactRoles.size === 1) return exactRoles.first();
return null;
}
}
function nameFilterExact(search) {
return thing => thing.name.toLowerCase() === search;
}
function nameFilterInexact(search) {
return thing => thing.name.toLowerCase().includes(search);
}
+18
View File
@@ -0,0 +1,18 @@
const ArgumentType = require('../ArgumentType');
module.exports = class StringArgumentType extends ArgumentType {
constructor(client) {
super(client, 'string');
}
validate(val, msg, arg) {
if (arg.oneOf && !arg.oneOf.includes(val.toLowerCase())) return false;
if (arg.min !== null && typeof arg.min !== 'undefined' && val.length < arg.min) return false;
if (arg.max !== null && typeof arg.max !== 'undefined' && val.length > arg.max) return false;
return true;
}
parse(val) {
return val;
}
}
+53
View File
@@ -0,0 +1,53 @@
const ArgumentType = require('../ArgumentType');
module.exports = class UserArgumentType extends ArgumentType {
constructor(client) {
super(client, 'user');
}
async validate(val, msg, arg) {
const matches = val.match(/^(?:<@!?)?([0-9]+)>?$/);
if (matches) {
try {
const user = await msg.client.users.fetch(matches[1]);
if (!user) return false;
return true;
} catch (err) {
return false;
}
}
if (!msg.guild) return false;
const search = val.toLowerCase();
const members = msg.guild.members.cache.filter(memberFilterInexact(search));
if (members.size === 0) return false;
if (members.size === 1) return true;
const exactMembers = members.filter(memberFilterExact(search));
if (exactMembers.size === 1) return true;
return false;
}
parse(val, msg) {
const matches = val.match(/^(?:<@!?)?([0-9]+)>?$/);
if (matches) return msg.client.users.cache.get(matches[1]) || null;
if (!msg.guild) return null;
const search = val.toLowerCase();
const members = msg.guild.members.cache.filter(memberFilterInexact(search));
if (members.size === 0) return null;
if (members.size === 1) return members.first().user;
const exactMembers = members.filter(memberFilterExact(search));
if (exactMembers.size === 1) return exactMembers.first().user;
return null;
}
}
function memberFilterExact(search) {
return mem => mem.user.username.toLowerCase() === search ||
(mem.nickname && mem.nickname.toLowerCase() === search) ||
mem.tag.toLowerCase() === search;
}
function memberFilterInexact(search) {
return mem => mem.user.username.toLowerCase().includes(search) ||
(mem.nickname && mem.nickname.toLowerCase().includes(search)) ||
mem.tag.toLowerCase().includes(search);
}