diff --git a/src/commands/fun/eco.ts b/src/commands/fun/eco.ts index c87241a..b265ebb 100644 --- a/src/commands/fun/eco.ts +++ b/src/commands/fun/eco.ts @@ -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); + } } }) }); diff --git a/src/commands/fun/whois.ts b/src/commands/fun/whois.ts index d7285f6..0c16b38 100644 --- a/src/commands/fun/whois.ts +++ b/src/commands/fun/whois.ts @@ -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); } } }) diff --git a/src/commands/utility/info.ts b/src/commands/utility/info.ts index 6b7ca82..a47e549 100644 --- a/src/commands/utility/info.ts +++ b/src/commands/utility/info.ts @@ -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); } } }) diff --git a/src/commands/utility/time.ts b/src/commands/utility/time.ts index 23250ba..b48a4a2 100644 --- a/src/commands/utility/time.ts +++ b/src/commands/utility/time.ts @@ -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); } }) }); diff --git a/src/core/command.ts b/src/core/command.ts index a89449b..0e7e5e4 100644 --- a/src/core/command.ts +++ b/src/core/command.ts @@ -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(""); - 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(""); + 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(""); - 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(""); + 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(""); - 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(""); @@ -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(""); + 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); diff --git a/src/core/index.ts b/src/core/index.ts index 5d75c14..16d1116 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -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"; diff --git a/src/core/interface.ts b/src/core/interface.ts index 1e1df4b..f6ead8f 100644 --- a/src/core/interface.ts +++ b/src/core/interface.ts @@ -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())(); +export let client = new Client(); export let permissionLevels: PermissionLevel[] = [ { name: "User", diff --git a/src/core/libd.ts b/src/core/libd.ts index 7f52c75..45bf919 100644 --- a/src/core/libd.ts +++ b/src/core/libd.ts @@ -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 { + 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 { + 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 { + 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 { + 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 { + 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}\`!`}; + } + } +}