Added more library functions to command handler

This commit is contained in:
WatDuhHekBro 2021-04-10 06:41:48 -05:00
parent bd67f3b8cc
commit 54ce28d8d4
8 changed files with 217 additions and 130 deletions

View File

@ -1,9 +1,10 @@
import {Command, NamedCommand, callMemberByUsername} from "../../core";
import {Command, NamedCommand, getMemberByName} from "../../core";
import {isAuthorized, getMoneyEmbed} from "./modules/eco-utils";
import {DailyCommand, PayCommand, GuildCommand, LeaderboardCommand} from "./modules/eco-core";
import {BuyCommand, ShopCommand} from "./modules/eco-shop";
import {MondayCommand, AwardCommand} from "./modules/eco-extras";
import {BetCommand} from "./modules/eco-bet";
import {GuildMember} from "discord.js";
export default new NamedCommand({
description: "Economy command for Monika.",
@ -35,10 +36,11 @@ export default new NamedCommand({
any: new Command({
description: "See how much money someone else has by using their username.",
async run({guild, channel, args, message}) {
if (isAuthorized(guild, channel))
callMemberByUsername(message, args.join(" "), (member) => {
channel.send(getMoneyEmbed(member.user));
});
if (isAuthorized(guild, channel)) {
const member = await getMemberByName(guild!, args.join(" "));
if (member instanceof GuildMember) channel.send(getMoneyEmbed(member.user));
else channel.send(member);
}
}
})
});

View File

@ -1,5 +1,5 @@
import {User} from "discord.js";
import {Command, NamedCommand, getMemberByUsername, CHANNEL_TYPE} from "../../core";
import {User, GuildMember} from "discord.js";
import {Command, NamedCommand, getMemberByName, CHANNEL_TYPE} from "../../core";
// Quotes must be used here or the numbers will change
const registry: {[id: string]: string} = {
@ -69,12 +69,10 @@ export default new NamedCommand({
channelType: CHANNEL_TYPE.GUILD,
async run({message, channel, guild, author, client, args}) {
const query = args.join(" ") as string;
const member = await getMemberByUsername(guild!, query);
const member = await getMemberByName(guild!, query);
if (member && member.id in registry) {
const id = member.id;
if (id in registry) {
if (member instanceof GuildMember) {
if (member.id in registry) {
channel.send(`\`${member.nickname ?? member.user.username}\` - ${registry[member.id]}`);
} else {
channel.send(
@ -82,7 +80,7 @@ export default new NamedCommand({
);
}
} else {
channel.send(`Couldn't find a user by the name of \`${query}\`!`);
channel.send(member);
}
}
})

View File

@ -1,7 +1,7 @@
import {MessageEmbed, version as djsversion, Guild, User, GuildMember} from "discord.js";
import ms from "ms";
import os from "os";
import {Command, NamedCommand, getMemberByUsername, CHANNEL_TYPE} from "../../core";
import {Command, NamedCommand, getMemberByName, CHANNEL_TYPE} from "../../core";
import {formatBytes, trimArray} from "../../lib";
import {verificationLevels, filterLevels, regions} from "../../defs/info";
import moment, {utc} from "moment";
@ -35,9 +35,9 @@ export default new NamedCommand({
channelType: CHANNEL_TYPE.GUILD,
async run({message, channel, guild, author, client, args}) {
const name = args.join(" ");
const member = await getMemberByUsername(guild!, name);
const member = await getMemberByName(guild!, name);
if (member) {
if (member instanceof GuildMember) {
channel.send(
member.user.displayAvatarURL({
dynamic: true,
@ -45,7 +45,7 @@ export default new NamedCommand({
})
);
} else {
channel.send(`No user found by the name \`${name}\`!`);
channel.send(member);
}
}
})

View File

@ -1,6 +1,6 @@
import {Command, NamedCommand, ask, askYesOrNo, askMultipleChoice, prompt, callMemberByUsername} from "../../core";
import {Command, NamedCommand, ask, askYesOrNo, askMultipleChoice, prompt, getMemberByName} from "../../core";
import {Storage} from "../../structures";
import {User} from "discord.js";
import {User, GuildMember} from "discord.js";
import moment from "moment";
const DATE_FORMAT = "D MMMM YYYY";
@ -383,10 +383,10 @@ export default new NamedCommand({
}),
any: new Command({
description: "See what time it is for someone else (by their username).",
async run({channel, args, message}) {
callMemberByUsername(message, args.join(" "), (member) => {
channel.send(getTimeEmbed(member.user));
});
async run({channel, args, guild}) {
const member = await getMemberByName(guild!, args.join(" "));
if (member instanceof GuildMember) channel.send(getTimeEmbed(member.user));
else channel.send(member);
}
})
});

View File

@ -8,9 +8,10 @@ import {
Guild,
User,
GuildMember,
GuildChannel
GuildChannel,
Channel
} from "discord.js";
import {SingleMessageOptions} from "./libd";
import {getChannelByID, getMessageByID, getUserByID, SingleMessageOptions} from "./libd";
import {hasPermission, getPermissionLevel, getPermissionName} from "./permissions";
import {getPrefix} from "./interface";
import {parseVars, requireAllCasesHandledFor} from "../lib";
@ -338,17 +339,20 @@ export class Command {
return this.subcommands.get(param)!.execute(args, menu, metadata);
} else if (this.channel && patterns.channel.test(param)) {
const id = patterns.channel.exec(param)![1];
const channel = menu.client.channels.cache.get(id);
const channel = await getChannelByID(id);
// Users can only enter in this format for text channels, so this restricts it to that.
if (channel instanceof TextChannel) {
metadata.symbolicArgs.push("<channel>");
menu.args.push(channel);
return this.channel.execute(args, menu, metadata);
if (channel instanceof Channel) {
if (channel instanceof TextChannel || channel instanceof DMChannel) {
metadata.symbolicArgs.push("<channel>");
menu.args.push(channel);
return this.channel.execute(args, menu, metadata);
} else {
return {
content: `\`${id}\` is not a valid text channel!`
};
}
} else {
return {
content: `\`${id}\` is not a valid text channel!`
};
return channel;
}
} else if (this.role && patterns.role.test(param)) {
const id = patterns.role.exec(param)![1];
@ -397,34 +401,25 @@ export class Command {
messageID = result[2];
}
const channel = menu.client.channels.cache.get(channelID);
const message = await getMessageByID(channelID, messageID);
if (channel instanceof TextChannel || channel instanceof DMChannel) {
try {
metadata.symbolicArgs.push("<message>");
menu.args.push(await channel.messages.fetch(messageID));
return this.message.execute(args, menu, metadata);
} catch {
return {
content: `\`${messageID}\` isn't a valid message of channel ${channel}!`
};
}
if (message instanceof Message) {
metadata.symbolicArgs.push("<message>");
menu.args.push(message);
return this.message.execute(args, menu, metadata);
} else {
return {
content: `\`${channelID}\` is not a valid text channel!`
};
return message;
}
} else if (this.user && patterns.user.test(param)) {
const id = patterns.user.exec(param)![1];
const user = await getUserByID(id);
try {
if (user instanceof User) {
metadata.symbolicArgs.push("<user>");
menu.args.push(await menu.client.users.fetch(id));
menu.args.push(user);
return this.user.execute(args, menu, metadata);
} catch {
return {
content: `No user found by the ID \`${id}\`!`
};
} else {
return user;
}
} else if (this.id && this.idType && patterns.id.test(param)) {
metadata.symbolicArgs.push("<id>");
@ -434,16 +429,20 @@ export class Command {
// Because this part is pretty much a whole bunch of copy pastes.
switch (this.idType) {
case "channel":
const channel = menu.client.channels.cache.get(id);
const channel = await getChannelByID(id);
// Users can only enter in this format for text channels, so this restricts it to that.
if (channel instanceof TextChannel) {
menu.args.push(channel);
return this.id.execute(args, menu, metadata);
if (channel instanceof Channel) {
if (channel instanceof TextChannel || channel instanceof DMChannel) {
metadata.symbolicArgs.push("<channel>");
menu.args.push(channel);
return this.id.execute(args, menu, metadata);
} else {
return {
content: `\`${id}\` is not a valid text channel!`
};
}
} else {
return {
content: `\`${id}\` isn't a valid text channel!`
};
return channel;
}
case "role":
if (!menu.guild) {
@ -474,22 +473,22 @@ export class Command {
};
}
case "message":
try {
menu.args.push(await menu.channel.messages.fetch(id));
const message = await getMessageByID(menu.channel, id);
if (message instanceof Message) {
menu.args.push(message);
return this.id.execute(args, menu, metadata);
} catch {
return {
content: `\`${id}\` isn't a valid message of channel ${menu.channel}!`
};
} else {
return message;
}
case "user":
try {
menu.args.push(await menu.client.users.fetch(id));
const user = await getUserByID(id);
if (user instanceof User) {
menu.args.push(user);
return this.id.execute(args, menu, metadata);
} catch {
return {
content: `No user found by the ID \`${id}\`!`
};
} else {
return user;
}
default:
requireAllCasesHandledFor(this.idType);

View File

@ -2,16 +2,6 @@
export {Command, NamedCommand, CHANNEL_TYPE} from "./command";
export {addInterceptRule} from "./handler";
export {launch} from "./interface";
export {
SingleMessageOptions,
botHasPermission,
paginate,
prompt,
ask,
askYesOrNo,
askMultipleChoice,
getMemberByUsername,
callMemberByUsername
} from "./libd";
export * from "./libd";
export {getCommandList, getCommandInfo} from "./loader";
export {hasPermission, getPermissionLevel, getPermissionName} from "./permissions";

View File

@ -25,11 +25,13 @@ interface LaunchSettings {
// Additionally, each method would return the object so multiple methods could be chained, such as OnionCore.setPermissions(...).setPrefixResolver(...).launch(client).
// I decided to not do this because creating a class then having a bunch of boilerplate around it just wouldn't really be worth it.
// commandsDirectory requires an absolute path to work, so use __dirname.
export async function launch(client: Client, commandsDirectory: string, settings?: LaunchSettings) {
export async function launch(newClient: Client, commandsDirectory: string, settings?: LaunchSettings) {
// Core Launch Parameters //
client.destroy(); // Release any resources/connections being used by the placeholder client.
client = newClient;
loadableCommands = loadCommands(commandsDirectory);
attachMessageHandlerToClient(client);
attachEventListenersToClient(client);
attachMessageHandlerToClient(newClient);
attachEventListenersToClient(newClient);
// Additional Configuration //
if (settings?.permissionLevels) {
@ -42,6 +44,7 @@ export async function launch(client: Client, commandsDirectory: string, settings
// Placeholder until properly loaded by the user.
export let loadableCommands = (async () => new Collection<string, NamedCommand>())();
export let client = new Client();
export let permissionLevels: PermissionLevel[] = [
{
name: "User",

View File

@ -7,9 +7,13 @@ import {
TextChannel,
DMChannel,
NewsChannel,
MessageOptions
MessageOptions,
Channel,
GuildChannel,
User
} from "discord.js";
import {unreactEventListeners, replyEventListeners} from "./eventListeners";
import {client} from "./interface";
export type SingleMessageOptions = MessageOptions & {split?: false};
@ -20,16 +24,19 @@ export function botHasPermission(guild: Guild | null, permission: number): boole
return !!guild?.me?.hasPermission(permission);
}
// The SoonTM Section //
// Maybe promisify this section to reduce the potential for creating callback hell? Especially if multiple questions in a row are being asked.
// Pagination function that allows for customization via a callback.
// Define your own pages outside the function because this only manages the actual turning of pages.
// It's probably a good idea to modularize the base reaction handler so there's less copy pasted code.
// Maybe also make a reaction handler that listens for when reactions are added and removed.
// The reaction handler would also run an async function to react in order (parallel to the reaction handler).
const FIVE_BACKWARDS_EMOJI = "⏪";
const BACKWARDS_EMOJI = "⬅️";
const FORWARDS_EMOJI = "➡️";
const FIVE_FORWARDS_EMOJI = "⏩";
// Pagination function that allows for customization via a callback.
// Define your own pages outside the function because this only manages the actual turning of pages.
/**
* Takes a message and some additional parameters and makes a reaction page with it. All the pagination logic is taken care of but nothing more, the page index is returned and you have to send a callback to do something with it.
*/
@ -251,44 +258,132 @@ export async function askMultipleChoice(
if (!isDeleted) message.delete();
}
/**
* Gets a user by their username. Gets the first one then rolls with it.
*/
export async function getMemberByUsername(guild: Guild, username: string) {
return (
await guild.members.fetch({
query: username,
limit: 1
})
).first();
}
/**
* Convenience function to handle cases where someone isn't found by a username automatically.
*/
export async function callMemberByUsername(
message: Message,
username: string,
onSuccess: (member: GuildMember) => void
) {
const guild = message.guild;
const send = message.channel.send;
if (guild) {
const member = await getMemberByUsername(guild, username);
if (member) onSuccess(member);
else send(`Couldn't find a user by the name of \`${username}\`!`);
} else send("You must execute this command in a server!");
}
// TO DO Section //
// getGuildByID() - checks for guild.available (boolean)
// getGuildByName()
// findMemberByNickname() - gets a member by their nickname or their username
// findUserByUsername()
// For "get x by y" methods:
// Caching: All guilds, channels, and roles are fully cached, while the caches for messages, users, and members aren't complete.
// It's more reliable to get users/members by fetching their IDs. fetch() will searching through the cache anyway.
// For guilds, do an extra check to make sure there isn't an outage (guild.available).
export function getGuildByID(id: string): Guild | SingleMessageOptions {
const guild = client.guilds.cache.get(id);
if (guild) {
if (guild.available) return guild;
else return {content: `The guild \`${guild.name}\` (ID: \`${id}\`) is unavailable due to an outage.`};
} else {
return {
content: `No guild found by the ID of \`${id}\`!`
};
}
}
export function getGuildByName(name: string): Guild | SingleMessageOptions {
const query = name.toLowerCase();
const guild = client.guilds.cache.find((guild) => guild.name.toLowerCase().includes(query));
if (guild) {
if (guild.available) return guild;
else return {content: `The guild \`${guild.name}\` (ID: \`${guild.id}\`) is unavailable due to an outage.`};
} else {
return {
content: `No guild found by the name of \`${name}\`!`
};
}
}
export async function getChannelByID(id: string): Promise<Channel | SingleMessageOptions> {
try {
return await client.channels.fetch(id);
} catch {
return {content: `No channel found by the ID of \`${id}\`!`};
}
}
// Only go through the cached channels (non-DM channels). Plus, searching DM channels by name wouldn't really make sense, nor do they have names to search anyway.
export function getChannelByName(name: string): GuildChannel | SingleMessageOptions {
const query = name.toLowerCase();
const channel = client.channels.cache.find(
(channel) => channel instanceof GuildChannel && channel.name.toLowerCase().includes(query)
) as GuildChannel | undefined;
if (channel) return channel;
else return {content: `No channel found by the name of \`${name}\`!`};
}
export async function getMessageByID(
channel: TextChannel | DMChannel | NewsChannel | string,
id: string
): Promise<Message | SingleMessageOptions> {
if (typeof channel === "string") {
const targetChannel = await getChannelByID(channel);
if (targetChannel instanceof TextChannel || targetChannel instanceof DMChannel) channel = targetChannel;
else if (targetChannel instanceof Channel) return {content: `\`${id}\` isn't a valid text-based channel!`};
else return targetChannel;
}
try {
return await channel.messages.fetch(id);
} catch {
return {content: `\`${id}\` isn't a valid message of the channel ${channel}!`};
}
}
export async function getUserByID(id: string): Promise<User | SingleMessageOptions> {
try {
return await client.users.fetch(id);
} catch {
return {content: `No user found by the ID of \`${id}\`!`};
}
}
// Also check tags (if provided) to narrow down users.
export function getUserByName(name: string): User | SingleMessageOptions {
let query = name.toLowerCase();
const tagMatch = /^(.+?)#(\d{4})$/.exec(name);
let tag: string | null = null;
if (tagMatch) {
query = tagMatch[1].toLowerCase();
tag = tagMatch[2];
}
const user = client.users.cache.find((user) => {
const hasUsernameMatch = user.username.toLowerCase().includes(query);
if (tag) return hasUsernameMatch && user.discriminator === tag;
else return hasUsernameMatch;
});
if (user) return user;
else return {content: `No user found by the name of \`${name}\`!`};
}
export async function getMemberByID(guild: Guild, id: string): Promise<GuildMember | SingleMessageOptions> {
try {
return await guild.members.fetch(id);
} catch {
return {content: `No member found by the ID of \`${id}\`!`};
}
}
// First checks if a member can be found by that nickname, then check if a member can be found by that username.
export async function getMemberByName(guild: Guild, name: string): Promise<GuildMember | SingleMessageOptions> {
const member = (
await guild.members.fetch({
query: name,
limit: 1
})
).first();
// Search by username if no member is found, then resolve the user into a member if possible.
if (member) {
return member;
} else {
const user = getUserByName(name);
if (user instanceof User) {
const member = guild.members.resolve(user);
if (member) return member;
else return {content: `The user \`${user.tag}\` isn't in this guild!`};
} else {
return {content: `No member found by the name of \`${name}\`!`};
}
}
}