From 20fb2135c77bf36879b56494b204d4d531c646e0 Mon Sep 17 00:00:00 2001 From: WatDuhHekBro <44940783+WatDuhHekBro@users.noreply.github.com> Date: Thu, 8 Apr 2021 06:37:49 -0500 Subject: [PATCH 01/14] Implemented various ideas from backlog --- CHANGELOG.md | 10 +++ src/commands/fun/eco.ts | 9 +- src/commands/fun/modules/eco-extras.ts | 44 ++++++++++ src/commands/fun/party.ts | 13 +++ src/commands/fun/thonk.ts | 9 +- src/commands/fun/urban.ts | 44 +++++----- src/commands/fun/vaporwave.ts | 34 ++++++++ src/commands/fun/weather.ts | 62 +++++++------- src/commands/system/admin.ts | 16 +++- src/commands/utility/emote.ts | 5 +- src/commands/utility/modules/emote-utils.ts | 1 + src/commands/utility/scanemotes.ts | 10 +++ src/commands/utility/translate.ts | 11 ++- src/defs/translate.d.ts | 9 ++ src/defs/urban.d.ts | 17 ++++ src/defs/weather.d.ts | 92 +++++++++++++++++++++ 16 files changed, 322 insertions(+), 64 deletions(-) create mode 100644 src/commands/fun/party.ts create mode 100644 src/commands/fun/vaporwave.ts create mode 100644 src/defs/translate.d.ts create mode 100644 src/defs/urban.d.ts create mode 100644 src/defs/weather.d.ts diff --git a/CHANGELOG.md b/CHANGELOG.md index fa8c546..4142c6c 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,3 +1,13 @@ +# ??? +- `vaporwave`: Transforms input into full-width text +- `eco post`: A play on `eco get` +- `admin set prefix (<@bot>)`: Allows you to target a bot when setting a prefix if two bots have conflicting prefixes +- `party`: Sets the bot's status to streaming with a certain URL +- `eco award`: Awards users with Mons, only accessible by that person +- `thonk`: A result can now be discarded if the person who called the command reacts with ❌ +- `scanemotes forcereset`: Removes the cooldown on `scanemotes`, only accessible by bot support and up +- `urban`: Bug fixes + # 3.2.0 - Internal refactor, more subcommand types, and more command type guards (2021-??-??) - The custom logger changed: `$.log` no longer exists, it's just `console.log`. Now you don't have to do `import $ from "../core/lib"` at the top of every file that uses the custom logger. - Utility functions are no longer attached to the command menu. Stuff like `$.paginate()` and `$(5).pluralise()` instead need to be imported and used as regular functions. diff --git a/src/commands/fun/eco.ts b/src/commands/fun/eco.ts index 022383a..c87241a 100644 --- a/src/commands/fun/eco.ts +++ b/src/commands/fun/eco.ts @@ -2,7 +2,7 @@ import {Command, NamedCommand, callMemberByUsername} 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} from "./modules/eco-extras"; +import {MondayCommand, AwardCommand} from "./modules/eco-extras"; import {BetCommand} from "./modules/eco-bet"; export default new NamedCommand({ @@ -18,7 +18,12 @@ export default new NamedCommand({ buy: BuyCommand, shop: ShopCommand, monday: MondayCommand, - bet: BetCommand + bet: BetCommand, + award: AwardCommand, + post: new NamedCommand({ + description: "A play on `eco get`", + run: "`405 Method Not Allowed`" + }) }, id: "user", user: new Command({ diff --git a/src/commands/fun/modules/eco-extras.ts b/src/commands/fun/modules/eco-extras.ts index f2760dd..2d69f29 100644 --- a/src/commands/fun/modules/eco-extras.ts +++ b/src/commands/fun/modules/eco-extras.ts @@ -1,6 +1,8 @@ import {Command, NamedCommand} from "../../../core"; import {Storage} from "../../../structures"; import {isAuthorized, getMoneyEmbed} from "./eco-utils"; +import {User} from "discord.js"; +import {pluralise} from "../../../lib"; const WEEKDAY = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; @@ -32,3 +34,45 @@ export const MondayCommand = new NamedCommand({ } } }); + +export const AwardCommand = new NamedCommand({ + description: "Only usable by Mon, awards one or a specified amount of Mons to the user.", + usage: " ()", + aliases: ["give"], + run: "You need to specify a user!", + user: new Command({ + async run({message, channel, guild, author, member, client, args}) { + if (author.id === "394808963356688394" || IS_DEV_MODE) { + const target = args[0] as User; + const user = Storage.getUser(target.id); + user.money++; + Storage.save(); + channel.send(`1 Mon given to ${target.username}.`, getMoneyEmbed(target)); + } else { + channel.send("This command is restricted to the bean."); + } + }, + number: new Command({ + async run({message, channel, guild, author, member, client, args}) { + if (author.id === "394808963356688394" || IS_DEV_MODE) { + const target = args[0] as User; + const amount = Math.floor(args[1]); + + if (amount > 0) { + const user = Storage.getUser(target.id); + user.money += amount; + Storage.save(); + channel.send( + `${pluralise(amount, "Mon", "s")} given to ${target.username}.`, + getMoneyEmbed(target) + ); + } else { + channel.send("You need to enter a number greater than 0."); + } + } else { + channel.send("This command is restricted to the bean."); + } + } + }) + }) +}); diff --git a/src/commands/fun/party.ts b/src/commands/fun/party.ts new file mode 100644 index 0000000..90e62c5 --- /dev/null +++ b/src/commands/fun/party.ts @@ -0,0 +1,13 @@ +import {Command, NamedCommand} from "../../core"; + +export default new NamedCommand({ + description: "Initiates a celebratory stream from the bot.", + async run({message, channel, guild, author, member, client, args}) { + channel.send("This calls for a celebration!"); + client.user!.setActivity({ + type: "STREAMING", + url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", + name: "Celebration!" + }); + } +}); diff --git a/src/commands/fun/thonk.ts b/src/commands/fun/thonk.ts index 762549e..35e4d4a 100644 --- a/src/commands/fun/thonk.ts +++ b/src/commands/fun/thonk.ts @@ -36,6 +36,13 @@ export default new NamedCommand({ usage: "thonk ([text])", async run({message, channel, guild, author, member, client, args}) { if (args.length > 0) phrase = args.join(" "); - channel.send(transform(phrase)); + const msg = await channel.send(transform(phrase)); + msg.createReactionCollector( + (reaction, user) => { + if (user.id === author.id && reaction.emoji.name === "❌") msg.delete(); + return false; + }, + {time: 60000} + ); } }); diff --git a/src/commands/fun/urban.ts b/src/commands/fun/urban.ts index 2901433..212a2a5 100644 --- a/src/commands/fun/urban.ts +++ b/src/commands/fun/urban.ts @@ -1,27 +1,31 @@ import {Command, NamedCommand} from "../../core"; import {MessageEmbed} from "discord.js"; -// Anycasting Alert -const urban = require("relevant-urban"); +import urban from "relevant-urban"; export default new NamedCommand({ description: "Gives you a definition of the inputted word.", - async run({message, channel, guild, author, member, client, args}) { - if (!args[0]) { - channel.send("Please input a word."); + run: "Please input a word.", + any: new Command({ + async run({message, channel, guild, author, member, client, args}) { + // [Bug Fix]: Use encodeURIComponent() when emojis are used: "TypeError [ERR_UNESCAPED_CHARACTERS]: Request path contains unescaped characters" + urban(encodeURIComponent(args.join(" "))) + .then((res) => { + const embed = new MessageEmbed() + .setColor(0x1d2439) + .setTitle(res.word) + .setURL(res.urbanURL) + .setDescription(`**Definition:**\n*${res.definition}*\n\n**Example:**\n*${res.example}*`) + // [Bug Fix] When an embed field is empty (if the author field is missing, like the top entry for "british"): "RangeError [EMBED_FIELD_VALUE]: MessageEmbed field values may not be empty." + .addField("Author", res.author || "N/A", true) + .addField("Rating", `**\`Upvotes: ${res.thumbsUp} | Downvotes: ${res.thumbsDown}\`**`); + if (res.tags && res.tags.length > 0 && res.tags.join(" ").length < 1024) + embed.addField("Tags", res.tags.join(", "), true); + + channel.send(embed); + }) + .catch(() => { + channel.send("Sorry, that word was not found."); + }); } - const res = await urban(args.join(" ")).catch((e: Error) => { - return channel.send("Sorry, that word was not found."); - }); - const embed = new MessageEmbed() - .setColor(0x1d2439) - .setTitle(res.word) - .setURL(res.urbanURL) - .setDescription(`**Definition:**\n*${res.definition}*\n\n**Example:**\n*${res.example}*`) - .addField("Author", res.author, true) - .addField("Rating", `**\`Upvotes: ${res.thumbsUp} | Downvotes: ${res.thumbsDown}\`**`); - if (res.tags.length > 0 && res.tags.join(" ").length < 1024) { - embed.addField("Tags", res.tags.join(", "), true); - } - channel.send(embed); - } + }) }); diff --git a/src/commands/fun/vaporwave.ts b/src/commands/fun/vaporwave.ts new file mode 100644 index 0000000..ad6b945 --- /dev/null +++ b/src/commands/fun/vaporwave.ts @@ -0,0 +1,34 @@ +import {Command, NamedCommand} from "../../core"; + +const vaporwave = (() => { + const map = new Map(); + const vaporwave = + "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!"#$%&'()*+,-./0123456789:;<=>?@[\]^_`{|}~ "; + const normal = "ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz!\"#$%&'()*+,-./0123456789:;<=>?@[\\]^_`{|}~ "; + if (vaporwave.length !== normal.length) console.error("Vaporwave text failed to load properly!"); + for (let i = 0; i < vaporwave.length; i++) map.set(normal[i], vaporwave[i]); + return map; +})(); + +function getVaporwaveText(text: string): string { + let output = ""; + + for (const c of text) { + const transformed = vaporwave.get(c); + if (transformed) output += transformed; + } + + return output; +} + +export default new NamedCommand({ + description: "Transforms your text into vaporwave.", + run: "You need to enter some text!", + any: new Command({ + async run({message, channel, guild, author, member, client, args}) { + const text = getVaporwaveText(args.join(" ")); + if (text !== "") channel.send(text); + else channel.send("Make sure to enter at least one valid character."); + } + }) +}); diff --git a/src/commands/fun/weather.ts b/src/commands/fun/weather.ts index 590755f..07c44f5 100644 --- a/src/commands/fun/weather.ts +++ b/src/commands/fun/weather.ts @@ -1,36 +1,38 @@ import {Command, NamedCommand} from "../../core"; import {MessageEmbed} from "discord.js"; -// Anycasting Alert -const weather = require("weather-js"); +import {find} from "weather-js"; export default new NamedCommand({ description: "Shows weather info of specified location.", - async run({message, channel, guild, author, member, client, args}) { - if (args.length == 0) return channel.send("You need to provide a city."); - return weather.find( - { - search: args.join(" "), - degreeType: "C" - }, - function (err: any, result: any) { - if (err) channel.send(err); - var current = result[0].current; - var location = result[0].location; - const embed = new MessageEmbed() - .setDescription(`**${current.skytext}**`) - .setAuthor(`Weather for ${current.observationpoint}`) - .setThumbnail(current.imageUrl) - .setColor(0x00ae86) - .addField("Timezone", `UTC${location.timezone}`, true) - .addField("Degree Type", "C", true) - .addField("Temperature", `${current.temperature} Degrees`, true) - .addField("Feels like", `${current.feelslike} Degrees`, true) - .addField("Winds", current.winddisplay, true) - .addField("Humidity", `${current.humidity}%`, true); - channel.send({ - embed - }); - } - ); - } + run: "You need to provide a city.", + any: new Command({ + async run({message, channel, guild, author, member, client, args}) { + find( + { + search: args.join(" "), + degreeType: "C" + }, + function (error, result) { + if (error) return channel.send(error.toString()); + if (result.length === 0) return channel.send("No city found by that name."); + var current = result[0].current; + var location = result[0].location; + const embed = new MessageEmbed() + .setDescription(`**${current.skytext}**`) + .setAuthor(`Weather for ${current.observationpoint}`) + .setThumbnail(current.imageUrl) + .setColor(0x00ae86) + .addField("Timezone", `UTC${location.timezone}`, true) + .addField("Degree Type", "C", true) + .addField("Temperature", `${current.temperature} Degrees`, true) + .addField("Feels like", `${current.feelslike} Degrees`, true) + .addField("Winds", current.winddisplay, true) + .addField("Humidity", `${current.humidity}%`, true); + return channel.send({ + embed + }); + } + ); + } + }) }); diff --git a/src/commands/system/admin.ts b/src/commands/system/admin.ts index 5e0c225..f487e07 100644 --- a/src/commands/system/admin.ts +++ b/src/commands/system/admin.ts @@ -1,7 +1,7 @@ import {Command, NamedCommand, botHasPermission, getPermissionLevel, getPermissionName, CHANNEL_TYPE} from "../../core"; import {clean} from "../../lib"; import {Config, Storage} from "../../structures"; -import {Permissions, TextChannel} from "discord.js"; +import {Permissions, TextChannel, User} from "discord.js"; import {logs} from "../../modules/globals"; function getLogBuffer(type: string) { @@ -34,7 +34,7 @@ export default new NamedCommand({ subcommands: { prefix: new NamedCommand({ description: "Set a custom prefix for your guild. Removes your custom prefix if none is provided.", - usage: "()", + usage: "() (<@bot>)", async run({message, channel, guild, author, member, client, args}) { Storage.getGuild(guild!.id).prefix = null; Storage.save(); @@ -47,7 +47,17 @@ export default new NamedCommand({ Storage.getGuild(guild!.id).prefix = args[0]; Storage.save(); channel.send(`The custom prefix for this guild is now \`${args[0]}\`.`); - } + }, + user: new Command({ + description: "Specifies the bot in case of conflicting prefixes.", + async run({message, channel, guild, author, member, client, args}) { + if ((args[1] as User).id === client.user!.id) { + Storage.getGuild(guild!.id).prefix = args[0]; + Storage.save(); + channel.send(`The custom prefix for this guild is now \`${args[0]}\`.`); + } + } + }) }) }), welcome: new NamedCommand({ diff --git a/src/commands/utility/emote.ts b/src/commands/utility/emote.ts index 22bc97b..1b8aa07 100644 --- a/src/commands/utility/emote.ts +++ b/src/commands/utility/emote.ts @@ -2,8 +2,9 @@ import {Command, NamedCommand} from "../../core"; import {processEmoteQueryFormatted} from "./modules/emote-utils"; export default new NamedCommand({ - description: "Send the specified emote.", - run: "Please provide a command name.", + description: + "Send the specified emote list. Enter + to move an emote list to the next line, - to add a space, and _ to add a zero-width space.", + run: "Please provide a list of emotes.", any: new Command({ description: "The emote(s) to send.", usage: "", diff --git a/src/commands/utility/modules/emote-utils.ts b/src/commands/utility/modules/emote-utils.ts index f766cba..2c6df68 100644 --- a/src/commands/utility/modules/emote-utils.ts +++ b/src/commands/utility/modules/emote-utils.ts @@ -76,6 +76,7 @@ function processEmoteQuery(query: string[], isFormatted: boolean): string[] { if (isFormatted) { if (emote == "-") return " "; if (emote == "+") return "\n"; + if (emote == "_") return "\u200b"; } // Selector number used for disambiguating multiple emotes with same name. diff --git a/src/commands/utility/scanemotes.ts b/src/commands/utility/scanemotes.ts index a60d861..3662c57 100644 --- a/src/commands/utility/scanemotes.ts +++ b/src/commands/utility/scanemotes.ts @@ -182,5 +182,15 @@ export default new NamedCommand({ } return await channel.send(lines, {split: true}); + }, + subcommands: { + forcereset: new NamedCommand({ + description: "Forces the cooldown timer to reset.", + permission: PERMISSIONS.BOT_SUPPORT, + async run({message, channel, guild, author, member, client, args}) { + lastUsedTimestamps[guild!.id] = 0; + channel.send("Reset the cooldown on `scanemotes`."); + } + }) } }); diff --git a/src/commands/utility/translate.ts b/src/commands/utility/translate.ts index b697855..e0e9250 100644 --- a/src/commands/utility/translate.ts +++ b/src/commands/utility/translate.ts @@ -1,6 +1,5 @@ import {Command, NamedCommand} from "../../core"; -// Anycasting Alert -const translate = require("translate-google"); +import translate from "translate-google"; export default new NamedCommand({ description: "Translates your input.", @@ -11,7 +10,7 @@ export default new NamedCommand({ translate(input, { to: lang }) - .then((res: any) => { + .then((res) => { channel.send({ embed: { title: "Translation", @@ -28,10 +27,10 @@ export default new NamedCommand({ } }); }) - .catch((err: any) => { - console.error(err); + .catch((error) => { + console.error(error); channel.send( - `${err}\nPlease use the following list: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes` + `${error}\nPlease use the following list: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes` ); }); } diff --git a/src/defs/translate.d.ts b/src/defs/translate.d.ts new file mode 100644 index 0000000..15e022b --- /dev/null +++ b/src/defs/translate.d.ts @@ -0,0 +1,9 @@ +interface TranslateOptions { + from?: string; + to?: string; +} + +declare module "translate-google" { + function translate(input: string, options: TranslateOptions): Promise; + export = translate; +} diff --git a/src/defs/urban.d.ts b/src/defs/urban.d.ts new file mode 100644 index 0000000..cc97806 --- /dev/null +++ b/src/defs/urban.d.ts @@ -0,0 +1,17 @@ +interface Definition { + id: number; + word: string; + thumbsUp: number; + thumbsDown: number; + author: string; + urbanURL: string; + example: string; + definition: string; + tags: string[] | null; + sounds: string[] | null; +} + +declare module "relevant-urban" { + function urban(query: string): Promise; + export = urban; +} diff --git a/src/defs/weather.d.ts b/src/defs/weather.d.ts new file mode 100644 index 0000000..972de8c --- /dev/null +++ b/src/defs/weather.d.ts @@ -0,0 +1,92 @@ +interface WeatherJSOptions { + search: string; + lang?: string; + degreeType?: string; + timeout?: number; +} + +interface WeatherJSResult { + location: { + name: string; + lat: string; + long: string; + timezone: string; + alert: string; + degreetype: string; + imagerelativeurl: string; + }; + current: { + temperature: string; + skycode: string; + skytext: string; + date: string; + observationtime: string; + observationpoint: string; + feelslike: string; + humidity: string; + winddisplay: string; + day: string; + shortday: string; + windspeed: string; + imageUrl: string; + }; + forecast: [ + { + low: string; + high: string; + skycodeday: string; + skytextday: string; + date: string; + day: string; + shortday: string; + precip: string; + }, + { + low: string; + high: string; + skycodeday: string; + skytextday: string; + date: string; + day: string; + shortday: string; + precip: string; + }, + { + low: string; + high: string; + skycodeday: string; + skytextday: string; + date: string; + day: string; + shortday: string; + precip: string; + }, + { + low: string; + high: string; + skycodeday: string; + skytextday: string; + date: string; + day: string; + shortday: string; + precip: string; + }, + { + low: string; + high: string; + skycodeday: string; + skytextday: string; + date: string; + day: string; + shortday: string; + precip: string; + } + ]; +} + +declare module "weather-js" { + const find: ( + options: WeatherJSOptions, + callback: (error: Error | string | null, result: WeatherJSResult[]) => any + ) => void; +} From 72ff144cc0323d8770a009f0020b82bed1f8842b Mon Sep 17 00:00:00 2001 From: WatDuhHekBro <44940783+WatDuhHekBro@users.noreply.github.com> Date: Fri, 9 Apr 2021 23:06:16 -0500 Subject: [PATCH 02/14] Split command resolution part of help command --- src/commands/system/help.ts | 53 ++++++----------------------- src/commands/utility/info.ts | 2 +- src/commands/utility/lsemotes.ts | 2 +- src/commands/utility/react.ts | 6 ++-- src/core/command.ts | 53 +++++++++++++++++++---------- src/core/handler.ts | 8 ++++- src/core/index.ts | 3 +- src/core/libd.ts | 27 ++++++++++++--- src/core/loader.ts | 57 ++++++++++++++++++++++++++++++-- src/lib.ts | 9 +++-- src/modules/messageEmbed.ts | 2 +- src/structures.ts | 10 +++--- 12 files changed, 146 insertions(+), 86 deletions(-) diff --git a/src/commands/system/help.ts b/src/commands/system/help.ts index d205a10..53a701c 100644 --- a/src/commands/system/help.ts +++ b/src/commands/system/help.ts @@ -1,61 +1,28 @@ -import {Command, NamedCommand, loadableCommands, categories, getPermissionName, CHANNEL_TYPE} from "../../core"; -import {toTitleCase, requireAllCasesHandledFor} from "../../lib"; +import {Command, NamedCommand, CHANNEL_TYPE, getPermissionName, getCommandList, getCommandInfo} from "../../core"; +import {requireAllCasesHandledFor} from "../../lib"; export default new NamedCommand({ description: "Lists all commands. If a command is specified, their arguments are listed as well.", usage: "([command, [subcommand/type], ...])", aliases: ["h"], async run({message, channel, guild, author, member, client, args}) { - const commands = await loadableCommands; + const commands = await getCommandList(); let output = `Legend: \`\`, \`[list/of/stuff]\`, \`(optional)\`, \`()\`, \`([optional/list/...])\``; - for (const [category, headers] of categories) { - let tmp = `\n\n===[ ${toTitleCase(category)} ]===`; - // Ignore empty categories, including ["test"]. - let hasActualCommands = false; - - for (const header of headers) { - if (header !== "test") { - const command = commands.get(header)!; - tmp += `\n- \`${header}\`: ${command.description}`; - hasActualCommands = true; - } - } - - if (hasActualCommands) output += tmp; + for (const [category, commandList] of commands) { + output += `\n\n===[ ${category} ]===`; + for (const command of commandList) output += `\n- \`${command.name}\`: ${command.description}`; } channel.send(output, {split: true}); }, any: new Command({ async run({message, channel, guild, author, member, client, args}) { - // Setup the root command - const commands = await loadableCommands; - let header = args.shift() as string; - let command = commands.get(header); - if (!command || header === "test") return channel.send(`No command found by the name \`${header}\`.`); - if (!(command instanceof NamedCommand)) - return channel.send(`Command is not a proper instance of NamedCommand.`); - if (command.name) header = command.name; - - // Search categories - let category = "Unknown"; - for (const [referenceCategory, headers] of categories) { - if (headers.includes(header)) { - category = toTitleCase(referenceCategory); - break; - } - } - - // Gather info - const result = await command.resolveInfo(args); - - if (result.type === "error") return channel.send(result.message); - + const [result, category] = await getCommandInfo(args); + if (typeof result === "string") return channel.send(result); let append = ""; - command = result.command; - - if (result.args.length > 0) header += " " + result.args.join(" "); + const command = result.command; + const header = result.args.length > 0 ? `${result.header} ${result.args.join(" ")}` : result.header; if (command.usage === "") { const list: string[] = []; diff --git a/src/commands/utility/info.ts b/src/commands/utility/info.ts index 7006a0e..6b7ca82 100644 --- a/src/commands/utility/info.ts +++ b/src/commands/utility/info.ts @@ -102,7 +102,7 @@ export default new NamedCommand({ description: "Display info about a guild by finding its name or ID.", async run({message, channel, guild, author, member, client, args}) { // If a guild ID is provided (avoid the "number" subcommand because of inaccuracies), search for that guild - if (args.length === 1 && /^\d{17,19}$/.test(args[0])) { + if (args.length === 1 && /^\d{17,}$/.test(args[0])) { const id = args[0]; const targetGuild = client.guilds.cache.get(id); diff --git a/src/commands/utility/lsemotes.ts b/src/commands/utility/lsemotes.ts index a04f044..fef7813 100644 --- a/src/commands/utility/lsemotes.ts +++ b/src/commands/utility/lsemotes.ts @@ -16,7 +16,7 @@ export default new NamedCommand({ "Filters emotes by via a regular expression. Flags can be added by adding a dash at the end. For example, to do a case-insensitive search, do %prefix%lsemotes somepattern -i", async run({message, channel, guild, author, member, client, args}) { // If a guild ID is provided, filter all emotes by that guild (but only if there aren't any arguments afterward) - if (args.length === 1 && /^\d{17,19}$/.test(args[0])) { + if (args.length === 1 && /^\d{17,}$/.test(args[0])) { const guildID: string = args[0]; displayEmoteList( diff --git a/src/commands/utility/react.ts b/src/commands/utility/react.ts index 5bb3eb0..42e8227 100644 --- a/src/commands/utility/react.ts +++ b/src/commands/utility/react.ts @@ -17,8 +17,8 @@ export default new NamedCommand({ // handles reacts by message id/distance else if (args.length >= 2) { const last = args[args.length - 1]; // Because this is optional, do not .pop() unless you're sure it's a message link indicator. - const URLPattern = /^(?:https:\/\/discord.com\/channels\/(\d{17,19})\/(\d{17,19})\/(\d{17,19}))$/; - const copyIDPattern = /^(?:(\d{17,19})-(\d{17,19}))$/; + const URLPattern = /^(?:https:\/\/discord.com\/channels\/(\d{17,})\/(\d{17,})\/(\d{17,}))$/; + const copyIDPattern = /^(?:(\d{17,})-(\d{17,}))$/; // https://discord.com/channels/// ("Copy Message Link" Button) if (URLPattern.test(last)) { @@ -70,7 +70,7 @@ export default new NamedCommand({ args.pop(); } // - else if (/^\d{17,19}$/.test(last)) { + else if (/^\d{17,}$/.test(last)) { try { target = await channel.messages.fetch(last); } catch { diff --git a/src/core/command.ts b/src/core/command.ts index 0835da9..a89449b 100644 --- a/src/core/command.ts +++ b/src/core/command.ts @@ -30,14 +30,16 @@ import {parseVars, requireAllCasesHandledFor} from "../lib"; */ // RegEx patterns used for identifying/extracting each type from a string argument. +// The reason why \d{17,} is used is because the max safe number for JS numbers is 16 characters when stringified (decimal). Beyond that are IDs. const patterns = { - channel: /^<#(\d{17,19})>$/, - role: /^<@&(\d{17,19})>$/, - emote: /^$/, - messageLink: /^https?:\/\/(?:ptb\.|canary\.)?discord(?:app)?\.com\/channels\/(?:\d{17,19}|@me)\/(\d{17,19})\/(\d{17,19})$/, - messagePair: /^(\d{17,19})-(\d{17,19})$/, - user: /^<@!?(\d{17,19})>$/, - id: /^(\d{17,19})$/ + channel: /^<#(\d{17,})>$/, + role: /^<@&(\d{17,})>$/, + emote: /^$/, + // The message type won't include #. At that point, you may as well just use a search usernames function. Even then, tags would only be taken into account to differentiate different users with identical usernames. + messageLink: /^https?:\/\/(?:ptb\.|canary\.)?discord(?:app)?\.com\/channels\/(?:\d{17,}|@me)\/(\d{17,})\/(\d{17,})$/, + messagePair: /^(\d{17,})-(\d{17,})$/, + user: /^<@!?(\d{17,})>$/, + id: /^(\d{17,})$/ }; // Maybe add a guild redirect... somehow? @@ -106,9 +108,10 @@ interface ExecuteCommandMetadata { permission: number; nsfw: boolean; channelType: CHANNEL_TYPE; + symbolicArgs: string[]; // i.e. instead of <#...> } -interface CommandInfo { +export interface CommandInfo { readonly type: "info"; readonly command: Command; readonly subcommandInfo: Collection; @@ -117,6 +120,7 @@ interface CommandInfo { readonly nsfw: boolean; readonly channelType: CHANNEL_TYPE; readonly args: string[]; + readonly header: string; } interface CommandInfoError { @@ -131,14 +135,9 @@ interface CommandInfoMetadata { args: string[]; usage: string; readonly originalArgs: string[]; + readonly header: string; } -export const defaultMetadata = { - permission: 0, - nsfw: false, - channelType: CHANNEL_TYPE.ANY -}; - // Each Command instance represents a block that links other Command instances under it. export class Command { public readonly description: string; @@ -298,12 +297,15 @@ export class Command { // Then capture any potential errors. try { if (typeof this.run === "string") { + // Although I *could* add an option in the launcher to attach arbitrary variables to this var string... + // I'll just leave it like this, because instead of using var strings for user stuff, you could just make "run" a template string. await menu.channel.send( parseVars( this.run, { author: menu.author.toString(), - prefix: getPrefix(menu.guild) + prefix: getPrefix(menu.guild), + command: `${metadata.header} ${metadata.symbolicArgs.join(", ")}` }, "???" ) @@ -332,6 +334,7 @@ export class Command { const isMessagePair = patterns.messagePair.test(param); if (this.subcommands.has(param)) { + metadata.symbolicArgs.push(param); return this.subcommands.get(param)!.execute(args, menu, metadata); } else if (this.channel && patterns.channel.test(param)) { const id = patterns.channel.exec(param)![1]; @@ -339,6 +342,7 @@ export class Command { // 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); } else { @@ -358,6 +362,7 @@ export class Command { const role = menu.guild.roles.cache.get(id); if (role) { + metadata.symbolicArgs.push(""); menu.args.push(role); return this.role.execute(args, menu, metadata); } else { @@ -370,6 +375,7 @@ export class Command { const emote = menu.client.emojis.cache.get(id); if (emote) { + metadata.symbolicArgs.push(""); menu.args.push(emote); return this.emote.execute(args, menu, metadata); } else { @@ -395,6 +401,7 @@ export class Command { 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 { @@ -411,6 +418,7 @@ export class Command { const id = patterns.user.exec(param)![1]; try { + metadata.symbolicArgs.push(""); menu.args.push(await menu.client.users.fetch(id)); return this.user.execute(args, menu, metadata); } catch { @@ -419,6 +427,7 @@ export class Command { }; } } else if (this.id && this.idType && patterns.id.test(param)) { + metadata.symbolicArgs.push(""); const id = patterns.id.exec(param)![1]; // Probably modularize the findXByY code in general in libd. @@ -486,9 +495,11 @@ export class Command { requireAllCasesHandledFor(this.idType); } } else if (this.number && !Number.isNaN(Number(param)) && param !== "Infinity" && param !== "-Infinity") { + metadata.symbolicArgs.push(""); menu.args.push(Number(param)); return this.number.execute(args, menu, metadata); } else if (this.any) { + metadata.symbolicArgs.push(""); menu.args.push(param); return this.any.execute(args, menu, metadata); } else { @@ -502,8 +513,16 @@ export class Command { } // What this does is resolve the resulting subcommand as well as the inherited properties and the available subcommands. - public async resolveInfo(args: string[]): Promise { - return this.resolveInfoInternal(args, {...defaultMetadata, args: [], usage: "", originalArgs: [...args]}); + public async resolveInfo(args: string[], header: string): Promise { + return this.resolveInfoInternal(args, { + permission: 0, + nsfw: false, + channelType: CHANNEL_TYPE.ANY, + header, + args: [], + usage: "", + originalArgs: [...args] + }); } private async resolveInfoInternal( diff --git a/src/core/handler.ts b/src/core/handler.ts index 54f8f1b..9503b3b 100644 --- a/src/core/handler.ts +++ b/src/core/handler.ts @@ -1,6 +1,5 @@ import {Client, Permissions, Message, TextChannel, DMChannel, NewsChannel} from "discord.js"; import {loadableCommands} from "./loader"; -import {defaultMetadata} from "./command"; import {getPrefix} from "./interface"; // For custom message events that want to cancel the command handler on certain conditions. @@ -20,6 +19,13 @@ const lastCommandInfo: { channel: null }; +const defaultMetadata = { + permission: 0, + nsfw: false, + channelType: 0, // CHANNEL_TYPE.ANY, apparently isn't initialized at this point yet + symbolicArgs: [] +}; + // Note: client.user is only undefined before the bot logs in, so by this point, client.user cannot be undefined. // Note: guild.available will never need to be checked because the message starts in either a DM channel or an already-available guild. export function attachMessageHandlerToClient(client: Client) { diff --git a/src/core/index.ts b/src/core/index.ts index 0b1d56f..5d75c14 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,3 +1,4 @@ +// Onion Lasers Command Handler // export {Command, NamedCommand, CHANNEL_TYPE} from "./command"; export {addInterceptRule} from "./handler"; export {launch} from "./interface"; @@ -12,5 +13,5 @@ export { getMemberByUsername, callMemberByUsername } from "./libd"; -export {loadableCommands, categories} from "./loader"; +export {getCommandList, getCommandInfo} from "./loader"; export {hasPermission, getPermissionLevel, getPermissionName} from "./permissions"; diff --git a/src/core/libd.ts b/src/core/libd.ts index 84d6ea2..7f52c75 100644 --- a/src/core/libd.ts +++ b/src/core/libd.ts @@ -25,6 +25,11 @@ export function botHasPermission(guild: Guild | null, permission: number): boole // 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. +const FIVE_BACKWARDS_EMOJI = "⏪"; +const BACKWARDS_EMOJI = "⬅️"; +const FORWARDS_EMOJI = "➡️"; +const FIVE_FORWARDS_EMOJI = "⏩"; + /** * 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. */ @@ -43,30 +48,42 @@ export async function paginate( const turn = (amount: number) => { page += amount; - if (page < 0) page += total; - else if (page >= total) page -= total; + if (page >= total) { + page %= total; + } else if (page < 0) { + // Assuming 3 total pages, it's a bit tricker, but if we just take the modulo of the absolute value (|page| % total), we get (1 2 0 ...), and we just need the pattern (2 1 0 ...). It needs to reverse order except for when it's 0. I want to find a better solution, but for the time being... total - (|page| % total) unless (|page| % total) = 0, then return 0. + const flattened = Math.abs(page) % total; + if (flattened !== 0) page = total - flattened; + } message.edit(callback(page, true)); }; - const BACKWARDS_EMOJI = "⬅️"; - const FORWARDS_EMOJI = "➡️"; const handle = (emote: string, reacterID: string) => { if (senderID === reacterID) { switch (emote) { + case FIVE_BACKWARDS_EMOJI: + if (total > 5) turn(-5); + break; case BACKWARDS_EMOJI: turn(-1); break; case FORWARDS_EMOJI: turn(1); break; + case FIVE_FORWARDS_EMOJI: + if (total > 5) turn(5); + break; } } }; // Listen for reactions and call the handler. + let backwardsReactionFive = total > 5 ? await message.react(FIVE_BACKWARDS_EMOJI) : null; let backwardsReaction = await message.react(BACKWARDS_EMOJI); let forwardsReaction = await message.react(FORWARDS_EMOJI); + let forwardsReactionFive = total > 5 ? await message.react(FIVE_FORWARDS_EMOJI) : null; unreactEventListeners.set(message.id, handle); + const collector = message.createReactionCollector( (reaction, user) => { if (user.id === senderID) { @@ -88,8 +105,10 @@ export async function paginate( // When time's up, remove the bot's own reactions. collector.on("end", () => { unreactEventListeners.delete(message.id); + backwardsReactionFive?.users.remove(message.author); backwardsReaction.users.remove(message.author); forwardsReaction.users.remove(message.author); + forwardsReactionFive?.users.remove(message.author); }); } } diff --git a/src/core/loader.ts b/src/core/loader.ts index d5ba5c1..8b20ad7 100644 --- a/src/core/loader.ts +++ b/src/core/loader.ts @@ -1,13 +1,14 @@ import {Collection} from "discord.js"; import glob from "glob"; -import {Command, NamedCommand} from "./command"; +import {NamedCommand, CommandInfo} from "./command"; +import {toTitleCase} from "../lib"; // Internally, it'll keep its original capitalization. It's up to you to convert it to title case when you make a help command. -export const categories = new Collection(); +const categories = new Collection(); /** Returns the cache of the commands if it exists and searches the directory if not. */ export const loadableCommands = (async () => { - const commands = new Collection(); + const commands = new Collection(); // Include all .ts files recursively in "src/commands/". const files = await globP("src/commands/**/*.ts"); // Extract the usable parts from "src/commands/" if: @@ -79,3 +80,53 @@ function globP(path: string) { }); }); } + +/** + * Returns a list of categories and their associated commands. + */ +export async function getCommandList(): Promise> { + const list = new Collection(); + const commands = await loadableCommands; + + for (const [category, headers] of categories) { + const commandList: NamedCommand[] = []; + for (const header of headers.filter((header) => header !== "test")) commandList.push(commands.get(header)!); + // Ignore empty categories like "miscellaneous" (if it's empty). + if (commandList.length > 0) list.set(toTitleCase(category), commandList); + } + + return list; +} + +/** + * Resolves a command based on the arguments given. + * - Returns a string if there was an error. + * - Returns a CommandInfo/category tuple if it was a success. + */ +export async function getCommandInfo(args: string[]): Promise<[CommandInfo, string] | string> { + // Use getCommandList() instead if you're just getting the list of all commands. + if (args.length === 0) return "No arguments were provided!"; + + // Setup the root command + const commands = await loadableCommands; + let header = args.shift()!; + const command = commands.get(header); + if (!command || header === "test") return `No command found by the name \`${header}\`.`; + if (!(command instanceof NamedCommand)) return "Command is not a proper instance of NamedCommand."; + // If it's an alias, set the header to the original command name. + if (command.name) header = command.name; + + // Search categories + let category = "Unknown"; + for (const [referenceCategory, headers] of categories) { + if (headers.includes(header)) { + category = toTitleCase(referenceCategory); + break; + } + } + + // Gather info + const result = await command.resolveInfo(args, header); + if (result.type === "error") return result.message; + else return [result, category]; +} diff --git a/src/lib.ts b/src/lib.ts index 8036a96..490dd13 100644 --- a/src/lib.ts +++ b/src/lib.ts @@ -144,14 +144,13 @@ export abstract class GenericStructure { constructor(tag?: string) { this.__meta__ = tag || this.__meta__; + Object.defineProperty(this, "__meta__", { + enumerable: false + }); } public save(asynchronous = true) { - const tag = this.__meta__; - /// @ts-ignore - delete this.__meta__; - FileManager.write(tag, this, asynchronous); - this.__meta__ = tag; + FileManager.write(this.__meta__, this, asynchronous); } } diff --git a/src/modules/messageEmbed.ts b/src/modules/messageEmbed.ts index 5018ef2..7ad000c 100644 --- a/src/modules/messageEmbed.ts +++ b/src/modules/messageEmbed.ts @@ -57,7 +57,7 @@ client.on("message", async (message) => { export function extractFirstMessageLink(message: string): [string, string, string] | null { const messageLinkMatch = message.match( - /([!<])?https?:\/\/(?:ptb\.|canary\.)?discord(?:app)?\.com\/channels\/(\d{17,19})\/(\d{17,19})\/(\d{17,19})(>)?/ + /([!<])?https?:\/\/(?:ptb\.|canary\.)?discord(?:app)?\.com\/channels\/(\d{17,})\/(\d{17,})\/(\d{17,})(>)?/ ); if (messageLinkMatch === null) return null; const [, leftToken, guildID, channelID, messageID, rightToken] = messageLinkMatch; diff --git a/src/structures.ts b/src/structures.ts index 351bbb6..f9065b9 100644 --- a/src/structures.ts +++ b/src/structures.ts @@ -91,15 +91,13 @@ class StorageStructure extends GenericStructure { super("storage"); this.users = {}; this.guilds = {}; - - for (let id in data.users) if (/\d{17,19}/g.test(id)) this.users[id] = new User(data.users[id]); - - for (let id in data.guilds) if (/\d{17,19}/g.test(id)) this.guilds[id] = new Guild(data.guilds[id]); + for (let id in data.users) if (/\d{17,}/g.test(id)) this.users[id] = new User(data.users[id]); + for (let id in data.guilds) if (/\d{17,}/g.test(id)) this.guilds[id] = new Guild(data.guilds[id]); } /** Gets a user's profile if they exist and generate one if not. */ public getUser(id: string): User { - if (!/\d{17,19}/g.test(id)) + if (!/\d{17,}/g.test(id)) console.warn(`"${id}" is not a valid user ID! It will be erased when the data loads again.`); if (id in this.users) return this.users[id]; @@ -112,7 +110,7 @@ class StorageStructure extends GenericStructure { /** Gets a guild's settings if they exist and generate one if not. */ public getGuild(id: string): Guild { - if (!/\d{17,19}/g.test(id)) + if (!/\d{17,}/g.test(id)) console.warn(`"${id}" is not a valid guild ID! It will be erased when the data loads again.`); if (id in this.guilds) return this.guilds[id]; From 653cc6f8a64934a4bb5cd4ac174d6c79971e4c9a Mon Sep 17 00:00:00 2001 From: WatDuhHekBro <44940783+WatDuhHekBro@users.noreply.github.com> Date: Fri, 9 Apr 2021 23:33:22 -0500 Subject: [PATCH 03/14] Turned the help command into a paginated embed --- CHANGELOG.md | 6 ++- docs/Documentation.md | 10 +++-- package-lock.json | 4 +- package.json | 2 +- src/commands/system/help.ts | 80 ++++++++++++++++++++++++++++--------- 5 files changed, 75 insertions(+), 27 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 4142c6c..cefffe1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -1,4 +1,4 @@ -# ??? +# 3.2.1 - `vaporwave`: Transforms input into full-width text - `eco post`: A play on `eco get` - `admin set prefix (<@bot>)`: Allows you to target a bot when setting a prefix if two bots have conflicting prefixes @@ -7,8 +7,10 @@ - `thonk`: A result can now be discarded if the person who called the command reacts with ❌ - `scanemotes forcereset`: Removes the cooldown on `scanemotes`, only accessible by bot support and up - `urban`: Bug fixes +- Changed `help` to display a paginated embed +- Various changes to core -# 3.2.0 - Internal refactor, more subcommand types, and more command type guards (2021-??-??) +# 3.2.0 - Internal refactor, more subcommand types, and more command type guards (2021-04-09) - The custom logger changed: `$.log` no longer exists, it's just `console.log`. Now you don't have to do `import $ from "../core/lib"` at the top of every file that uses the custom logger. - Utility functions are no longer attached to the command menu. Stuff like `$.paginate()` and `$(5).pluralise()` instead need to be imported and used as regular functions. - The `paginate` function was reworked to reduce the amount of repetition you had to do. diff --git a/docs/Documentation.md b/docs/Documentation.md index d035587..1907bc9 100644 --- a/docs/Documentation.md +++ b/docs/Documentation.md @@ -65,6 +65,7 @@ Because versions are assigned to batches of changes rather than single changes ( - `%author%` - A user mention of the person who called the command. - `%prefix%` - The prefix of the current guild. +- `%command%` - The command's execution path up to the current subcommand. # Utility Functions @@ -72,11 +73,12 @@ Because versions are assigned to batches of changes rather than single changes ( `paginate()` ```ts -const pages = ['one', 'two', 'three']; -const msg = await channel.send(pages[0]); +const pages = ["one", "two", "three"]; -paginate(msg, author.id, pages.length, (page) => { - msg.edit(pages[page]); +paginate(channel, author.id, pages.length, (page) => { + return { + content: pages[page] + }; }); ``` diff --git a/package-lock.json b/package-lock.json index a2b61d6..344651c 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1,12 +1,12 @@ { "name": "travebot", - "version": "3.2.0", + "version": "3.2.1", "lockfileVersion": 2, "requires": true, "packages": { "": { "name": "travebot", - "version": "3.2.0", + "version": "3.2.1", "hasInstallScript": true, "license": "MIT", "dependencies": { diff --git a/package.json b/package.json index 4903443..74c35eb 100644 --- a/package.json +++ b/package.json @@ -1,6 +1,6 @@ { "name": "travebot", - "version": "3.2.0", + "version": "3.2.1", "description": "TravBot Discord bot.", "main": "dist/index.js", "scripts": { diff --git a/src/commands/system/help.ts b/src/commands/system/help.ts index 53a701c..180819d 100644 --- a/src/commands/system/help.ts +++ b/src/commands/system/help.ts @@ -1,5 +1,16 @@ -import {Command, NamedCommand, CHANNEL_TYPE, getPermissionName, getCommandList, getCommandInfo} from "../../core"; +import { + Command, + NamedCommand, + CHANNEL_TYPE, + getPermissionName, + getCommandList, + getCommandInfo, + paginate +} from "../../core"; import {requireAllCasesHandledFor} from "../../lib"; +import {MessageEmbed} from "discord.js"; + +const EMBED_COLOR = "#158a28"; export default new NamedCommand({ description: "Lists all commands. If a command is specified, their arguments are listed as well.", @@ -7,14 +18,18 @@ export default new NamedCommand({ aliases: ["h"], async run({message, channel, guild, author, member, client, args}) { const commands = await getCommandList(); - let output = `Legend: \`\`, \`[list/of/stuff]\`, \`(optional)\`, \`()\`, \`([optional/list/...])\``; + const categoryArray = commands.keyArray(); - for (const [category, commandList] of commands) { - output += `\n\n===[ ${category} ]===`; - for (const command of commandList) output += `\n- \`${command.name}\`: ${command.description}`; - } - - channel.send(output, {split: true}); + paginate(channel, author.id, categoryArray.length, (page, hasMultiplePages) => { + const category = categoryArray[page]; + const commandList = commands.get(category)!; + let output = `Legend: \`\`, \`[list/of/stuff]\`, \`(optional)\`, \`()\`, \`([optional/list/...])\`\n`; + for (const command of commandList) output += `\n❯ \`${command.name}\`: ${command.description}`; + return new MessageEmbed() + .setTitle(hasMultiplePages ? `${category} (Page ${page + 1} of ${categoryArray.length})` : category) + .setDescription(output) + .setColor(EMBED_COLOR); + }); }, any: new Command({ async run({message, channel, guild, author, member, client, args}) { @@ -29,17 +44,17 @@ export default new NamedCommand({ for (const [tag, subcommand] of result.keyedSubcommandInfo) { const customUsage = subcommand.usage ? ` ${subcommand.usage}` : ""; - list.push(`- \`${header} ${tag}${customUsage}\` - ${subcommand.description}`); + list.push(`❯ \`${header} ${tag}${customUsage}\` - ${subcommand.description}`); } for (const [type, subcommand] of result.subcommandInfo) { const customUsage = subcommand.usage ? ` ${subcommand.usage}` : ""; - list.push(`- \`${header} ${type}${customUsage}\` - ${subcommand.description}`); + list.push(`❯ \`${header} ${type}${customUsage}\` - ${subcommand.description}`); } - append = "Usages:" + (list.length > 0 ? `\n${list.join("\n")}` : " None."); + append = list.length > 0 ? list.join("\n") : "None"; } else { - append = `Usage: \`${header} ${command.usage}\``; + append = `\`${header} ${command.usage}\``; } let aliases = "N/A"; @@ -52,12 +67,41 @@ export default new NamedCommand({ } return channel.send( - `Command: \`${header}\`\nAliases: ${aliases}\nCategory: \`${category}\`\nPermission Required: \`${getPermissionName( - result.permission - )}\` (${result.permission})\nChannel Type: ${getChannelTypeName(result.channelType)}\nNSFW Only: ${ - result.nsfw ? "Yes" : "No" - }\nDescription: ${command.description}\n${append}`, - {split: true} + new MessageEmbed() + .setTitle(header) + .setDescription(command.description) + .setColor(EMBED_COLOR) + .addFields( + { + name: "Aliases", + value: aliases, + inline: true + }, + { + name: "Category", + value: category, + inline: true + }, + { + name: "Permission Required", + value: `\`${getPermissionName(result.permission)}\` (Level ${result.permission})`, + inline: true + }, + { + name: "Channel Type", + value: getChannelTypeName(result.channelType), + inline: true + }, + { + name: "NSFW Only?", + value: result.nsfw ? "Yes" : "No", + inline: true + }, + { + name: "Usages", + value: append + } + ) ); } }) From 4c3437a1775472dd83d9ca2e9897936871f37fe4 Mon Sep 17 00:00:00 2001 From: WatDuhHekBro <44940783+WatDuhHekBro@users.noreply.github.com> Date: Sat, 10 Apr 2021 02:38:46 -0500 Subject: [PATCH 04/14] Finally made the commands directory configurable --- src/core/handler.ts | 3 +-- src/core/interface.ts | 59 ++++++++++++++++++++++++++++++----------- src/core/loader.ts | 55 +++++++++++++++++++++++--------------- src/core/permissions.ts | 3 ++- src/index.ts | 17 +++++++----- 5 files changed, 91 insertions(+), 46 deletions(-) diff --git a/src/core/handler.ts b/src/core/handler.ts index 9503b3b..560ca77 100644 --- a/src/core/handler.ts +++ b/src/core/handler.ts @@ -1,6 +1,5 @@ import {Client, Permissions, Message, TextChannel, DMChannel, NewsChannel} from "discord.js"; -import {loadableCommands} from "./loader"; -import {getPrefix} from "./interface"; +import {getPrefix, loadableCommands} from "./interface"; // For custom message events that want to cancel the command handler on certain conditions. const interceptRules: ((message: Message) => boolean)[] = [(message) => message.author.bot]; diff --git a/src/core/interface.ts b/src/core/interface.ts index b04d2af..1e1df4b 100644 --- a/src/core/interface.ts +++ b/src/core/interface.ts @@ -1,23 +1,52 @@ -import {Client, User, GuildMember, Guild} from "discord.js"; +import {Collection, Client, User, GuildMember, Guild} from "discord.js"; import {attachMessageHandlerToClient} from "./handler"; import {attachEventListenersToClient} from "./eventListeners"; - -interface LaunchSettings { - permissionLevels: PermissionLevel[]; - getPrefix: (guild: Guild | null) => string; -} - -export async function launch(client: Client, settings: LaunchSettings) { - attachMessageHandlerToClient(client); - attachEventListenersToClient(client); - permissionLevels = settings.permissionLevels; - getPrefix = settings.getPrefix; -} +import {NamedCommand} from "./command"; +import {loadCommands} from "./loader"; interface PermissionLevel { name: string; check: (user: User, member: GuildMember | null) => boolean; } -export let permissionLevels: PermissionLevel[] = []; -export let getPrefix: (guild: Guild | null) => string = () => "."; +type PrefixResolver = (guild: Guild | null) => string; +type CategoryTransformer = (text: string) => string; + +// One potential option is to let the user customize system messages such as "This command must be executed in a guild." +// I decided not to do that because I don't think it'll be worth the trouble. +interface LaunchSettings { + permissionLevels?: PermissionLevel[]; + getPrefix?: PrefixResolver; + categoryTransformer?: CategoryTransformer; +} + +// One alternative to putting everything in launch(client, ...) is to create an object then set each individual aspect, such as OnionCore.setPermissions(...). +// That way, you can split different pieces of logic into different files, then do OnionCore.launch(client). +// 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) { + // Core Launch Parameters // + loadableCommands = loadCommands(commandsDirectory); + attachMessageHandlerToClient(client); + attachEventListenersToClient(client); + + // Additional Configuration // + if (settings?.permissionLevels) { + if (settings.permissionLevels.length > 0) permissionLevels = settings.permissionLevels; + else console.warn("permissionLevels must have at least one element to work!"); + } + if (settings?.getPrefix) getPrefix = settings.getPrefix; + if (settings?.categoryTransformer) categoryTransformer = settings.categoryTransformer; +} + +// Placeholder until properly loaded by the user. +export let loadableCommands = (async () => new Collection())(); +export let permissionLevels: PermissionLevel[] = [ + { + name: "User", + check: () => true + } +]; +export let getPrefix: PrefixResolver = () => "."; +export let categoryTransformer: CategoryTransformer = (text) => text; diff --git a/src/core/loader.ts b/src/core/loader.ts index 8b20ad7..da22a4a 100644 --- a/src/core/loader.ts +++ b/src/core/loader.ts @@ -1,35 +1,46 @@ import {Collection} from "discord.js"; import glob from "glob"; +import path from "path"; import {NamedCommand, CommandInfo} from "./command"; -import {toTitleCase} from "../lib"; +import {loadableCommands, categoryTransformer} from "./interface"; // Internally, it'll keep its original capitalization. It's up to you to convert it to title case when you make a help command. const categories = new Collection(); -/** Returns the cache of the commands if it exists and searches the directory if not. */ -export const loadableCommands = (async () => { +// This will go through all the .js files and import them. Because the import has to be .js (and cannot be .ts), there's no need for a custom filename checker in the launch settings. +// This will avoid the problems of being a node module by requiring absolute imports, which the user will pass in as a launch parameter. +export async function loadCommands(commandsDir: string): Promise> { + // Add a trailing separator so that the reduced filename list will reliably cut off the starting part. + // "C:/some/path/to/commands" --> "C:/some/path/to/commands/" (and likewise for \) + commandsDir = path.normalize(commandsDir); + if (!commandsDir.endsWith(path.sep)) commandsDir += path.sep; + const commands = new Collection(); // Include all .ts files recursively in "src/commands/". - const files = await globP("src/commands/**/*.ts"); - // Extract the usable parts from "src/commands/" if: + const files = await globP(path.join(commandsDir, "**", "*.js")); // This stage filters out source maps (.js.map). + // Because glob will use / regardless of platform, the following regex pattern can rely on / being the case. + const filesClean = files.map((filename) => filename.substring(commandsDir.length)); + // Extract the usable parts from commands directory if: // - The path is 1 to 2 subdirectories (a or a/b, not a/b/c) // - Any leading directory isn't "modules" - // - The filename doesn't end in .test.ts (for jest testing) - // - The filename cannot be the hardcoded top-level "template.ts", reserved for generating templates - const pattern = /src\/commands\/(?!template\.ts)(?!modules\/)(\w+(?:\/\w+)?)(?:test\.)?\.ts/; + // - The filename doesn't end in .test.js (for jest testing) + // - The filename cannot be the hardcoded top-level "template.js", reserved for generating templates + const pattern = /^(?!template\.js)(?!modules\/)(\w+(?:\/\w+)?)(?:test\.)?\.js$/; const lists: {[category: string]: string[]} = {}; - for (const path of files) { - const match = pattern.exec(path); + for (let i = 0; i < files.length; i++) { + const match = pattern.exec(filesClean[i]); + if (!match) continue; + const commandID = match[1]; // e.g. "utilities/info" + const slashIndex = commandID.indexOf("/"); + const isMiscCommand = slashIndex !== -1; + const category = isMiscCommand ? commandID.substring(0, slashIndex) : "miscellaneous"; + const commandName = isMiscCommand ? commandID.substring(slashIndex + 1) : commandID; // e.g. "info" - if (match) { - const commandID = match[1]; // e.g. "utilities/info" - const slashIndex = commandID.indexOf("/"); - const isMiscCommand = slashIndex !== -1; - const category = isMiscCommand ? commandID.substring(0, slashIndex) : "miscellaneous"; - const commandName = isMiscCommand ? commandID.substring(slashIndex + 1) : commandID; // e.g. "info" + // This try-catch block MUST be here or Node.js' dynamic require() will silently fail. + try { // If the dynamic import works, it must be an object at the very least. Then, just test to see if it's a proper instance. - const command = (await import(`../commands/${commandID}`)).default as unknown; + const command = (await import(files[i])).default as unknown; if (command instanceof NamedCommand) { command.name = commandName; @@ -55,10 +66,12 @@ export const loadableCommands = (async () => { if (!(category in lists)) lists[category] = []; lists[category].push(commandName); - console.log(`Loading Command: ${commandID}`); + console.log(`Loaded Command: ${commandID}`); } else { console.warn(`Command "${commandID}" has no default export which is a NamedCommand instance!`); } + } catch (error) { + console.log(error); } } @@ -67,7 +80,7 @@ export const loadableCommands = (async () => { } return commands; -})(); +} function globP(path: string) { return new Promise((resolve, reject) => { @@ -92,7 +105,7 @@ export async function getCommandList(): Promise header !== "test")) commandList.push(commands.get(header)!); // Ignore empty categories like "miscellaneous" (if it's empty). - if (commandList.length > 0) list.set(toTitleCase(category), commandList); + if (commandList.length > 0) list.set(categoryTransformer(category), commandList); } return list; @@ -120,7 +133,7 @@ export async function getCommandInfo(args: string[]): Promise<[CommandInfo, stri let category = "Unknown"; for (const [referenceCategory, headers] of categories) { if (headers.includes(header)) { - category = toTitleCase(referenceCategory); + category = categoryTransformer(referenceCategory); break; } } diff --git a/src/core/permissions.ts b/src/core/permissions.ts index ba3be33..f6b515e 100644 --- a/src/core/permissions.ts +++ b/src/core/permissions.ts @@ -6,6 +6,7 @@ import {permissionLevels} from "./interface"; * Checks if a `Member` has a certain permission. */ export function hasPermission(user: User, member: GuildMember | null, permission: number): boolean { + if (permissionLevels.length === 0) return true; for (let i = permissionLevels.length - 1; i >= permission; i--) if (permissionLevels[i].check(user, member)) return true; return false; @@ -20,6 +21,6 @@ export function getPermissionLevel(user: User, member: GuildMember | null): numb } export function getPermissionName(level: number) { - if (level > permissionLevels.length || level < 0) return "N/A"; + if (level > permissionLevels.length || level < 0 || permissionLevels.length === 0) return "N/A"; else return permissionLevels[level].name; } diff --git a/src/index.ts b/src/index.ts index 6c917d7..95befe7 100644 --- a/src/index.ts +++ b/src/index.ts @@ -1,21 +1,25 @@ -// Bootstrapping Section // import "./modules/globals"; import {Client, Permissions} from "discord.js"; -import {launch} from "./core"; -import setup from "./modules/setup"; -import {Config, getPrefix} from "./structures"; +import path from "path"; // This is here in order to make it much less of a headache to access the client from other files. // This of course won't actually do anything until the setup process is complete and it logs in. export const client = new Client(); +import {launch} from "./core"; +import setup from "./modules/setup"; +import {Config, getPrefix} from "./structures"; +import {toTitleCase} from "./lib"; + // Send the login request to Discord's API and then load modules while waiting for it. setup.init().then(() => { client.login(Config.token).catch(setup.again); }); // Setup the command handler. -launch(client, { +launch(client, path.join(__dirname, "commands"), { + getPrefix, + categoryTransformer: toTitleCase, permissionLevels: [ { // NONE // @@ -57,8 +61,7 @@ launch(client, { name: "Bot Owner", check: (user) => Config.owner === user.id } - ], - getPrefix: getPrefix + ] }); // Initialize Modules // From 54ce28d8d448f323973fe64d6216da323f60aae1 Mon Sep 17 00:00:00 2001 From: WatDuhHekBro <44940783+WatDuhHekBro@users.noreply.github.com> Date: Sat, 10 Apr 2021 06:41:48 -0500 Subject: [PATCH 05/14] Added more library functions to command handler --- src/commands/fun/eco.ts | 12 ++- src/commands/fun/whois.ts | 14 ++- src/commands/utility/info.ts | 8 +- src/commands/utility/time.ts | 12 +-- src/core/command.ts | 101 ++++++++++---------- src/core/index.ts | 12 +-- src/core/interface.ts | 9 +- src/core/libd.ts | 179 +++++++++++++++++++++++++++-------- 8 files changed, 217 insertions(+), 130 deletions(-) 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}\`!`}; + } + } +} From e8def0aec3faf4375d0e1ade4b7d55acf722b656 Mon Sep 17 00:00:00 2001 From: WatDuhHekBro <44940783+WatDuhHekBro@users.noreply.github.com> Date: Sat, 10 Apr 2021 07:51:32 -0500 Subject: [PATCH 06/14] Added guild subcommand type and various changes --- CHANGELOG.md | 1 + docs/DesignDecisions.md | 4 ++ src/commands/fun/modules/eco-shop.ts | 2 +- src/commands/system/help.ts | 2 +- src/commands/utility/info.ts | 37 ++++++-------- src/commands/utility/lsemotes.ts | 3 +- src/commands/utility/scanemotes.ts | 8 +-- src/core/command.ts | 26 ++++++++-- src/core/handler.ts | 12 +++-- src/core/libd.ts | 20 ++++++-- src/core/loader.ts | 2 +- src/defs/info.ts | 8 +-- src/modules/messageEmbed.ts | 75 ++++++++++++---------------- src/modules/streamNotifications.ts | 1 + src/structures.ts | 1 + 15 files changed, 113 insertions(+), 89 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index cefffe1..3f52e17 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,6 +9,7 @@ - `urban`: Bug fixes - Changed `help` to display a paginated embed - Various changes to core + - Added `guild` subcommand type (only accessible when `id` is set to `guild`) # 3.2.0 - Internal refactor, more subcommand types, and more command type guards (2021-04-09) - The custom logger changed: `$.log` no longer exists, it's just `console.log`. Now you don't have to do `import $ from "../core/lib"` at the top of every file that uses the custom logger. diff --git a/docs/DesignDecisions.md b/docs/DesignDecisions.md index 4a2bbd5..584396c 100644 --- a/docs/DesignDecisions.md +++ b/docs/DesignDecisions.md @@ -71,6 +71,10 @@ Boolean subcommand types won't be implemented: For common use cases, there wouldn't be a need to go accept numbers of different bases. The only time it would be applicable is if there was some sort of base converter command, and even then, it'd be better to just implement custom logic. +## User Mention + Search by Username Type + +While it's a pretty common pattern, it's probably a bit too specific for the `Command` class itself. Instead, this pattern will be comprised of two subcommands: A `user` type and an `any` type. + # The Command Handler ## The Scope of the Command Handler diff --git a/src/commands/fun/modules/eco-shop.ts b/src/commands/fun/modules/eco-shop.ts index 47d0178..ca161f1 100644 --- a/src/commands/fun/modules/eco-shop.ts +++ b/src/commands/fun/modules/eco-shop.ts @@ -34,7 +34,7 @@ export const ShopCommand = new NamedCommand({ const shopPages = split(ShopItems, 5); const pageAmount = shopPages.length; - paginate(channel, author.id, pageAmount, (page, hasMultiplePages) => { + paginate(channel.send, author.id, pageAmount, (page, hasMultiplePages) => { return getShopEmbed( shopPages[page], hasMultiplePages ? `Shop (Page ${page + 1} of ${pageAmount})` : "Shop" diff --git a/src/commands/system/help.ts b/src/commands/system/help.ts index 180819d..c6ec382 100644 --- a/src/commands/system/help.ts +++ b/src/commands/system/help.ts @@ -20,7 +20,7 @@ export default new NamedCommand({ const commands = await getCommandList(); const categoryArray = commands.keyArray(); - paginate(channel, author.id, categoryArray.length, (page, hasMultiplePages) => { + paginate(channel.send, author.id, categoryArray.length, (page, hasMultiplePages) => { const category = categoryArray[page]; const commandList = commands.get(category)!; let output = `Legend: \`\`, \`[list/of/stuff]\`, \`(optional)\`, \`()\`, \`([optional/list/...])\`\n`; diff --git a/src/commands/utility/info.ts b/src/commands/utility/info.ts index a47e549..f3d5bb3 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, getMemberByName, CHANNEL_TYPE} from "../../core"; +import {Command, NamedCommand, getMemberByName, CHANNEL_TYPE, getGuildByName} from "../../core"; import {formatBytes, trimArray} from "../../lib"; import {verificationLevels, filterLevels, regions} from "../../defs/info"; import moment, {utc} from "moment"; @@ -98,30 +98,23 @@ export default new NamedCommand({ async run({message, channel, guild, author, member, client, args}) { channel.send(await getGuildInfo(guild!, guild)); }, - any: new Command({ - description: "Display info about a guild by finding its name or ID.", + id: "guild", + guild: new Command({ + description: "Display info about a guild by its ID.", async run({message, channel, guild, author, member, client, args}) { - // If a guild ID is provided (avoid the "number" subcommand because of inaccuracies), search for that guild - if (args.length === 1 && /^\d{17,}$/.test(args[0])) { - const id = args[0]; - const targetGuild = client.guilds.cache.get(id); + const targetGuild = args[0] as Guild; + channel.send(await getGuildInfo(targetGuild, guild)); + } + }), + any: new Command({ + description: "Display info about a guild by finding its name.", + async run({message, channel, guild, author, member, client, args}) { + const targetGuild = getGuildByName(args.join(" ")); - if (targetGuild) { - channel.send(await getGuildInfo(targetGuild, guild)); - } else { - channel.send(`None of the servers I'm in matches the guild ID \`${id}\`!`); - } + if (targetGuild instanceof Guild) { + channel.send(await getGuildInfo(targetGuild, guild)); } else { - const query: string = args.join(" ").toLowerCase(); - const targetGuild = client.guilds.cache.find((guild) => - guild.name.toLowerCase().includes(query) - ); - - if (targetGuild) { - channel.send(await getGuildInfo(targetGuild, guild)); - } else { - channel.send(`None of the servers I'm in matches the query \`${query}\`!`); - } + channel.send(targetGuild); } } }) diff --git a/src/commands/utility/lsemotes.ts b/src/commands/utility/lsemotes.ts index fef7813..0d858bc 100644 --- a/src/commands/utility/lsemotes.ts +++ b/src/commands/utility/lsemotes.ts @@ -35,7 +35,6 @@ export default new NamedCommand({ let emoteCollection = client.emojis.cache.array(); // Creates a sandbox to stop a regular expression if it takes too much time to search. // To avoid passing in a giant data structure, I'll just pass in the structure {[id: string]: [name: string]}. - //let emotes: {[id: string]: string} = {}; let emotes = new Map(); for (const emote of emoteCollection) { @@ -91,7 +90,7 @@ async function displayEmoteList(emotes: GuildEmoji[], channel: TextChannel | DMC // Gather the first page (if it even exists, which it might not if there no valid emotes appear) if (pages > 0) { - paginate(channel, author.id, pages, (page, hasMultiplePages) => { + paginate(channel.send, author.id, pages, (page, hasMultiplePages) => { embed.setTitle(hasMultiplePages ? `**Emotes** (Page ${page + 1} of ${pages})` : "**Emotes**"); let desc = ""; diff --git a/src/commands/utility/scanemotes.ts b/src/commands/utility/scanemotes.ts index 3662c57..a432147 100644 --- a/src/commands/utility/scanemotes.ts +++ b/src/commands/utility/scanemotes.ts @@ -3,7 +3,7 @@ import {pluralise} from "../../lib"; import moment from "moment"; import {Collection, TextChannel} from "discord.js"; -const lastUsedTimestamps: {[id: string]: number} = {}; +const lastUsedTimestamps = new Collection(); export default new NamedCommand({ description: @@ -13,7 +13,7 @@ export default new NamedCommand({ // Test if the command is on cooldown. This isn't the strictest cooldown possible, because in the event that the bot crashes, the cooldown will be reset. But for all intends and purposes, it's a good enough cooldown. It's a per-server cooldown. const startTime = Date.now(); const cooldown = 86400000; // 24 hours - const lastUsedTimestamp = lastUsedTimestamps[guild!.id] ?? 0; + const lastUsedTimestamp = lastUsedTimestamps.get(guild!.id) ?? 0; const difference = startTime - lastUsedTimestamp; const howLong = moment(startTime).to(lastUsedTimestamp + cooldown); @@ -22,7 +22,7 @@ export default new NamedCommand({ return channel.send( `This command requires a day to cooldown. You'll be able to activate this command ${howLong}.` ); - else lastUsedTimestamps[guild!.id] = startTime; + else lastUsedTimestamps.set(guild!.id, startTime); const stats: { [id: string]: { @@ -188,7 +188,7 @@ export default new NamedCommand({ description: "Forces the cooldown timer to reset.", permission: PERMISSIONS.BOT_SUPPORT, async run({message, channel, guild, author, member, client, args}) { - lastUsedTimestamps[guild!.id] = 0; + lastUsedTimestamps.set(guild!.id, 0); channel.send("Reset the cooldown on `scanemotes`."); } }) diff --git a/src/core/command.ts b/src/core/command.ts index 0e7e5e4..d38d78a 100644 --- a/src/core/command.ts +++ b/src/core/command.ts @@ -11,7 +11,7 @@ import { GuildChannel, Channel } from "discord.js"; -import {getChannelByID, getMessageByID, getUserByID, SingleMessageOptions} from "./libd"; +import {getChannelByID, getGuildByID, getMessageByID, getUserByID, SingleMessageOptions, SendFunction} from "./libd"; import {hasPermission, getPermissionLevel, getPermissionName} from "./permissions"; import {getPrefix} from "./interface"; import {parseVars, requireAllCasesHandledFor} from "../lib"; @@ -44,7 +44,7 @@ const patterns = { }; // Maybe add a guild redirect... somehow? -type ID = "channel" | "role" | "emote" | "message" | "user"; +type ID = "channel" | "role" | "emote" | "message" | "user" | "guild"; // Callbacks don't work with discriminated unions: // - https://github.com/microsoft/TypeScript/issues/41759 @@ -68,6 +68,7 @@ interface CommandMenu { // According to the documentation, a message can be part of a guild while also not having a // member object for the author. This will happen if the author of a message left the guild. readonly member: GuildMember | null; + readonly send: SendFunction; } interface CommandOptionsBase { @@ -95,6 +96,7 @@ interface CommandOptionsNonEndpoint { readonly emote?: Command; readonly message?: Command; readonly user?: Command; + readonly guild?: Command; // Only available if an ID is set to reroute to it. readonly id?: ID; readonly number?: Command; readonly any?: Command; @@ -156,6 +158,7 @@ export class Command { private emote: Command | null; private message: Command | null; private user: Command | null; + private guild: Command | null; private id: Command | null; private idType: ID | null; private number: Command | null; @@ -175,6 +178,7 @@ export class Command { this.emote = null; this.message = null; this.user = null; + this.guild = null; this.id = null; this.idType = null; this.number = null; @@ -186,6 +190,7 @@ export class Command { if (options?.emote) this.emote = options.emote; if (options?.message) this.message = options.message; if (options?.user) this.user = options.user; + if (options?.guild) this.guild = options.guild; if (options?.number) this.number = options.number; if (options?.any) this.any = options.any; if (options?.id) this.idType = options.id; @@ -207,6 +212,9 @@ export class Command { case "user": this.id = this.user; break; + case "guild": + this.id = this.guild; + break; default: requireAllCasesHandledFor(options.id); } @@ -246,6 +254,9 @@ export class Command { // // Calls the resulting subcommand's execute method in order to make more modular code, basically pushing the chain of execution to the subcommand. // For example, a numeric subcommand would accept args of [4] then execute on it. + // + // Because each Command instance is isolated from others, it becomes practically impossible to predict the total amount of subcommands when isolating the code to handle each individual layer of recursion. + // Therefore, if a Command is declared as a rest type, any typed args that come at the end must be handled manually. public async execute( args: string[], menu: CommandMenu, @@ -300,7 +311,7 @@ export class Command { if (typeof this.run === "string") { // Although I *could* add an option in the launcher to attach arbitrary variables to this var string... // I'll just leave it like this, because instead of using var strings for user stuff, you could just make "run" a template string. - await menu.channel.send( + await menu.send( parseVars( this.run, { @@ -490,6 +501,15 @@ export class Command { } else { return user; } + case "guild": + const guild = getGuildByID(id); + + if (guild instanceof Guild) { + menu.args.push(guild); + return this.id.execute(args, menu, metadata); + } else { + return guild; + } default: requireAllCasesHandledFor(this.idType); } diff --git a/src/core/handler.ts b/src/core/handler.ts index 560ca77..52fdc80 100644 --- a/src/core/handler.ts +++ b/src/core/handler.ts @@ -37,6 +37,7 @@ export function attachMessageHandlerToClient(client: Client) { const commands = await loadableCommands; const {author, channel, content, guild, member} = message; + const send = channel.send.bind(channel); const text = content; const menu = { author, @@ -45,7 +46,8 @@ export function attachMessageHandlerToClient(client: Client) { guild, member, message, - args: [] + args: [], + send }; // Execute a dedicated block for messages in DM channels. @@ -70,10 +72,10 @@ export function attachMessageHandlerToClient(client: Client) { // If something went wrong, let the user know (like if they don't have permission to use a command). if (result) { - channel.send(result); + send(result); } } else { - channel.send( + send( `I couldn't find the command or alias that starts with \`${header}\`. To see the list of commands, type \`help\`` ); } @@ -84,7 +86,7 @@ export function attachMessageHandlerToClient(client: Client) { // First, test if the message is just a ping to the bot. if (new RegExp(`^<@!?${client.user!.id}>$`).test(text)) { - channel.send(`${author}, my prefix on this server is \`${prefix}\`.`); + send(`${author}, my prefix on this server is \`${prefix}\`.`); } // Then check if it's a normal command. else if (text.startsWith(prefix)) { @@ -107,7 +109,7 @@ export function attachMessageHandlerToClient(client: Client) { // If something went wrong, let the user know (like if they don't have permission to use a command). if (result) { - channel.send(result); + send(result); } } } diff --git a/src/core/libd.ts b/src/core/libd.ts index 45bf919..c9d5bcf 100644 --- a/src/core/libd.ts +++ b/src/core/libd.ts @@ -10,13 +10,27 @@ import { MessageOptions, Channel, GuildChannel, - User + User, + APIMessageContentResolvable, + MessageAdditions, + SplitOptions, + APIMessage, + StringResolvable } from "discord.js"; import {unreactEventListeners, replyEventListeners} from "./eventListeners"; import {client} from "./interface"; export type SingleMessageOptions = MessageOptions & {split?: false}; +export type SendFunction = (( + content: APIMessageContentResolvable | (MessageOptions & {split?: false}) | MessageAdditions +) => Promise) & + ((options: MessageOptions & {split: true | SplitOptions}) => Promise) & + ((options: MessageOptions | APIMessage) => Promise) & + ((content: StringResolvable, options: (MessageOptions & {split?: false}) | MessageAdditions) => Promise) & + ((content: StringResolvable, options: MessageOptions & {split: true | SplitOptions}) => Promise) & + ((content: StringResolvable, options: MessageOptions) => Promise); + /** * Tests if a bot has a certain permission in a specified guild. */ @@ -41,14 +55,14 @@ const FIVE_FORWARDS_EMOJI = "⏩"; * 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. */ export async function paginate( - channel: TextChannel | DMChannel | NewsChannel, + send: SendFunction, senderID: string, total: number, callback: (page: number, hasMultiplePages: boolean) => SingleMessageOptions, duration = 60000 ) { const hasMultiplePages = total > 1; - const message = await channel.send(callback(0, hasMultiplePages)); + const message = await send(callback(0, hasMultiplePages)); if (hasMultiplePages) { let page = 0; diff --git a/src/core/loader.ts b/src/core/loader.ts index da22a4a..cabb0fd 100644 --- a/src/core/loader.ts +++ b/src/core/loader.ts @@ -71,7 +71,7 @@ export async function loadCommands(commandsDir: string): Promise { // Only execute if the message is from a user and isn't a command. @@ -10,49 +10,38 @@ client.on("message", async (message) => { if (!messageLink) return; const [guildID, channelID, messageID] = messageLink; - try { - const channel = client.guilds.cache.get(guildID)?.channels.cache.get(channelID) as TextChannel; - const link_message = await channel.messages.fetch(messageID); + const linkMessage = await getMessageByID(channelID, messageID); - let rtmsg: string | APIMessage = ""; - if (link_message.cleanContent) { - rtmsg = new APIMessage(message.channel as TextChannel, { - content: link_message.cleanContent, - disableMentions: "all", - files: link_message.attachments.array() - }); - } - - const embeds = [...link_message.embeds.filter((v) => v.type == "rich"), ...link_message.attachments.values()]; - - /// @ts-ignore - if (!link_message.cleanContent && embeds.empty) { - const Embed = new MessageEmbed().setDescription("🚫 The message is empty."); - return message.channel.send(Embed); - } - - const infoEmbed = new MessageEmbed() - .setAuthor( - link_message.author.username, - link_message.author.displayAvatarURL({format: "png", dynamic: true, size: 4096}) - ) - .setTimestamp(link_message.createdTimestamp) - .setDescription( - `${link_message.cleanContent}\n\nSent in **${link_message.guild?.name}** | <#${link_message.channel.id}> ([link](https://discord.com/channels/${guildID}/${channelID}/${messageID}))` - ); - if (link_message.attachments.size !== 0) { - const image = link_message.attachments.first(); - /// @ts-ignore - infoEmbed.setImage(image.url); - } - - await message.channel.send(infoEmbed); - } catch (error) { - if (error instanceof DiscordAPIError) { - message.channel.send("I don't have access to this channel, or something else went wrong."); - } - return console.error(error); + // If it's an invalid link (or the bot doesn't have access to it). + if (!(linkMessage instanceof Message)) { + return message.channel.send("I don't have access to that channel!"); } + + const embeds = [ + ...linkMessage.embeds.filter((embed) => embed.type === "rich"), + ...linkMessage.attachments.values() + ]; + + if (!linkMessage.cleanContent && embeds.length === 0) { + return message.channel.send(new MessageEmbed().setDescription("🚫 The message is empty.")); + } + + const infoEmbed = new MessageEmbed() + .setAuthor( + linkMessage.author.username, + linkMessage.author.displayAvatarURL({format: "png", dynamic: true, size: 4096}) + ) + .setTimestamp(linkMessage.createdTimestamp) + .setDescription( + `${linkMessage.cleanContent}\n\nSent in **${linkMessage.guild?.name}** | <#${linkMessage.channel.id}> ([link](https://discord.com/channels/${guildID}/${channelID}/${messageID}))` + ); + + if (linkMessage.attachments.size !== 0) { + const image = linkMessage.attachments.first(); + infoEmbed.setImage(image!.url); + } + + return await message.channel.send(infoEmbed); }); export function extractFirstMessageLink(message: string): [string, string, string] | null { diff --git a/src/modules/streamNotifications.ts b/src/modules/streamNotifications.ts index f6e2843..b6e9fef 100644 --- a/src/modules/streamNotifications.ts +++ b/src/modules/streamNotifications.ts @@ -47,6 +47,7 @@ client.on("voiceStateUpdate", async (before, after) => { const voiceChannel = after.channel!; const textChannel = client.channels.cache.get(streamingChannel); + // Although checking the bot's permission to send might seem like a good idea, having the error be thrown will cause it to show up in the last channel rather than just show up in the console. if (textChannel instanceof TextChannel) { if (isStartStreamEvent) { streamList.set(member.id, { diff --git a/src/structures.ts b/src/structures.ts index f9065b9..8991e3f 100644 --- a/src/structures.ts +++ b/src/structures.ts @@ -5,6 +5,7 @@ import {watch} from "fs"; import {Guild as DiscordGuild, Snowflake} from "discord.js"; // Maybe use getters and setters to auto-save on set? +// And maybe use Collections/Maps instead of objects? class ConfigStructure extends GenericStructure { public token: string; From e1e6910b1de3815e0ad12ba3be20df69e31d3ac7 Mon Sep 17 00:00:00 2001 From: WatDuhHekBro <44940783+WatDuhHekBro@users.noreply.github.com> Date: Sat, 10 Apr 2021 08:34:55 -0500 Subject: [PATCH 07/14] Reduced channel.send() to send() --- CHANGELOG.md | 1 + src/commands/fun/8ball.ts | 4 +- src/commands/fun/cookie.ts | 10 +-- src/commands/fun/eco.ts | 14 +-- src/commands/fun/figlet.ts | 6 +- src/commands/fun/insult.ts | 4 +- src/commands/fun/love.ts | 4 +- src/commands/fun/modules/eco-bet.ts | 56 ++++++------ src/commands/fun/modules/eco-core.ts | 49 +++++------ src/commands/fun/modules/eco-extras.ts | 25 +++--- src/commands/fun/modules/eco-shop.ts | 10 +-- src/commands/fun/neko.ts | 12 ++- src/commands/fun/ok.ts | 4 +- src/commands/fun/owoify.ts | 4 +- src/commands/fun/party.ts | 4 +- src/commands/fun/poll.ts | 4 +- src/commands/fun/ravi.ts | 10 +-- src/commands/fun/thonk.ts | 4 +- src/commands/fun/urban.ts | 6 +- src/commands/fun/vaporwave.ts | 6 +- src/commands/fun/weather.ts | 8 +- src/commands/fun/whois.ts | 22 +++-- src/commands/system/admin.ts | 116 ++++++++++++------------- src/commands/system/help.ts | 10 +-- src/commands/template.ts | 2 +- src/commands/utility/calc.ts | 8 +- src/commands/utility/desc.ts | 10 +-- src/commands/utility/emote.ts | 4 +- src/commands/utility/info.ts | 40 ++++----- src/commands/utility/invite.ts | 4 +- src/commands/utility/lsemotes.ts | 24 ++--- src/commands/utility/react.ts | 16 ++-- src/commands/utility/say.ts | 4 +- src/commands/utility/scanemotes.ts | 14 ++- src/commands/utility/shorten.ts | 4 +- src/commands/utility/streaminfo.ts | 6 +- src/commands/utility/time.ts | 34 ++++---- src/commands/utility/todo.ts | 18 ++-- src/commands/utility/translate.ts | 8 +- 39 files changed, 286 insertions(+), 303 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 3f52e17..2295ec1 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -10,6 +10,7 @@ - Changed `help` to display a paginated embed - Various changes to core - Added `guild` subcommand type (only accessible when `id` is set to `guild`) + - Further reduced `channel.send()` to `send()` because it's used in *every, single, command* # 3.2.0 - Internal refactor, more subcommand types, and more command type guards (2021-04-09) - The custom logger changed: `$.log` no longer exists, it's just `console.log`. Now you don't have to do `import $ from "../core/lib"` at the top of every file that uses the custom logger. diff --git a/src/commands/fun/8ball.ts b/src/commands/fun/8ball.ts index 2c45770..afc1f23 100644 --- a/src/commands/fun/8ball.ts +++ b/src/commands/fun/8ball.ts @@ -31,9 +31,9 @@ export default new NamedCommand({ run: "Please provide a question.", any: new Command({ description: "Question to ask the 8-ball.", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const sender = message.author; - channel.send(`${random(responses)} <@${sender.id}>`); + send(`${random(responses)} <@${sender.id}>`); } }) }); diff --git a/src/commands/fun/cookie.ts b/src/commands/fun/cookie.ts index 69a624a..2275c08 100644 --- a/src/commands/fun/cookie.ts +++ b/src/commands/fun/cookie.ts @@ -31,21 +31,21 @@ export default new NamedCommand({ run: ":cookie: Here's a cookie!", subcommands: { all: new NamedCommand({ - async run({message, channel, guild, author, member, client, args}) { - channel.send(`${author} gave everybody a cookie!`); + async run({send, message, channel, guild, author, member, client, args}) { + send(`${author} gave everybody a cookie!`); } }) }, id: "user", user: new Command({ description: "User to give cookie to.", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const sender = author; const mention: User = args[0]; - if (mention.id == sender.id) return channel.send("You can't give yourself cookies!"); + if (mention.id == sender.id) return send("You can't give yourself cookies!"); - return channel.send( + return send( `:cookie: <@${sender.id}> ${parseVars(random(cookies), { target: mention.toString() })}` diff --git a/src/commands/fun/eco.ts b/src/commands/fun/eco.ts index b265ebb..1572151 100644 --- a/src/commands/fun/eco.ts +++ b/src/commands/fun/eco.ts @@ -8,8 +8,8 @@ import {GuildMember} from "discord.js"; export default new NamedCommand({ description: "Economy command for Monika.", - async run({guild, channel, author}) { - if (isAuthorized(guild, channel)) channel.send(getMoneyEmbed(author)); + async run({send, guild, channel, author}) { + if (isAuthorized(guild, channel)) send(getMoneyEmbed(author)); }, subcommands: { daily: DailyCommand, @@ -29,17 +29,17 @@ export default new NamedCommand({ id: "user", user: new Command({ description: "See how much money someone else has by using their user ID or pinging them.", - async run({guild, channel, args}) { - if (isAuthorized(guild, channel)) channel.send(getMoneyEmbed(args[0])); + async run({send, guild, channel, args}) { + if (isAuthorized(guild, channel)) send(getMoneyEmbed(args[0])); } }), any: new Command({ description: "See how much money someone else has by using their username.", - async run({guild, channel, args, message}) { + async run({send, guild, channel, args, message}) { if (isAuthorized(guild, channel)) { const member = await getMemberByName(guild!, args.join(" ")); - if (member instanceof GuildMember) channel.send(getMoneyEmbed(member.user)); - else channel.send(member); + if (member instanceof GuildMember) send(getMoneyEmbed(member.user)); + else send(member); } } }) diff --git a/src/commands/fun/figlet.ts b/src/commands/fun/figlet.ts index 439ce00..0ae2538 100644 --- a/src/commands/fun/figlet.ts +++ b/src/commands/fun/figlet.ts @@ -3,10 +3,10 @@ import figlet from "figlet"; export default new NamedCommand({ description: "Generates a figlet of your input.", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const input = args.join(" "); - if (!args[0]) return channel.send("You have to provide input for me to create a figlet!"); - return channel.send( + if (!args[0]) return send("You have to provide input for me to create a figlet!"); + return send( "```" + figlet.textSync(`${input}`, { horizontalLayout: "full" diff --git a/src/commands/fun/insult.ts b/src/commands/fun/insult.ts index 63847fd..3cd982d 100644 --- a/src/commands/fun/insult.ts +++ b/src/commands/fun/insult.ts @@ -2,10 +2,10 @@ import {Command, NamedCommand} from "../../core"; export default new NamedCommand({ description: "Insult TravBot! >:D", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { channel.startTyping(); setTimeout(() => { - channel.send( + send( `${author} What the fuck did you just fucking say about me, you little bitch? I'll have you know I graduated top of my class in the Navy Seals, and I've been involved in numerous secret raids on Al-Quaeda, and I have over 300 confirmed kills. I am trained in gorilla warfare and I'm the top sniper in the entire US armed forces. You are nothing to me but just another target. I will wipe you the fuck out with precision the likes of which has never been seen before on this Earth, mark my fucking words. You think you can get away with saying that shit to me over the Internet? Think again, fucker. As we speak I am contacting my secret network of spies across the USA and your IP is being traced right now so you better prepare for the storm, maggot. The storm that wipes out the pathetic little thing you call your life. You're fucking dead, kid. I can be anywhere, anytime, and I can kill you in over seven hundred ways, and that's just with my bare hands. Not only am I extensively trained in unarmed combat, but I have access to the entire arsenal of the United States Marine Corps and I will use it to its full extent to wipe your miserable ass off the face of the continent, you little shit. If only you could have known what unholy retribution your little "clever" comment was about to bring down upon you, maybe you would have held your fucking tongue. But you couldn't, you didn't, and now you're paying the price, you goddamn idiot. I will shit fury all over you and you will drown in it. You're fucking dead, kiddo.` ); channel.stopTyping(); diff --git a/src/commands/fun/love.ts b/src/commands/fun/love.ts index bf42d0d..e7d2557 100644 --- a/src/commands/fun/love.ts +++ b/src/commands/fun/love.ts @@ -3,8 +3,8 @@ import {Command, NamedCommand, CHANNEL_TYPE} from "../../core"; export default new NamedCommand({ description: "Chooses someone to love.", channelType: CHANNEL_TYPE.GUILD, - async run({message, channel, guild, author, client, args}) { + async run({send, message, channel, guild, author, client, args}) { const member = guild!.members.cache.random(); - channel.send(`I love ${member.nickname ?? member.user.username}!`); + send(`I love ${member.nickname ?? member.user.username}!`); } }); diff --git a/src/commands/fun/modules/eco-bet.ts b/src/commands/fun/modules/eco-bet.ts index f3ea452..9095124 100644 --- a/src/commands/fun/modules/eco-bet.ts +++ b/src/commands/fun/modules/eco-bet.ts @@ -11,21 +11,21 @@ export const BetCommand = new NamedCommand({ user: new Command({ description: "User to bet with.", // handles missing amount argument - async run({args, author, channel, guild}) { + async run({send, args, author, channel, guild}) { if (isAuthorized(guild, channel)) { const target = args[0]; // handle invalid target - if (target.id == author.id) return channel.send("You can't bet Mons with yourself!"); - else if (target.bot && process.argv[2] !== "dev") return channel.send("You can't bet Mons with a bot!"); + if (target.id == author.id) return send("You can't bet Mons with yourself!"); + else if (target.bot && process.argv[2] !== "dev") return send("You can't bet Mons with a bot!"); - return channel.send("How much are you betting?"); + return send("How much are you betting?"); } else return; }, number: new Command({ description: "Amount of Mons to bet.", // handles missing duration argument - async run({args, author, channel, guild}) { + async run({send, args, author, channel, guild}) { if (isAuthorized(guild, channel)) { const sender = Storage.getUser(author.id); const target = args[0] as User; @@ -33,23 +33,22 @@ export const BetCommand = new NamedCommand({ const amount = Math.floor(args[1]); // handle invalid target - if (target.id == author.id) return channel.send("You can't bet Mons with yourself!"); - else if (target.bot && process.argv[2] !== "dev") - return channel.send("You can't bet Mons with a bot!"); + if (target.id == author.id) return send("You can't bet Mons with yourself!"); + else if (target.bot && process.argv[2] !== "dev") return send("You can't bet Mons with a bot!"); // handle invalid amount - if (amount <= 0) return channel.send("You must bet at least one Mon!"); + if (amount <= 0) return send("You must bet at least one Mon!"); else if (sender.money < amount) - return channel.send("You don't have enough Mons for that.", getMoneyEmbed(author)); + return send("You don't have enough Mons for that.", getMoneyEmbed(author)); else if (receiver.money < amount) - return channel.send("They don't have enough Mons for that.", getMoneyEmbed(target)); + return send("They don't have enough Mons for that.", getMoneyEmbed(target)); - return channel.send("How long until the bet ends?"); + return send("How long until the bet ends?"); } else return; }, any: new Command({ description: "Duration of the bet.", - async run({client, args, author, message, channel, guild}) { + async run({send, client, args, author, message, channel, guild}) { if (isAuthorized(guild, channel)) { // [Pertinence to make configurable on the fly.] // Lower and upper bounds for bet @@ -62,27 +61,26 @@ export const BetCommand = new NamedCommand({ const duration = parseDuration(args[2].trim()); // handle invalid target - if (target.id == author.id) return channel.send("You can't bet Mons with yourself!"); - else if (target.bot && process.argv[2] !== "dev") - return channel.send("You can't bet Mons with a bot!"); + if (target.id == author.id) return send("You can't bet Mons with yourself!"); + else if (target.bot && process.argv[2] !== "dev") return send("You can't bet Mons with a bot!"); // handle invalid amount - if (amount <= 0) return channel.send("You must bet at least one Mon!"); + if (amount <= 0) return send("You must bet at least one Mon!"); else if (sender.money < amount) - return channel.send("You don't have enough Mons for that.", getMoneyEmbed(author)); + return send("You don't have enough Mons for that.", getMoneyEmbed(author)); else if (receiver.money < amount) - return channel.send("They don't have enough Mons for that.", getMoneyEmbed(target)); + return send("They don't have enough Mons for that.", getMoneyEmbed(target)); // handle invalid duration - if (duration <= 0) return channel.send("Invalid bet duration"); + if (duration <= 0) return send("Invalid bet duration"); else if (duration <= parseDuration(durationBounds.min)) - return channel.send(`Bet duration is too short, maximum duration is ${durationBounds.min}`); + return send(`Bet duration is too short, maximum duration is ${durationBounds.min}`); else if (duration >= parseDuration(durationBounds.max)) - return channel.send(`Bet duration is too long, maximum duration is ${durationBounds.max}`); + return send(`Bet duration is too long, maximum duration is ${durationBounds.max}`); // Ask target whether or not they want to take the bet. const takeBet = await askYesOrNo( - await channel.send( + await send( `<@${target.id}>, do you want to take this bet of ${pluralise(amount, "Mon", "s")}` ), target.id @@ -99,7 +97,7 @@ export const BetCommand = new NamedCommand({ Storage.save(); // Notify both users. - await channel.send( + await send( `<@${target.id}> has taken <@${author.id}>'s bet, the bet amount of ${pluralise( amount, "Mon", @@ -114,7 +112,7 @@ export const BetCommand = new NamedCommand({ const receiver = Storage.getUser(target.id); // [TODO: when D.JSv13 comes out, inline reply to clean up.] // When bet is over, give a vote to ask people their thoughts. - const voteMsg = await channel.send( + const voteMsg = await send( `VOTE: do you think that <@${ target.id }> has won the bet?\nhttps://discord.com/channels/${guild!.id}/${channel.id}/${ @@ -142,18 +140,18 @@ export const BetCommand = new NamedCommand({ if (ok > no) { receiver.money += amount * 2; - channel.send( + send( `By the people's votes, <@${target.id}> has won the bet that <@${author.id}> had sent them.` ); } else if (ok < no) { sender.money += amount * 2; - channel.send( + send( `By the people's votes, <@${target.id}> has lost the bet that <@${author.id}> had sent them.` ); } else { sender.money += amount; receiver.money += amount; - channel.send( + send( `By the people's votes, <@${target.id}> couldn't be determined to have won or lost the bet that <@${author.id}> had sent them.` ); } @@ -162,7 +160,7 @@ export const BetCommand = new NamedCommand({ Storage.save(); }); }, duration); - } else return await channel.send(`<@${target.id}> has rejected your bet, <@${author.id}>`); + } else return await send(`<@${target.id}> has rejected your bet, <@${author.id}>`); } else return; } }) diff --git a/src/commands/fun/modules/eco-core.ts b/src/commands/fun/modules/eco-core.ts index 6ed9628..4bb5808 100644 --- a/src/commands/fun/modules/eco-core.ts +++ b/src/commands/fun/modules/eco-core.ts @@ -6,7 +6,7 @@ import {isAuthorized, getMoneyEmbed, getSendEmbed, ECO_EMBED_COLOR} from "./eco- export const DailyCommand = new NamedCommand({ description: "Pick up your daily Mons. The cooldown is per user and every 22 hours to allow for some leeway.", aliases: ["get"], - async run({author, channel, guild}) { + async run({send, author, channel, guild}) { if (isAuthorized(guild, channel)) { const user = Storage.getUser(author.id); const now = Date.now(); @@ -15,7 +15,7 @@ export const DailyCommand = new NamedCommand({ user.money++; user.lastReceived = now; Storage.save(); - channel.send({ + send({ embed: { title: "Daily Reward", description: "You received 1 Mon!", @@ -23,7 +23,7 @@ export const DailyCommand = new NamedCommand({ } }); } else - channel.send({ + send({ embed: { title: "Daily Reward", description: `It's too soon to pick up your daily Mons. You have about ${( @@ -39,7 +39,7 @@ export const DailyCommand = new NamedCommand({ export const GuildCommand = new NamedCommand({ description: "Get info on the guild's economy as a whole.", - async run({guild, channel}) { + async run({send, guild, channel}) { if (isAuthorized(guild, channel)) { const users = Storage.users; let totalAmount = 0; @@ -49,7 +49,7 @@ export const GuildCommand = new NamedCommand({ totalAmount += user.money; } - channel.send({ + send({ embed: { title: `The Bank of ${guild!.name}`, color: ECO_EMBED_COLOR, @@ -77,7 +77,7 @@ export const GuildCommand = new NamedCommand({ export const LeaderboardCommand = new NamedCommand({ description: "See the richest players.", aliases: ["top"], - async run({guild, channel, client}) { + async run({send, guild, channel, client}) { if (isAuthorized(guild, channel)) { const users = Storage.users; const ids = Object.keys(users); @@ -94,7 +94,7 @@ export const LeaderboardCommand = new NamedCommand({ }); } - channel.send({ + send({ embed: { title: "Top 10 Richest Players", color: ECO_EMBED_COLOR, @@ -116,24 +116,23 @@ export const PayCommand = new NamedCommand({ user: new Command({ run: "You need to enter an amount you're sending!", number: new Command({ - async run({args, author, channel, guild}): Promise { + async run({send, args, author, channel, guild}): Promise { if (isAuthorized(guild, channel)) { const amount = Math.floor(args[1]); const sender = Storage.getUser(author.id); const target = args[0]; const receiver = Storage.getUser(target.id); - if (amount <= 0) return channel.send("You must send at least one Mon!"); + if (amount <= 0) return send("You must send at least one Mon!"); else if (sender.money < amount) - return channel.send("You don't have enough Mons for that.", getMoneyEmbed(author)); - else if (target.id === author.id) return channel.send("You can't send Mons to yourself!"); - else if (target.bot && process.argv[2] !== "dev") - return channel.send("You can't send Mons to a bot!"); + return send("You don't have enough Mons for that.", getMoneyEmbed(author)); + else if (target.id === author.id) return send("You can't send Mons to yourself!"); + else if (target.bot && process.argv[2] !== "dev") return send("You can't send Mons to a bot!"); sender.money -= amount; receiver.money += amount; Storage.save(); - return channel.send(getSendEmbed(author, target, amount)); + return send(getSendEmbed(author, target, amount)); } } }) @@ -142,21 +141,20 @@ export const PayCommand = new NamedCommand({ run: "You must use the format `eco pay `!" }), any: new Command({ - async run({args, author, channel, guild}) { + async run({send, args, author, channel, guild}) { if (isAuthorized(guild, channel)) { const last = args.pop(); - if (!/\d+/g.test(last) && args.length === 0) - return channel.send("You need to enter an amount you're sending!"); + if (!/\d+/g.test(last) && args.length === 0) return send("You need to enter an amount you're sending!"); const amount = Math.floor(last); const sender = Storage.getUser(author.id); - if (amount <= 0) return channel.send("You must send at least one Mon!"); + if (amount <= 0) return send("You must send at least one Mon!"); else if (sender.money < amount) - return channel.send("You don't have enough Mons to do that!", getMoneyEmbed(author)); + return send("You don't have enough Mons to do that!", getMoneyEmbed(author)); else if (!guild) - return channel.send("You have to use this in a server if you want to send Mons with a username!"); + return send("You have to use this in a server if you want to send Mons with a username!"); const username = args.join(" "); const member = ( @@ -167,17 +165,16 @@ export const PayCommand = new NamedCommand({ ).first(); if (!member) - return channel.send( + return send( `Couldn't find a user by the name of \`${username}\`! If you want to send Mons to someone in a different server, you have to use their user ID!` ); - else if (member.user.id === author.id) return channel.send("You can't send Mons to yourself!"); - else if (member.user.bot && process.argv[2] !== "dev") - return channel.send("You can't send Mons to a bot!"); + else if (member.user.id === author.id) return send("You can't send Mons to yourself!"); + else if (member.user.bot && process.argv[2] !== "dev") return send("You can't send Mons to a bot!"); const target = member.user; return prompt( - await channel.send( + await send( `Are you sure you want to send ${pluralise( amount, "Mon", @@ -202,7 +199,7 @@ export const PayCommand = new NamedCommand({ sender.money -= amount; receiver.money += amount; Storage.save(); - channel.send(getSendEmbed(author, target, amount)); + send(getSendEmbed(author, target, amount)); } ); } diff --git a/src/commands/fun/modules/eco-extras.ts b/src/commands/fun/modules/eco-extras.ts index 2d69f29..a1876d8 100644 --- a/src/commands/fun/modules/eco-extras.ts +++ b/src/commands/fun/modules/eco-extras.ts @@ -8,7 +8,7 @@ const WEEKDAY = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday export const MondayCommand = new NamedCommand({ description: "Use this on a UTC Monday to get an extra Mon. Does not affect your 22 hour timer for `eco daily`.", - async run({guild, channel, author}) { + async run({send, guild, channel, author}) { if (isAuthorized(guild, channel)) { const user = Storage.getUser(author.id); const now = new Date(); @@ -21,13 +21,13 @@ export const MondayCommand = new NamedCommand({ user.money++; user.lastMonday = now.getTime(); Storage.save(); - channel.send("It is **Mon**day, my dudes.", getMoneyEmbed(author)); - } else channel.send("You've already claimed your **Mon**day reward for this week."); + send("It is **Mon**day, my dudes.", getMoneyEmbed(author)); + } else send("You've already claimed your **Mon**day reward for this week."); } else { const weekdayName = WEEKDAY[weekday]; const hourText = now.getUTCHours().toString().padStart(2, "0"); const minuteText = now.getUTCMinutes().toString().padStart(2, "0"); - channel.send( + send( `Come back when it's **Mon**day. Right now, it's ${weekdayName}, ${hourText}:${minuteText} (UTC).` ); } @@ -41,19 +41,19 @@ export const AwardCommand = new NamedCommand({ aliases: ["give"], run: "You need to specify a user!", user: new Command({ - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { if (author.id === "394808963356688394" || IS_DEV_MODE) { const target = args[0] as User; const user = Storage.getUser(target.id); user.money++; Storage.save(); - channel.send(`1 Mon given to ${target.username}.`, getMoneyEmbed(target)); + send(`1 Mon given to ${target.username}.`, getMoneyEmbed(target)); } else { - channel.send("This command is restricted to the bean."); + send("This command is restricted to the bean."); } }, number: new Command({ - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { if (author.id === "394808963356688394" || IS_DEV_MODE) { const target = args[0] as User; const amount = Math.floor(args[1]); @@ -62,15 +62,12 @@ export const AwardCommand = new NamedCommand({ const user = Storage.getUser(target.id); user.money += amount; Storage.save(); - channel.send( - `${pluralise(amount, "Mon", "s")} given to ${target.username}.`, - getMoneyEmbed(target) - ); + send(`${pluralise(amount, "Mon", "s")} given to ${target.username}.`, getMoneyEmbed(target)); } else { - channel.send("You need to enter a number greater than 0."); + send("You need to enter a number greater than 0."); } } else { - channel.send("This command is restricted to the bean."); + send("This command is restricted to the bean."); } } }) diff --git a/src/commands/fun/modules/eco-shop.ts b/src/commands/fun/modules/eco-shop.ts index ca161f1..b76c47d 100644 --- a/src/commands/fun/modules/eco-shop.ts +++ b/src/commands/fun/modules/eco-shop.ts @@ -7,7 +7,7 @@ import {EmbedField} from "discord.js"; export const ShopCommand = new NamedCommand({ description: "Displays the list of items you can buy in the shop.", - async run({guild, channel, author}) { + async run({send, guild, channel, author}) { if (isAuthorized(guild, channel)) { function getShopEmbed(selection: ShopItem[], title: string) { const fields: EmbedField[] = []; @@ -34,7 +34,7 @@ export const ShopCommand = new NamedCommand({ const shopPages = split(ShopItems, 5); const pageAmount = shopPages.length; - paginate(channel.send, author.id, pageAmount, (page, hasMultiplePages) => { + paginate(send, author.id, pageAmount, (page, hasMultiplePages) => { return getShopEmbed( shopPages[page], hasMultiplePages ? `Shop (Page ${page + 1} of ${pageAmount})` : "Shop" @@ -47,7 +47,7 @@ export const ShopCommand = new NamedCommand({ export const BuyCommand = new NamedCommand({ description: "Buys an item from the shop.", usage: "", - async run({guild, channel, args, message, author}) { + async run({send, guild, channel, args, message, author}) { if (isAuthorized(guild, channel)) { let found = false; @@ -65,7 +65,7 @@ export const BuyCommand = new NamedCommand({ const cost = item.cost * amount; if (cost > user.money) { - channel.send("Not enough Mons!"); + send("Not enough Mons!"); } else { user.money -= cost; Storage.save(); @@ -77,7 +77,7 @@ export const BuyCommand = new NamedCommand({ } } - if (!found) channel.send(`There's no item in the shop that goes by \`${requested}\`!`); + if (!found) send(`There's no item in the shop that goes by \`${requested}\`!`); } } }); diff --git a/src/commands/fun/neko.ts b/src/commands/fun/neko.ts index 18cfe76..1722232 100644 --- a/src/commands/fun/neko.ts +++ b/src/commands/fun/neko.ts @@ -36,19 +36,17 @@ const endpoints: {sfw: {[key: string]: string}} = { export default new NamedCommand({ description: "Provides you with a random image with the selected argument.", - async run({message, channel, guild, author, member, client, args}) { - channel.send( - `Please provide an image type. Available arguments:\n\`[${Object.keys(endpoints.sfw).join(", ")}]\`.` - ); + async run({send, message, channel, guild, author, member, client, args}) { + send(`Please provide an image type. Available arguments:\n\`[${Object.keys(endpoints.sfw).join(", ")}]\`.`); }, any: new Command({ description: "Image type to send.", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const arg = args[0]; - if (!(arg in endpoints.sfw)) return channel.send("Couldn't find that endpoint!"); + if (!(arg in endpoints.sfw)) return send("Couldn't find that endpoint!"); let url = new URL(`https://nekos.life/api/v2${endpoints.sfw[arg]}`); const content = await getContent(url.toString()); - return channel.send(content.url); + return send(content.url); } }) }); diff --git a/src/commands/fun/ok.ts b/src/commands/fun/ok.ts index b2998ab..66bfa6b 100644 --- a/src/commands/fun/ok.ts +++ b/src/commands/fun/ok.ts @@ -61,7 +61,7 @@ const responses = [ export default new NamedCommand({ description: "Sends random ok message.", - async run({message, channel, guild, author, member, client, args}) { - channel.send(`ok ${random(responses)}`); + async run({send, message, channel, guild, author, member, client, args}) { + send(`ok ${random(responses)}`); } }); diff --git a/src/commands/fun/owoify.ts b/src/commands/fun/owoify.ts index 2b4cdd7..e9e59d9 100644 --- a/src/commands/fun/owoify.ts +++ b/src/commands/fun/owoify.ts @@ -4,9 +4,9 @@ import {URL} from "url"; export default new NamedCommand({ description: "OwO-ifies the input.", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { let url = new URL(`https://nekos.life/api/v2/owoify?text=${args.join(" ")}`); const content = (await getContent(url.toString())) as any; // Apparently, the object in question is {owo: string}. - channel.send(content.owo); + send(content.owo); } }); diff --git a/src/commands/fun/party.ts b/src/commands/fun/party.ts index 90e62c5..db4c0e3 100644 --- a/src/commands/fun/party.ts +++ b/src/commands/fun/party.ts @@ -2,8 +2,8 @@ import {Command, NamedCommand} from "../../core"; export default new NamedCommand({ description: "Initiates a celebratory stream from the bot.", - async run({message, channel, guild, author, member, client, args}) { - channel.send("This calls for a celebration!"); + async run({send, message, channel, guild, author, member, client, args}) { + send("This calls for a celebration!"); client.user!.setActivity({ type: "STREAMING", url: "https://www.youtube.com/watch?v=dQw4w9WgXcQ", diff --git a/src/commands/fun/poll.ts b/src/commands/fun/poll.ts index 38efb68..3d60e1e 100644 --- a/src/commands/fun/poll.ts +++ b/src/commands/fun/poll.ts @@ -7,7 +7,7 @@ export default new NamedCommand({ run: "Please provide a question.", any: new Command({ description: "Question for the poll.", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const embed = new MessageEmbed() .setAuthor( `Poll created by ${message.author.username}`, @@ -16,7 +16,7 @@ export default new NamedCommand({ .setColor(0xffffff) .setFooter("React to vote.") .setDescription(args.join(" ")); - const msg = await channel.send(embed); + const msg = await send(embed); await msg.react("✅"); await msg.react("⛔"); message.delete({ diff --git a/src/commands/fun/ravi.ts b/src/commands/fun/ravi.ts index 7b408f6..e0862e0 100644 --- a/src/commands/fun/ravi.ts +++ b/src/commands/fun/ravi.ts @@ -4,8 +4,8 @@ import {Random} from "../../lib"; export default new NamedCommand({ description: "Ravioli ravioli...", usage: "[number from 1 to 9]", - async run({message, channel, guild, author, member, client, args}) { - channel.send({ + async run({send, message, channel, guild, author, member, client, args}) { + send({ embed: { title: "Ravioli ravioli...", image: { @@ -18,11 +18,11 @@ export default new NamedCommand({ }); }, number: new Command({ - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const arg: number = args[0]; if (arg >= 1 && arg <= 9) { - channel.send({ + send({ embed: { title: "Ravioli ravioli...", image: { @@ -31,7 +31,7 @@ export default new NamedCommand({ } }); } else { - channel.send("Please provide a number between 1 and 9."); + send("Please provide a number between 1 and 9."); } } }) diff --git a/src/commands/fun/thonk.ts b/src/commands/fun/thonk.ts index 35e4d4a..cdc3afe 100644 --- a/src/commands/fun/thonk.ts +++ b/src/commands/fun/thonk.ts @@ -34,9 +34,9 @@ let phrase = "I have no currently set phrase!"; export default new NamedCommand({ description: "Transforms your text into vietnamese.", usage: "thonk ([text])", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { if (args.length > 0) phrase = args.join(" "); - const msg = await channel.send(transform(phrase)); + const msg = await send(transform(phrase)); msg.createReactionCollector( (reaction, user) => { if (user.id === author.id && reaction.emoji.name === "❌") msg.delete(); diff --git a/src/commands/fun/urban.ts b/src/commands/fun/urban.ts index 212a2a5..44d5a64 100644 --- a/src/commands/fun/urban.ts +++ b/src/commands/fun/urban.ts @@ -6,7 +6,7 @@ export default new NamedCommand({ description: "Gives you a definition of the inputted word.", run: "Please input a word.", any: new Command({ - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { // [Bug Fix]: Use encodeURIComponent() when emojis are used: "TypeError [ERR_UNESCAPED_CHARACTERS]: Request path contains unescaped characters" urban(encodeURIComponent(args.join(" "))) .then((res) => { @@ -21,10 +21,10 @@ export default new NamedCommand({ if (res.tags && res.tags.length > 0 && res.tags.join(" ").length < 1024) embed.addField("Tags", res.tags.join(", "), true); - channel.send(embed); + send(embed); }) .catch(() => { - channel.send("Sorry, that word was not found."); + send("Sorry, that word was not found."); }); } }) diff --git a/src/commands/fun/vaporwave.ts b/src/commands/fun/vaporwave.ts index ad6b945..e2c1727 100644 --- a/src/commands/fun/vaporwave.ts +++ b/src/commands/fun/vaporwave.ts @@ -25,10 +25,10 @@ export default new NamedCommand({ description: "Transforms your text into vaporwave.", run: "You need to enter some text!", any: new Command({ - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const text = getVaporwaveText(args.join(" ")); - if (text !== "") channel.send(text); - else channel.send("Make sure to enter at least one valid character."); + if (text !== "") send(text); + else send("Make sure to enter at least one valid character."); } }) }); diff --git a/src/commands/fun/weather.ts b/src/commands/fun/weather.ts index 07c44f5..6760286 100644 --- a/src/commands/fun/weather.ts +++ b/src/commands/fun/weather.ts @@ -6,15 +6,15 @@ export default new NamedCommand({ description: "Shows weather info of specified location.", run: "You need to provide a city.", any: new Command({ - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { find( { search: args.join(" "), degreeType: "C" }, function (error, result) { - if (error) return channel.send(error.toString()); - if (result.length === 0) return channel.send("No city found by that name."); + if (error) return send(error.toString()); + if (result.length === 0) return send("No city found by that name."); var current = result[0].current; var location = result[0].location; const embed = new MessageEmbed() @@ -28,7 +28,7 @@ export default new NamedCommand({ .addField("Feels like", `${current.feelslike} Degrees`, true) .addField("Winds", current.winddisplay, true) .addField("Humidity", `${current.humidity}%`, true); - return channel.send({ + return send({ embed }); } diff --git a/src/commands/fun/whois.ts b/src/commands/fun/whois.ts index 0c16b38..aea91c7 100644 --- a/src/commands/fun/whois.ts +++ b/src/commands/fun/whois.ts @@ -43,44 +43,42 @@ const registry: {[id: string]: string} = { export default new NamedCommand({ description: "Tells you who you or the specified user is.", aliases: ["whoami"], - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const id = author.id; if (id in registry) { - channel.send(registry[id]); + send(registry[id]); } else { - channel.send("You haven't been added to the registry yet!"); + send("You haven't been added to the registry yet!"); } }, id: "user", user: new Command({ - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const user: User = args[0]; const id = user.id; if (id in registry) { - channel.send(`\`${user.username}\` - ${registry[id]}`); + send(`\`${user.username}\` - ${registry[id]}`); } else { - channel.send(`\`${user.tag}\` hasn't been added to the registry yet!`); + send(`\`${user.tag}\` hasn't been added to the registry yet!`); } } }), any: new Command({ channelType: CHANNEL_TYPE.GUILD, - async run({message, channel, guild, author, client, args}) { + async run({send, message, channel, guild, author, client, args}) { const query = args.join(" ") as string; const member = await getMemberByName(guild!, query); if (member instanceof GuildMember) { if (member.id in registry) { - channel.send(`\`${member.nickname ?? member.user.username}\` - ${registry[member.id]}`); + send(`\`${member.nickname ?? member.user.username}\` - ${registry[member.id]}`); } else { - channel.send( - `\`${member.nickname ?? member.user.username}\` hasn't been added to the registry yet!` - ); + send(`\`${member.nickname ?? member.user.username}\` hasn't been added to the registry yet!`); } } else { - channel.send(member); + send(member); } } }) diff --git a/src/commands/system/admin.ts b/src/commands/system/admin.ts index f487e07..6b2593a 100644 --- a/src/commands/system/admin.ts +++ b/src/commands/system/admin.ts @@ -21,9 +21,9 @@ const statuses = ["online", "idle", "dnd", "invisible"]; export default new NamedCommand({ description: "An all-in-one command to do admin stuff. You need to be either an admin of the server or one of the bot's mechanics to use this command.", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const permLevel = getPermissionLevel(author, member); - return channel.send(`${author}, your permission level is \`${getPermissionName(permLevel)}\` (${permLevel}).`); + return send(`${author}, your permission level is \`${getPermissionName(permLevel)}\` (${permLevel}).`); }, subcommands: { set: new NamedCommand({ @@ -35,26 +35,26 @@ export default new NamedCommand({ prefix: new NamedCommand({ description: "Set a custom prefix for your guild. Removes your custom prefix if none is provided.", usage: "() (<@bot>)", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { Storage.getGuild(guild!.id).prefix = null; Storage.save(); - channel.send( + send( `The custom prefix for this guild has been removed. My prefix is now back to \`${Config.prefix}\`.` ); }, any: new Command({ - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { Storage.getGuild(guild!.id).prefix = args[0]; Storage.save(); - channel.send(`The custom prefix for this guild is now \`${args[0]}\`.`); + send(`The custom prefix for this guild is now \`${args[0]}\`.`); }, user: new Command({ description: "Specifies the bot in case of conflicting prefixes.", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { if ((args[1] as User).id === client.user!.id) { Storage.getGuild(guild!.id).prefix = args[0]; Storage.save(); - channel.send(`The custom prefix for this guild is now \`${args[0]}\`.`); + send(`The custom prefix for this guild is now \`${args[0]}\`.`); } } }) @@ -69,25 +69,25 @@ export default new NamedCommand({ description: "Sets how welcome messages are displayed for your server. Removes welcome messages if unspecified.", usage: "`none`/`text`/`graphical`", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { Storage.getGuild(guild!.id).welcomeType = "none"; Storage.save(); - channel.send("Set this server's welcome type to `none`."); + send("Set this server's welcome type to `none`."); }, // I should probably make this a bit more dynamic... Oh well. subcommands: { text: new NamedCommand({ - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { Storage.getGuild(guild!.id).welcomeType = "text"; Storage.save(); - channel.send("Set this server's welcome type to `text`."); + send("Set this server's welcome type to `text`."); } }), graphical: new NamedCommand({ - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { Storage.getGuild(guild!.id).welcomeType = "graphical"; Storage.save(); - channel.send("Set this server's welcome type to `graphical`."); + send("Set this server's welcome type to `graphical`."); } }) } @@ -95,18 +95,18 @@ export default new NamedCommand({ channel: new NamedCommand({ description: "Sets the welcome channel for your server. Type `#` to reference the channel.", usage: "()", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { Storage.getGuild(guild!.id).welcomeChannel = channel.id; Storage.save(); - channel.send(`Successfully set ${channel} as the welcome channel for this server.`); + send(`Successfully set ${channel} as the welcome channel for this server.`); }, id: "channel", channel: new Command({ - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const result = args[0] as TextChannel; Storage.getGuild(guild!.id).welcomeChannel = result.id; Storage.save(); - channel.send(`Successfully set this server's welcome channel to ${result}.`); + send(`Successfully set this server's welcome channel to ${result}.`); } }) }), @@ -114,17 +114,17 @@ export default new NamedCommand({ description: "Sets a custom welcome message for your server. Use `%user%` as the placeholder for the user.", usage: "()", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { Storage.getGuild(guild!.id).welcomeMessage = null; Storage.save(); - channel.send("Reset your server's welcome message to the default."); + send("Reset your server's welcome message to the default."); }, any: new Command({ - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const newMessage = args.join(" "); Storage.getGuild(guild!.id).welcomeMessage = newMessage; Storage.save(); - channel.send(`Set your server's welcome message to \`${newMessage}\`.`); + send(`Set your server's welcome message to \`${newMessage}\`.`); } }) }) @@ -133,26 +133,26 @@ export default new NamedCommand({ stream: new NamedCommand({ description: "Set a channel to send stream notifications. Type `#` to reference the channel.", usage: "()", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const targetGuild = Storage.getGuild(guild!.id); if (targetGuild.streamingChannel) { targetGuild.streamingChannel = null; - channel.send("Removed your server's stream notifications channel."); + send("Removed your server's stream notifications channel."); } else { targetGuild.streamingChannel = channel.id; - channel.send(`Set your server's stream notifications channel to ${channel}.`); + send(`Set your server's stream notifications channel to ${channel}.`); } Storage.save(); }, id: "channel", channel: new Command({ - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const result = args[0] as TextChannel; Storage.getGuild(guild!.id).streamingChannel = result.id; Storage.save(); - channel.send(`Successfully set this server's stream notifications channel to ${result}.`); + send(`Successfully set this server's stream notifications channel to ${result}.`); } }) }) @@ -161,17 +161,17 @@ export default new NamedCommand({ diag: new NamedCommand({ description: 'Requests a debug log with the "info" verbosity level.', permission: PERMISSIONS.BOT_SUPPORT, - async run({message, channel, guild, author, member, client, args}) { - channel.send(getLogBuffer("info")); + async run({send, message, channel, guild, author, member, client, args}) { + send(getLogBuffer("info")); }, any: new Command({ description: `Select a verbosity to listen to. Available levels: \`[${Object.keys(logs).join(", ")}]\``, - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const type = args[0]; - if (type in logs) channel.send(getLogBuffer(type)); + if (type in logs) send(getLogBuffer(type)); else - channel.send( + send( `Couldn't find a verbosity level named \`${type}\`! The available types are \`[${Object.keys( logs ).join(", ")}]\`.` @@ -182,17 +182,17 @@ export default new NamedCommand({ status: new NamedCommand({ description: "Changes the bot's status.", permission: PERMISSIONS.BOT_SUPPORT, - async run({message, channel, guild, author, member, client, args}) { - channel.send("Setting status to `online`..."); + async run({send, message, channel, guild, author, member, client, args}) { + send("Setting status to `online`..."); }, any: new Command({ description: `Select a status to set to. Available statuses: \`[${statuses.join(", ")}]\`.`, - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { if (!statuses.includes(args[0])) { - return channel.send("That status doesn't exist!"); + return send("That status doesn't exist!"); } else { client.user?.setStatus(args[0]); - return channel.send(`Setting status to \`${args[0]}\`...`); + return send(`Setting status to \`${args[0]}\`...`); } } }) @@ -201,7 +201,7 @@ export default new NamedCommand({ description: "Purges the bot's own messages.", permission: PERMISSIONS.BOT_SUPPORT, channelType: CHANNEL_TYPE.GUILD, - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { // It's probably better to go through the bot's own messages instead of calling bulkDelete which requires MANAGE_MESSAGES. if (botHasPermission(guild, Permissions.FLAGS.MANAGE_MESSAGES)) { message.delete(); @@ -210,16 +210,14 @@ export default new NamedCommand({ }); const travMessages = msgs.filter((m) => m.author.id === client.user?.id); - await channel.send(`Found ${travMessages.size} messages to delete.`).then((m) => + await send(`Found ${travMessages.size} messages to delete.`).then((m) => m.delete({ timeout: 5000 }) ); await (channel as TextChannel).bulkDelete(travMessages); } else { - channel.send( - "This command must be executed in a guild where I have the `MANAGE_MESSAGES` permission." - ); + send("This command must be executed in a guild where I have the `MANAGE_MESSAGES` permission."); } } }), @@ -230,7 +228,7 @@ export default new NamedCommand({ run: "A number was not provided.", number: new Command({ description: "Amount of messages to delete.", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { message.delete(); const fetched = await channel.messages.fetch({ limit: args[0] @@ -244,15 +242,15 @@ export default new NamedCommand({ usage: "", permission: PERMISSIONS.BOT_OWNER, // You have to bring everything into scope to use them. AFAIK, there isn't a more maintainable way to do this, but at least TS will let you know if anything gets removed. - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { try { const code = args.join(" "); let evaled = eval(code); if (typeof evaled !== "string") evaled = require("util").inspect(evaled); - channel.send(clean(evaled), {code: "js", split: true}); + send(clean(evaled), {code: "js", split: true}); } catch (err) { - channel.send(clean(err), {code: "js", split: true}); + send(clean(err), {code: "js", split: true}); } } }), @@ -260,43 +258,43 @@ export default new NamedCommand({ description: "Change the bot's nickname.", permission: PERMISSIONS.BOT_SUPPORT, channelType: CHANNEL_TYPE.GUILD, - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const nickName = args.join(" "); await guild!.me?.setNickname(nickName); if (botHasPermission(guild, Permissions.FLAGS.MANAGE_MESSAGES)) message.delete({timeout: 5000}); - channel.send(`Nickname set to \`${nickName}\``).then((m) => m.delete({timeout: 5000})); + send(`Nickname set to \`${nickName}\``).then((m) => m.delete({timeout: 5000})); } }), guilds: new NamedCommand({ description: "Shows a list of all guilds the bot is a member of.", permission: PERMISSIONS.BOT_SUPPORT, - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const guildList = client.guilds.cache.array().map((e) => e.name); - channel.send(guildList, {split: true}); + send(guildList, {split: true}); } }), activity: new NamedCommand({ description: "Set the activity of the bot.", permission: PERMISSIONS.BOT_SUPPORT, usage: " ", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { client.user?.setActivity(".help", { type: "LISTENING" }); - channel.send("Activity set to default."); + send("Activity set to default."); }, any: new Command({ description: `Select an activity type to set. Available levels: \`[${activities.join(", ")}]\``, - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const type = args[0]; if (activities.includes(type)) { client.user?.setActivity(args.slice(1).join(" "), { type: args[0].toUpperCase() }); - channel.send(`Set activity to \`${args[0].toUpperCase()}\` \`${args.slice(1).join(" ")}\`.`); + send(`Set activity to \`${args[0].toUpperCase()}\` \`${args.slice(1).join(" ")}\`.`); } else - channel.send( + send( `Couldn't find an activity type named \`${type}\`! The available types are \`[${activities.join( ", " )}]\`.` @@ -308,17 +306,17 @@ export default new NamedCommand({ description: "Sets up the current channel to receive system logs.", permission: PERMISSIONS.BOT_ADMIN, channelType: CHANNEL_TYPE.GUILD, - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { Config.systemLogsChannel = channel.id; Config.save(); - channel.send(`Successfully set ${channel} as the system logs channel.`); + send(`Successfully set ${channel} as the system logs channel.`); }, channel: new Command({ - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const targetChannel = args[0] as TextChannel; Config.systemLogsChannel = targetChannel.id; Config.save(); - channel.send(`Successfully set ${targetChannel} as the system logs channel.`); + send(`Successfully set ${targetChannel} as the system logs channel.`); } }) }) diff --git a/src/commands/system/help.ts b/src/commands/system/help.ts index c6ec382..9cf6b8c 100644 --- a/src/commands/system/help.ts +++ b/src/commands/system/help.ts @@ -16,11 +16,11 @@ export default new NamedCommand({ description: "Lists all commands. If a command is specified, their arguments are listed as well.", usage: "([command, [subcommand/type], ...])", aliases: ["h"], - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const commands = await getCommandList(); const categoryArray = commands.keyArray(); - paginate(channel.send, author.id, categoryArray.length, (page, hasMultiplePages) => { + paginate(send, author.id, categoryArray.length, (page, hasMultiplePages) => { const category = categoryArray[page]; const commandList = commands.get(category)!; let output = `Legend: \`\`, \`[list/of/stuff]\`, \`(optional)\`, \`()\`, \`([optional/list/...])\`\n`; @@ -32,9 +32,9 @@ export default new NamedCommand({ }); }, any: new Command({ - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const [result, category] = await getCommandInfo(args); - if (typeof result === "string") return channel.send(result); + if (typeof result === "string") return send(result); let append = ""; const command = result.command; const header = result.args.length > 0 ? `${result.header} ${result.args.join(" ")}` : result.header; @@ -66,7 +66,7 @@ export default new NamedCommand({ aliases = formattedAliases.join(", ") || "None"; } - return channel.send( + return send( new MessageEmbed() .setTitle(header) .setDescription(command.description) diff --git a/src/commands/template.ts b/src/commands/template.ts index 70d0e65..bc7770e 100644 --- a/src/commands/template.ts +++ b/src/commands/template.ts @@ -1,7 +1,7 @@ import {Command, NamedCommand} from "../core"; export default new NamedCommand({ - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { // code } }); diff --git a/src/commands/utility/calc.ts b/src/commands/utility/calc.ts index dad3e88..e31940d 100644 --- a/src/commands/utility/calc.ts +++ b/src/commands/utility/calc.ts @@ -4,19 +4,19 @@ import {MessageEmbed} from "discord.js"; export default new NamedCommand({ description: "Calculates a specified math expression.", - async run({message, channel, guild, author, member, client, args}) { - if (!args[0]) return channel.send("Please provide a calculation."); + async run({send, message, channel, guild, author, member, client, args}) { + if (!args[0]) return send("Please provide a calculation."); let resp; try { resp = math.evaluate(args.join(" ")); } catch (e) { - return channel.send("Please provide a *valid* calculation."); + return send("Please provide a *valid* calculation."); } const embed = new MessageEmbed() .setColor(0xffffff) .setTitle("Math Calculation") .addField("Input", `\`\`\`js\n${args.join("")}\`\`\``) .addField("Output", `\`\`\`js\n${resp}\`\`\``); - return channel.send(embed); + return send(embed); } }); diff --git a/src/commands/utility/desc.ts b/src/commands/utility/desc.ts index b3c3d57..773b997 100644 --- a/src/commands/utility/desc.ts +++ b/src/commands/utility/desc.ts @@ -3,17 +3,17 @@ import {Command, NamedCommand} from "../../core"; export default new NamedCommand({ description: "Renames current voice channel.", usage: "", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const voiceChannel = message.member?.voice.channel; - if (!voiceChannel) return channel.send("You are not in a voice channel."); + if (!voiceChannel) return send("You are not in a voice channel."); if (!voiceChannel.guild.me?.hasPermission("MANAGE_CHANNELS")) - return channel.send("I am lacking the required permissions to perform this action."); - if (args.length === 0) return channel.send("Please provide a new voice channel name."); + return send("I am lacking the required permissions to perform this action."); + if (args.length === 0) return send("Please provide a new voice channel name."); const prevName = voiceChannel.name; const newName = args.join(" "); await voiceChannel.setName(newName); - return await channel.send(`Changed channel name from "${prevName}" to "${newName}".`); + return await send(`Changed channel name from "${prevName}" to "${newName}".`); } }); diff --git a/src/commands/utility/emote.ts b/src/commands/utility/emote.ts index 1b8aa07..4f5e933 100644 --- a/src/commands/utility/emote.ts +++ b/src/commands/utility/emote.ts @@ -8,9 +8,9 @@ export default new NamedCommand({ any: new Command({ description: "The emote(s) to send.", usage: "", - async run({guild, channel, message, args}) { + async run({send, guild, channel, message, args}) { const output = processEmoteQueryFormatted(args); - if (output.length > 0) channel.send(output); + if (output.length > 0) send(output); } }) }); diff --git a/src/commands/utility/info.ts b/src/commands/utility/info.ts index f3d5bb3..fff17c5 100644 --- a/src/commands/utility/info.ts +++ b/src/commands/utility/info.ts @@ -8,21 +8,21 @@ import moment, {utc} from "moment"; export default new NamedCommand({ description: "Command to provide all sorts of info about the current server, a user, etc.", - async run({message, channel, guild, author, member, client, args}) { - channel.send(await getUserInfo(author, member)); + async run({send, message, channel, guild, author, member, client, args}) { + send(await getUserInfo(author, member)); }, subcommands: { avatar: new NamedCommand({ description: "Shows your own, or another user's avatar.", usage: "()", - async run({message, channel, guild, author, member, client, args}) { - channel.send(author.displayAvatarURL({dynamic: true, size: 2048})); + async run({send, message, channel, guild, author, member, client, args}) { + send(author.displayAvatarURL({dynamic: true, size: 2048})); }, id: "user", user: new Command({ description: "Shows your own, or another user's avatar.", - async run({message, channel, guild, author, member, client, args}) { - channel.send( + async run({send, message, channel, guild, author, member, client, args}) { + send( args[0].displayAvatarURL({ dynamic: true, size: 2048 @@ -33,26 +33,26 @@ export default new NamedCommand({ any: new Command({ description: "Shows another user's avatar by searching their name", channelType: CHANNEL_TYPE.GUILD, - async run({message, channel, guild, author, client, args}) { + async run({send, message, channel, guild, author, client, args}) { const name = args.join(" "); const member = await getMemberByName(guild!, name); if (member instanceof GuildMember) { - channel.send( + send( member.user.displayAvatarURL({ dynamic: true, size: 2048 }) ); } else { - channel.send(member); + send(member); } } }) }), bot: new NamedCommand({ description: "Displays info about the bot.", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const core = os.cpus()[0]; const embed = new MessageEmbed() .setColor(guild?.me?.displayHexColor || "BLUE") @@ -88,33 +88,33 @@ export default new NamedCommand({ size: 2048 }); if (avatarURL) embed.setThumbnail(avatarURL); - channel.send(embed); + send(embed); } }), guild: new NamedCommand({ description: "Displays info about the current guild or another guild.", usage: "(/)", channelType: CHANNEL_TYPE.GUILD, - async run({message, channel, guild, author, member, client, args}) { - channel.send(await getGuildInfo(guild!, guild)); + async run({send, message, channel, guild, author, member, client, args}) { + send(await getGuildInfo(guild!, guild)); }, id: "guild", guild: new Command({ description: "Display info about a guild by its ID.", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const targetGuild = args[0] as Guild; - channel.send(await getGuildInfo(targetGuild, guild)); + send(await getGuildInfo(targetGuild, guild)); } }), any: new Command({ description: "Display info about a guild by finding its name.", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const targetGuild = getGuildByName(args.join(" ")); if (targetGuild instanceof Guild) { - channel.send(await getGuildInfo(targetGuild, guild)); + send(await getGuildInfo(targetGuild, guild)); } else { - channel.send(targetGuild); + send(targetGuild); } } }) @@ -123,11 +123,11 @@ export default new NamedCommand({ id: "user", user: new Command({ description: "Displays info about mentioned user.", - async run({message, channel, guild, author, client, args}) { + async run({send, message, channel, guild, author, client, args}) { const user = args[0] as User; // Transforms the User object into a GuildMember object of the current guild. const member = guild?.members.resolve(args[0]); - channel.send(await getUserInfo(user, member)); + send(await getUserInfo(user, member)); } }) }); diff --git a/src/commands/utility/invite.ts b/src/commands/utility/invite.ts index 5d5657c..9dc0fa3 100644 --- a/src/commands/utility/invite.ts +++ b/src/commands/utility/invite.ts @@ -2,8 +2,8 @@ import {Command, NamedCommand} from "../../core"; export default new NamedCommand({ description: "Gives you the invite link.", - async run({message, channel, guild, author, member, client, args}) { - channel.send( + async run({send, message, channel, guild, author, member, client, args}) { + send( `https://discordapp.com/api/oauth2/authorize?client_id=${client.user!.id}&permissions=${ args[0] || 8 }&scope=bot` diff --git a/src/commands/utility/lsemotes.ts b/src/commands/utility/lsemotes.ts index 0d858bc..6633d63 100644 --- a/src/commands/utility/lsemotes.ts +++ b/src/commands/utility/lsemotes.ts @@ -1,5 +1,5 @@ -import {GuildEmoji, MessageEmbed, TextChannel, DMChannel, NewsChannel, User} from "discord.js"; -import {Command, NamedCommand, paginate} from "../../core"; +import {GuildEmoji, MessageEmbed, User} from "discord.js"; +import {Command, NamedCommand, paginate, SendFunction} from "../../core"; import {split} from "../../lib"; import vm from "vm"; @@ -8,20 +8,20 @@ const REGEX_TIMEOUT_MS = 1000; export default new NamedCommand({ description: "Lists all emotes the bot has in it's registry,", usage: " (-flags)", - async run({message, channel, guild, author, member, client, args}) { - displayEmoteList(client.emojis.cache.array(), channel, author); + async run({send, message, channel, guild, author, member, client, args}) { + displayEmoteList(client.emojis.cache.array(), send, author); }, any: new Command({ description: "Filters emotes by via a regular expression. Flags can be added by adding a dash at the end. For example, to do a case-insensitive search, do %prefix%lsemotes somepattern -i", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { // If a guild ID is provided, filter all emotes by that guild (but only if there aren't any arguments afterward) if (args.length === 1 && /^\d{17,}$/.test(args[0])) { const guildID: string = args[0]; displayEmoteList( client.emojis.cache.filter((emote) => emote.guild.id === guildID).array(), - channel, + send, author ); } else { @@ -57,10 +57,10 @@ export default new NamedCommand({ script.runInContext(context, {timeout: REGEX_TIMEOUT_MS}); emotes = sandbox.emotes; emoteCollection = emoteCollection.filter((emote) => emotes.has(emote.id)); // Only allow emotes that haven't been deleted. - displayEmoteList(emoteCollection, channel, author); + displayEmoteList(emoteCollection, send, author); } catch (error) { if (error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") { - channel.send( + send( `The regular expression you entered exceeded the time limit of ${REGEX_TIMEOUT_MS} milliseconds.` ); } else { @@ -68,14 +68,14 @@ export default new NamedCommand({ } } } else { - channel.send("Failed to initialize sandbox."); + send("Failed to initialize sandbox."); } } } }) }); -async function displayEmoteList(emotes: GuildEmoji[], channel: TextChannel | DMChannel | NewsChannel, author: User) { +async function displayEmoteList(emotes: GuildEmoji[], send: SendFunction, author: User) { emotes.sort((a, b) => { const first = a.name.toLowerCase(); const second = b.name.toLowerCase(); @@ -90,7 +90,7 @@ async function displayEmoteList(emotes: GuildEmoji[], channel: TextChannel | DMC // Gather the first page (if it even exists, which it might not if there no valid emotes appear) if (pages > 0) { - paginate(channel.send, author.id, pages, (page, hasMultiplePages) => { + paginate(send, author.id, pages, (page, hasMultiplePages) => { embed.setTitle(hasMultiplePages ? `**Emotes** (Page ${page + 1} of ${pages})` : "**Emotes**"); let desc = ""; @@ -102,6 +102,6 @@ async function displayEmoteList(emotes: GuildEmoji[], channel: TextChannel | DMC return embed; }); } else { - channel.send("No valid emotes found by that query."); + send("No valid emotes found by that query."); } } diff --git a/src/commands/utility/react.ts b/src/commands/utility/react.ts index 42e8227..c4150c2 100644 --- a/src/commands/utility/react.ts +++ b/src/commands/utility/react.ts @@ -6,7 +6,7 @@ export default new NamedCommand({ description: "Reacts to the a previous message in your place. You have to react with the same emote before the bot removes that reaction.", usage: 'react ()', - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { let target: Message | undefined; let distance = 1; @@ -32,18 +32,18 @@ export default new NamedCommand({ try { guild = await client.guilds.fetch(guildID); } catch { - return channel.send(`\`${guildID}\` is an invalid guild ID!`); + return send(`\`${guildID}\` is an invalid guild ID!`); } } if (tmpChannel.id !== channelID) tmpChannel = guild.channels.cache.get(channelID); - if (!tmpChannel) return channel.send(`\`${channelID}\` is an invalid channel ID!`); + if (!tmpChannel) return send(`\`${channelID}\` is an invalid channel ID!`); if (message.id !== messageID) { try { target = await (tmpChannel as TextChannel).messages.fetch(messageID); } catch { - return channel.send(`\`${messageID}\` is an invalid message ID!`); + return send(`\`${messageID}\` is an invalid message ID!`); } } @@ -57,13 +57,13 @@ export default new NamedCommand({ let tmpChannel: Channel | undefined = channel; if (tmpChannel.id !== channelID) tmpChannel = guild?.channels.cache.get(channelID); - if (!tmpChannel) return channel.send(`\`${channelID}\` is an invalid channel ID!`); + if (!tmpChannel) return send(`\`${channelID}\` is an invalid channel ID!`); if (message.id !== messageID) { try { target = await (tmpChannel as TextChannel).messages.fetch(messageID); } catch { - return channel.send(`\`${messageID}\` is an invalid message ID!`); + return send(`\`${messageID}\` is an invalid message ID!`); } } @@ -74,7 +74,7 @@ export default new NamedCommand({ try { target = await channel.messages.fetch(last); } catch { - return channel.send(`No valid message found by the ID \`${last}\`!`); + return send(`No valid message found by the ID \`${last}\`!`); } args.pop(); @@ -84,7 +84,7 @@ export default new NamedCommand({ distance = parseInt(last); if (distance >= 0 && distance <= 99) args.pop(); - else return channel.send("Your distance must be between 0 and 99!"); + else return send("Your distance must be between 0 and 99!"); } } diff --git a/src/commands/utility/say.ts b/src/commands/utility/say.ts index 6c70dfb..02067a1 100644 --- a/src/commands/utility/say.ts +++ b/src/commands/utility/say.ts @@ -6,8 +6,8 @@ export default new NamedCommand({ run: "Please provide a message for me to say!", any: new Command({ description: "Message to repeat.", - async run({message, channel, guild, author, member, client, args}) { - channel.send(`*${author} says:*\n${args.join(" ")}`); + async run({send, message, channel, guild, author, member, client, args}) { + send(`*${author} says:*\n${args.join(" ")}`); } }) }); diff --git a/src/commands/utility/scanemotes.ts b/src/commands/utility/scanemotes.ts index a432147..08fb74f 100644 --- a/src/commands/utility/scanemotes.ts +++ b/src/commands/utility/scanemotes.ts @@ -9,7 +9,7 @@ export default new NamedCommand({ description: "Scans all text channels in the current guild and returns the number of times each emoji specific to the guild has been used. Has a cooldown of 24 hours per guild.", channelType: CHANNEL_TYPE.GUILD, - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { // Test if the command is on cooldown. This isn't the strictest cooldown possible, because in the event that the bot crashes, the cooldown will be reset. But for all intends and purposes, it's a good enough cooldown. It's a per-server cooldown. const startTime = Date.now(); const cooldown = 86400000; // 24 hours @@ -19,9 +19,7 @@ export default new NamedCommand({ // If it's been less than an hour since the command was last used, prevent it from executing. if (difference < cooldown) - return channel.send( - `This command requires a day to cooldown. You'll be able to activate this command ${howLong}.` - ); + return send(`This command requires a day to cooldown. You'll be able to activate this command ${howLong}.`); else lastUsedTimestamps.set(guild!.id, startTime); const stats: { @@ -41,7 +39,7 @@ export default new NamedCommand({ let channelsSearched = 0; let currentChannelName = ""; const totalChannels = allTextChannelsInCurrentGuild.size; - const statusMessage = await channel.send("Gathering emotes..."); + const statusMessage = await send("Gathering emotes..."); let warnings = 0; channel.startTyping(); @@ -181,15 +179,15 @@ export default new NamedCommand({ ); } - return await channel.send(lines, {split: true}); + return await send(lines, {split: true}); }, subcommands: { forcereset: new NamedCommand({ description: "Forces the cooldown timer to reset.", permission: PERMISSIONS.BOT_SUPPORT, - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { lastUsedTimestamps.set(guild!.id, 0); - channel.send("Reset the cooldown on `scanemotes`."); + send("Reset the cooldown on `scanemotes`."); } }) } diff --git a/src/commands/utility/shorten.ts b/src/commands/utility/shorten.ts index a14c460..9d74544 100644 --- a/src/commands/utility/shorten.ts +++ b/src/commands/utility/shorten.ts @@ -5,14 +5,14 @@ export default new NamedCommand({ description: "Shortens a given URL.", run: "Please provide a URL.", any: new Command({ - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { https.get("https://is.gd/create.php?format=simple&url=" + encodeURIComponent(args[0]), function (res) { var body = ""; res.on("data", function (chunk) { body += chunk; }); res.on("end", function () { - channel.send(`<${body}>`); + send(`<${body}>`); }); }); } diff --git a/src/commands/utility/streaminfo.ts b/src/commands/utility/streaminfo.ts index c1a0c9d..c1cff8a 100644 --- a/src/commands/utility/streaminfo.ts +++ b/src/commands/utility/streaminfo.ts @@ -3,7 +3,7 @@ import {streamList} from "../../modules/streamNotifications"; export default new NamedCommand({ description: "Sets the description of your stream. You can embed links by writing `[some name](some link)`", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const userID = author.id; if (streamList.has(userID)) { @@ -11,7 +11,7 @@ export default new NamedCommand({ const description = args.join(" ") || "No description set."; stream.description = description; stream.update(); - channel.send(`Successfully set the stream description to:`, { + send(`Successfully set the stream description to:`, { embed: { description, color: member!.displayColor @@ -19,7 +19,7 @@ export default new NamedCommand({ }); } else { // Alternatively, I could make descriptions last outside of just one stream. - channel.send("You can only use this command when streaming."); + send("You can only use this command when streaming."); } } }); diff --git a/src/commands/utility/time.ts b/src/commands/utility/time.ts index b48a4a2..656df97 100644 --- a/src/commands/utility/time.ts +++ b/src/commands/utility/time.ts @@ -169,21 +169,21 @@ function getTimeEmbed(user: User) { export default new NamedCommand({ description: "Show others what time it is for you.", aliases: ["tz"], - async run({channel, author}) { - channel.send(getTimeEmbed(author)); + async run({send, channel, author}) { + send(getTimeEmbed(author)); }, subcommands: { // Welcome to callback hell. We hope you enjoy your stay here! setup: new NamedCommand({ description: "Registers your timezone information for the bot.", - async run({author, channel}) { + async run({send, author, channel}) { const profile = Storage.getUser(author.id); profile.timezone = null; profile.daylightSavingsRegion = null; let hour: number; ask( - await channel.send( + await send( "What hour (0 to 23) is it for you right now?\n*(Note: Make sure to use Discord's inline reply feature or this won't work!)*" ), author.id, @@ -257,7 +257,7 @@ export default new NamedCommand({ // I calculate the list beforehand and check for duplicates to reduce unnecessary asking. if (duplicates.includes(hour)) { const isSameDay = await askYesOrNo( - await channel.send( + await send( `Is the current day of the month the ${moment().utc().format("Do")} for you?` ), author.id @@ -289,13 +289,13 @@ export default new NamedCommand({ // I should note that error handling should be added sometime because await throws an exception on Promise.reject. const hasDST = await askYesOrNo( - await channel.send("Does your timezone change based on daylight savings?"), + await send("Does your timezone change based on daylight savings?"), author.id ); const finalize = () => { Storage.save(); - channel.send( + send( "You've finished setting up your timezone! Just check to see if this looks right, and if it doesn't, run this setup again.", getTimeEmbed(author) ); @@ -313,7 +313,7 @@ export default new NamedCommand({ finalize(); }; - askMultipleChoice(await channel.send(DST_NOTE_SETUP), author.id, [ + askMultipleChoice(await send(DST_NOTE_SETUP), author.id, [ () => finalizeDST("na"), () => finalizeDST("eu"), () => finalizeDST("sh") @@ -328,9 +328,9 @@ export default new NamedCommand({ }), delete: new NamedCommand({ description: "Delete your timezone information.", - async run({channel, author}) { + async run({send, channel, author}) { prompt( - await channel.send( + await send( "Are you sure you want to delete your timezone information?\n*(This message will automatically be deleted after 10 seconds.)*" ), author.id, @@ -345,10 +345,10 @@ export default new NamedCommand({ }), utc: new NamedCommand({ description: "Displays UTC time.", - async run({channel}) { + async run({send, channel}) { const time = moment().utc(); - channel.send({ + send({ embed: { color: TIME_EMBED_COLOR, fields: [ @@ -377,16 +377,16 @@ export default new NamedCommand({ id: "user", user: new Command({ description: "See what time it is for someone else.", - async run({channel, args}) { - channel.send(getTimeEmbed(args[0])); + async run({send, channel, args}) { + send(getTimeEmbed(args[0])); } }), any: new Command({ description: "See what time it is for someone else (by their username).", - async run({channel, args, guild}) { + async run({send, channel, args, guild}) { const member = await getMemberByName(guild!, args.join(" ")); - if (member instanceof GuildMember) channel.send(getTimeEmbed(member.user)); - else channel.send(member); + if (member instanceof GuildMember) send(getTimeEmbed(member.user)); + else send(member); } }) }); diff --git a/src/commands/utility/todo.ts b/src/commands/utility/todo.ts index 594610b..57961c0 100644 --- a/src/commands/utility/todo.ts +++ b/src/commands/utility/todo.ts @@ -5,7 +5,7 @@ import {MessageEmbed} from "discord.js"; export default new NamedCommand({ description: "Keep and edit your personal todo list.", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const user = Storage.getUser(author.id); const embed = new MessageEmbed().setTitle(`Todo list for ${author.tag}`).setColor("BLUE"); @@ -17,21 +17,21 @@ export default new NamedCommand({ ); } - channel.send(embed); + send(embed); }, subcommands: { add: new NamedCommand({ - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const user = Storage.getUser(author.id); const note = args.join(" "); user.todoList[Date.now().toString()] = note; console.debug(user.todoList); Storage.save(); - channel.send(`Successfully added \`${note}\` to your todo list.`); + send(`Successfully added \`${note}\` to your todo list.`); } }), remove: new NamedCommand({ - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const user = Storage.getUser(author.id); const note = args.join(" "); let isFound = false; @@ -43,19 +43,19 @@ export default new NamedCommand({ delete user.todoList[timestamp]; Storage.save(); isFound = true; - channel.send(`Removed \`${note}\` from your todo list.`); + send(`Removed \`${note}\` from your todo list.`); } } - if (!isFound) channel.send("That item couldn't be found."); + if (!isFound) send("That item couldn't be found."); } }), clear: new NamedCommand({ - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const user = Storage.getUser(author.id); user.todoList = {}; Storage.save(); - channel.send("Cleared todo list."); + send("Cleared todo list."); } }) } diff --git a/src/commands/utility/translate.ts b/src/commands/utility/translate.ts index e0e9250..c026cfb 100644 --- a/src/commands/utility/translate.ts +++ b/src/commands/utility/translate.ts @@ -4,14 +4,14 @@ import translate from "translate-google"; export default new NamedCommand({ description: "Translates your input.", usage: " ", - async run({message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const lang = args[0]; const input = args.slice(1).join(" "); translate(input, { to: lang }) .then((res) => { - channel.send({ + send({ embed: { title: "Translation", fields: [ @@ -29,9 +29,7 @@ export default new NamedCommand({ }) .catch((error) => { console.error(error); - channel.send( - `${error}\nPlease use the following list: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes` - ); + send(`${error}\nPlease use the following list: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes`); }); } }); From 26e0bb58248ceb780964ae9c044491a162bcbf91 Mon Sep 17 00:00:00 2001 From: WatDuhHekBro <44940783+WatDuhHekBro@users.noreply.github.com> Date: Sat, 10 Apr 2021 11:30:27 -0500 Subject: [PATCH 08/14] Added rest subcommand type --- CHANGELOG.md | 3 +- src/commands/system/help.ts | 6 +- src/commands/utility/time.ts | 2 +- src/core/command.ts | 337 +++++++++++++++++++++++------------ src/core/index.ts | 2 +- src/core/loader.ts | 17 +- 6 files changed, 245 insertions(+), 122 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index 2295ec1..c366d51 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -9,8 +9,9 @@ - `urban`: Bug fixes - Changed `help` to display a paginated embed - Various changes to core - - Added `guild` subcommand type (only accessible when `id` is set to `guild`) + - Added `guild` subcommand type (only accessible when `id: "guild"`) - Further reduced `channel.send()` to `send()` because it's used in *every, single, command* + - Added `rest` subcommand type (only available when `endpoint: true`), declaratively states that the following command will do `args.join(" ")`, preventing any other subcommands from being added # 3.2.0 - Internal refactor, more subcommand types, and more command type guards (2021-04-09) - The custom logger changed: `$.log` no longer exists, it's just `console.log`. Now you don't have to do `import $ from "../core/lib"` at the top of every file that uses the custom logger. diff --git a/src/commands/system/help.ts b/src/commands/system/help.ts index 9cf6b8c..8e32c35 100644 --- a/src/commands/system/help.ts +++ b/src/commands/system/help.ts @@ -33,8 +33,9 @@ export default new NamedCommand({ }, any: new Command({ async run({send, message, channel, guild, author, member, client, args}) { - const [result, category] = await getCommandInfo(args); - if (typeof result === "string") return send(result); + const resultingBlob = await getCommandInfo(args); + if (typeof resultingBlob === "string") return send(resultingBlob); + const [result, category] = resultingBlob; let append = ""; const command = result.command; const header = result.args.length > 0 ? `${result.header} ${result.args.join(" ")}` : result.header; @@ -52,6 +53,7 @@ export default new NamedCommand({ list.push(`❯ \`${header} ${type}${customUsage}\` - ${subcommand.description}`); } + if (result.hasRestCommand) list.push(`❯ \`${header} <...>\``); append = list.length > 0 ? list.join("\n") : "None"; } else { append = `\`${header} ${command.usage}\``; diff --git a/src/commands/utility/time.ts b/src/commands/utility/time.ts index 656df97..1196ea5 100644 --- a/src/commands/utility/time.ts +++ b/src/commands/utility/time.ts @@ -280,7 +280,7 @@ export default new NamedCommand({ } } else { // If it's a unique hour, just search through the tuple list and find the matching entry. - for (const [hourPoint, dayOffset, timezoneOffset] of timezoneTupleList) { + for (const [hourPoint, _dayOffset, timezoneOffset] of timezoneTupleList) { if (hour === hourPoint) { profile.timezone = timezoneOffset; } diff --git a/src/core/command.ts b/src/core/command.ts index d38d78a..703ca9d 100644 --- a/src/core/command.ts +++ b/src/core/command.ts @@ -78,11 +78,12 @@ interface CommandOptionsBase { readonly permission?: number; readonly nsfw?: boolean; readonly channelType?: CHANNEL_TYPE; - readonly run?: (($: CommandMenu) => Promise) | string; } interface CommandOptionsEndpoint { readonly endpoint: true; + readonly rest?: RestCommand; + readonly run?: (($: CommandMenu) => Promise) | string; } // Prevents subcommands from being added by compile-time. @@ -100,10 +101,15 @@ interface CommandOptionsNonEndpoint { readonly id?: ID; readonly number?: Command; readonly any?: Command; + readonly rest?: undefined; // Redeclare it here as undefined to prevent its use otherwise. + readonly run?: (($: CommandMenu) => Promise) | string; } type CommandOptions = CommandOptionsBase & (CommandOptionsEndpoint | CommandOptionsNonEndpoint); -type NamedCommandOptions = CommandOptions & {aliases?: string[]}; +type NamedCommandOptions = CommandOptions & {aliases?: string[]; nameOverride?: string}; +type RestCommandOptions = CommandOptionsBase & { + run?: (($: CommandMenu & {readonly combined: string}) => Promise) | string; +}; interface ExecuteCommandMetadata { readonly header: string; @@ -116,9 +122,10 @@ interface ExecuteCommandMetadata { export interface CommandInfo { readonly type: "info"; - readonly command: Command; + readonly command: BaseCommand; readonly subcommandInfo: Collection; readonly keyedSubcommandInfo: Collection; + readonly hasRestCommand: boolean; readonly permission: number; readonly nsfw: boolean; readonly channelType: CHANNEL_TYPE; @@ -141,14 +148,26 @@ interface CommandInfoMetadata { readonly header: string; } -// Each Command instance represents a block that links other Command instances under it. -export class Command { +// An isolated command of just the metadata. +abstract class BaseCommand { public readonly description: string; - public readonly endpoint: boolean; public readonly usage: string; public readonly permission: number; // -1 (default) indicates to inherit, 0 is the lowest rank, 1 is second lowest rank, and so on. public readonly nsfw: boolean | null; // null (default) indicates to inherit public readonly channelType: CHANNEL_TYPE | null; // null (default) indicates to inherit + + constructor(options?: CommandOptionsBase) { + this.description = options?.description || "No description."; + this.usage = options?.usage ?? ""; + this.permission = options?.permission ?? -1; + this.nsfw = options?.nsfw ?? null; + this.channelType = options?.channelType ?? null; + } +} + +// Each Command instance represents a block that links other Command instances under it. +export class Command extends BaseCommand { + public readonly endpoint: boolean; // The execute and subcommand properties are restricted to the class because subcommand recursion could easily break when manually handled. // The class will handle checking for null fields. private run: (($: CommandMenu) => Promise) | string; @@ -163,14 +182,11 @@ export class Command { private idType: ID | null; private number: Command | null; private any: Command | null; + private rest: RestCommand | null; constructor(options?: CommandOptions) { - this.description = options?.description || "No description."; + super(options); this.endpoint = !!options?.endpoint; - this.usage = options?.usage ?? ""; - this.permission = options?.permission ?? -1; - this.nsfw = options?.nsfw ?? null; - this.channelType = options?.channelType ?? null; this.run = options?.run || "No action was set on this command!"; this.subcommands = new Collection(); // Populate this collection after setting subcommands. this.channel = null; @@ -183,44 +199,45 @@ export class Command { this.idType = null; this.number = null; this.any = null; + this.rest = null; if (options && !options.endpoint) { - if (options?.channel) this.channel = options.channel; - if (options?.role) this.role = options.role; - if (options?.emote) this.emote = options.emote; - if (options?.message) this.message = options.message; - if (options?.user) this.user = options.user; - if (options?.guild) this.guild = options.guild; - if (options?.number) this.number = options.number; - if (options?.any) this.any = options.any; - if (options?.id) this.idType = options.id; + if (options.channel) this.channel = options.channel; + if (options.role) this.role = options.role; + if (options.emote) this.emote = options.emote; + if (options.message) this.message = options.message; + if (options.user) this.user = options.user; + if (options.guild) this.guild = options.guild; + if (options.number) this.number = options.number; + if (options.any) this.any = options.any; + if (options.id) this.idType = options.id; - if (options?.id) { - switch (options.id) { - case "channel": - this.id = this.channel; - break; - case "role": - this.id = this.role; - break; - case "emote": - this.id = this.emote; - break; - case "message": - this.id = this.message; - break; - case "user": - this.id = this.user; - break; - case "guild": - this.id = this.guild; - break; - default: - requireAllCasesHandledFor(options.id); - } + switch (options.id) { + case "channel": + this.id = this.channel; + break; + case "role": + this.id = this.role; + break; + case "emote": + this.id = this.emote; + break; + case "message": + this.id = this.message; + break; + case "user": + this.id = this.user; + break; + case "guild": + this.id = this.guild; + break; + case undefined: + break; + default: + requireAllCasesHandledFor(options.id); } - if (options?.subcommands) { + if (options.subcommands) { const baseSubcommands = Object.keys(options.subcommands); // Loop once to set the base subcommands. @@ -246,6 +263,8 @@ export class Command { } } } + } else if (options && options.endpoint) { + if (options.rest) this.rest = options.rest; } } @@ -273,72 +292,49 @@ export class Command { // If there are no arguments left, execute the current command. Otherwise, continue on. if (param === undefined) { - // See if there is anything that'll prevent the user from executing the command. + const error = canExecute(menu, metadata); + if (error) return error; - // 1. Does this command specify a required channel type? If so, does the channel type match? - if ( - metadata.channelType === CHANNEL_TYPE.GUILD && - (!(menu.channel instanceof GuildChannel) || menu.guild === null || menu.member === null) - ) { - return {content: "This command must be executed in a server."}; - } else if ( - metadata.channelType === CHANNEL_TYPE.DM && - (menu.channel.type !== "dm" || menu.guild !== null || menu.member !== null) - ) { - return {content: "This command must be executed as a direct message."}; - } - - // 2. Is this an NSFW command where the channel prevents such use? (DM channels bypass this requirement.) - if (metadata.nsfw && menu.channel.type !== "dm" && !menu.channel.nsfw) { - return {content: "This command must be executed in either an NSFW channel or as a direct message."}; - } - - // 3. Does the user have permission to execute the command? - if (!hasPermission(menu.author, menu.member, metadata.permission)) { - const userPermLevel = getPermissionLevel(menu.author, menu.member); - - return { - content: `You don't have access to this command! Your permission level is \`${getPermissionName( - userPermLevel - )}\` (${userPermLevel}), but this command requires a permission level of \`${getPermissionName( - metadata.permission - )}\` (${metadata.permission}).` - }; - } - - // Then capture any potential errors. - try { - if (typeof this.run === "string") { - // Although I *could* add an option in the launcher to attach arbitrary variables to this var string... - // I'll just leave it like this, because instead of using var strings for user stuff, you could just make "run" a template string. - await menu.send( - parseVars( - this.run, - { - author: menu.author.toString(), - prefix: getPrefix(menu.guild), - command: `${metadata.header} ${metadata.symbolicArgs.join(", ")}` - }, - "???" - ) - ); - } else { + if (typeof this.run === "string") { + // Although I *could* add an option in the launcher to attach arbitrary variables to this var string... + // I'll just leave it like this, because instead of using var strings for user stuff, you could just make "run" a template string. + await menu.send( + parseVars( + this.run, + { + author: menu.author.toString(), + prefix: getPrefix(menu.guild), + command: `${metadata.header} ${metadata.symbolicArgs.join(", ")}` + }, + "???" + ) + ); + } else { + // Then capture any potential errors. + try { await this.run(menu); + } catch (error) { + const errorMessage = error.stack ?? error; + console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`); + + return { + content: `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\`` + }; } - - return null; - } catch (error) { - const errorMessage = error.stack ?? error; - console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`); - - return { - content: `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\`` - }; } + + return null; } - // If the current command is an endpoint but there are still some arguments left, don't continue. - if (this.endpoint) return {content: "Too many arguments!"}; + // If the current command is an endpoint but there are still some arguments left, don't continue unless there's a RestCommand. + if (this.endpoint) { + if (this.rest) { + args.unshift(param); + return this.rest.execute(args.join(" "), menu, metadata); + } else { + return {content: "Too many arguments!"}; + } + } // Resolve the value of the current command's argument (adding it to the resolved args), // then pass the thread of execution to whichever subcommand is valid (if any). @@ -532,7 +528,7 @@ export class Command { } // What this does is resolve the resulting subcommand as well as the inherited properties and the available subcommands. - public async resolveInfo(args: string[], header: string): Promise { + public resolveInfo(args: string[], header: string): CommandInfo | CommandInfoError { return this.resolveInfoInternal(args, { permission: 0, nsfw: false, @@ -544,10 +540,7 @@ export class Command { }); } - private async resolveInfoInternal( - args: string[], - metadata: CommandInfoMetadata - ): Promise { + private resolveInfoInternal(args: string[], metadata: CommandInfoMetadata): CommandInfo | CommandInfoError { // Update inherited properties if the current command specifies a property. // In case there are no initial arguments, these should go first so that it can register. if (this.permission !== -1) metadata.permission = this.permission; @@ -586,6 +579,7 @@ export class Command { command: this, keyedSubcommandInfo, subcommandInfo, + hasRestCommand: !!this.rest, ...metadata }; } @@ -652,6 +646,13 @@ export class Command { } else { return invalidSubcommandGenerator(); } + } else if (param === "<...>") { + if (this.rest) { + metadata.args.push("<...>"); + return this.rest.resolveInfoFinale(metadata); + } else { + return invalidSubcommandGenerator(); + } } else if (this.subcommands?.has(param)) { metadata.args.push(param); return this.subcommands.get(param)!.resolveInfoInternal(args, metadata); @@ -668,7 +669,8 @@ export class NamedCommand extends Command { constructor(options?: NamedCommandOptions) { super(options); this.aliases = options?.aliases || []; - this.originalCommandName = null; + // The name override exists in case a user wants to bypass filename restrictions. + this.originalCommandName = options?.nameOverride ?? null; } public get name(): string { @@ -681,4 +683,119 @@ export class NamedCommand extends Command { throw new Error(`originalCommandName cannot be set twice! Attempted to set the value to "${value}".`); else this.originalCommandName = value; } + + public isNameSet(): boolean { + return this.originalCommandName !== null; + } +} + +// RestCommand is a declarative version of the common "any: args.join(' ')" pattern, basically the Command version of a rest parameter. +// This way, you avoid having extra subcommands when using this pattern. +// I'm probably not going to add a transformer function (a callback to automatically handle stuff like searching for usernames). +// I don't think the effort to figure this part out via generics or something is worth it. +export class RestCommand extends BaseCommand { + private run: (($: CommandMenu & {readonly combined: string}) => Promise) | string; + + constructor(options?: RestCommandOptions) { + super(options); + this.run = options?.run || "No action was set on this command!"; + } + + public async execute( + combined: string, + menu: CommandMenu, + metadata: ExecuteCommandMetadata + ): Promise { + // Update inherited properties if the current command specifies a property. + // In case there are no initial arguments, these should go first so that it can register. + if (this.permission !== -1) metadata.permission = this.permission; + if (this.nsfw !== null) metadata.nsfw = this.nsfw; + if (this.channelType !== null) metadata.channelType = this.channelType; + + const error = canExecute(menu, metadata); + if (error) return error; + + if (typeof this.run === "string") { + // Although I *could* add an option in the launcher to attach arbitrary variables to this var string... + // I'll just leave it like this, because instead of using var strings for user stuff, you could just make "run" a template string. + await menu.send( + parseVars( + this.run, + { + author: menu.author.toString(), + prefix: getPrefix(menu.guild), + command: `${metadata.header} ${metadata.symbolicArgs.join(", ")}` + }, + "???" + ) + ); + } else { + // Then capture any potential errors. + try { + await this.run({...menu, combined}); + } catch (error) { + const errorMessage = error.stack ?? error; + console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`); + + return { + content: `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\`` + }; + } + } + + return null; + } + + public resolveInfoFinale(metadata: CommandInfoMetadata): CommandInfo { + if (this.permission !== -1) metadata.permission = this.permission; + if (this.nsfw !== null) metadata.nsfw = this.nsfw; + if (this.channelType !== null) metadata.channelType = this.channelType; + if (this.usage !== "") metadata.usage = this.usage; + + return { + type: "info", + command: this, + keyedSubcommandInfo: new Collection(), + subcommandInfo: new Collection(), + hasRestCommand: false, + ...metadata + }; + } +} + +// See if there is anything that'll prevent the user from executing the command. +// Returns null if successful, otherwise returns a message with the error. +function canExecute(menu: CommandMenu, metadata: ExecuteCommandMetadata): SingleMessageOptions | null { + // 1. Does this command specify a required channel type? If so, does the channel type match? + if ( + metadata.channelType === CHANNEL_TYPE.GUILD && + (!(menu.channel instanceof GuildChannel) || menu.guild === null || menu.member === null) + ) { + return {content: "This command must be executed in a server."}; + } else if ( + metadata.channelType === CHANNEL_TYPE.DM && + (menu.channel.type !== "dm" || menu.guild !== null || menu.member !== null) + ) { + return {content: "This command must be executed as a direct message."}; + } + + // 2. Is this an NSFW command where the channel prevents such use? (DM channels bypass this requirement.) + if (metadata.nsfw && menu.channel.type !== "dm" && !menu.channel.nsfw) { + return {content: "This command must be executed in either an NSFW channel or as a direct message."}; + } + + // 3. Does the user have permission to execute the command? + if (!hasPermission(menu.author, menu.member, metadata.permission)) { + const userPermLevel = getPermissionLevel(menu.author, menu.member); + + return { + content: `You don't have access to this command! Your permission level is \`${getPermissionName( + userPermLevel + )}\` (${userPermLevel}), but this command requires a permission level of \`${getPermissionName( + metadata.permission + )}\` (${metadata.permission}).` + }; + } + + return null; } diff --git a/src/core/index.ts b/src/core/index.ts index 16d1116..f80074a 100644 --- a/src/core/index.ts +++ b/src/core/index.ts @@ -1,5 +1,5 @@ // Onion Lasers Command Handler // -export {Command, NamedCommand, CHANNEL_TYPE} from "./command"; +export {Command, NamedCommand, RestCommand, CHANNEL_TYPE} from "./command"; export {addInterceptRule} from "./handler"; export {launch} from "./interface"; export * from "./libd"; diff --git a/src/core/loader.ts b/src/core/loader.ts index cabb0fd..9939519 100644 --- a/src/core/loader.ts +++ b/src/core/loader.ts @@ -43,14 +43,16 @@ export async function loadCommands(commandsDir: string): Promise Date: Sat, 10 Apr 2021 12:07:55 -0500 Subject: [PATCH 09/14] Reduced clunkiness of rest type and applied changes to commands --- src/commands/fun/eco.ts | 8 ++-- src/commands/fun/figlet.ts | 24 ++++++------ src/commands/fun/modules/eco-core.ts | 21 +++-------- src/commands/fun/modules/eco-shop.ts | 52 +++++++++++++------------- src/commands/fun/owoify.ts | 15 +++++--- src/commands/fun/poll.ts | 8 ++-- src/commands/fun/urban.ts | 8 ++-- src/commands/fun/vaporwave.ts | 8 ++-- src/commands/fun/weather.ts | 8 ++-- src/commands/fun/whois.ts | 9 ++--- src/commands/system/admin.ts | 56 ++++++++++++++++------------ src/commands/system/help.ts | 1 - src/commands/utility/calc.ts | 32 ++++++++-------- src/commands/utility/desc.ts | 26 +++++++------ src/commands/utility/info.ts | 15 ++++---- src/commands/utility/say.ts | 8 ++-- src/commands/utility/time.ts | 17 +++++++-- src/commands/utility/todo.ts | 50 +++++++++++++------------ src/core/command.ts | 55 ++++++++++++--------------- 19 files changed, 217 insertions(+), 204 deletions(-) diff --git a/src/commands/fun/eco.ts b/src/commands/fun/eco.ts index 1572151..96c1b43 100644 --- a/src/commands/fun/eco.ts +++ b/src/commands/fun/eco.ts @@ -1,4 +1,4 @@ -import {Command, NamedCommand, getMemberByName} from "../../core"; +import {Command, NamedCommand, getMemberByName, RestCommand} 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"; @@ -33,11 +33,11 @@ export default new NamedCommand({ if (isAuthorized(guild, channel)) send(getMoneyEmbed(args[0])); } }), - any: new Command({ + any: new RestCommand({ description: "See how much money someone else has by using their username.", - async run({send, guild, channel, args, message}) { + async run({send, guild, channel, args, message, combined}) { if (isAuthorized(guild, channel)) { - const member = await getMemberByName(guild!, args.join(" ")); + const member = await getMemberByName(guild!, combined); if (member instanceof GuildMember) send(getMoneyEmbed(member.user)); else send(member); } diff --git a/src/commands/fun/figlet.ts b/src/commands/fun/figlet.ts index 0ae2538..a25ed2a 100644 --- a/src/commands/fun/figlet.ts +++ b/src/commands/fun/figlet.ts @@ -1,17 +1,19 @@ -import {Command, NamedCommand} from "../../core"; +import {Command, NamedCommand, RestCommand} from "../../core"; import figlet from "figlet"; export default new NamedCommand({ description: "Generates a figlet of your input.", - async run({send, message, channel, guild, author, member, client, args}) { - const input = args.join(" "); - if (!args[0]) return send("You have to provide input for me to create a figlet!"); - return send( - "```" + - figlet.textSync(`${input}`, { + run: "You have to provide input for me to create a figlet!", + any: new RestCommand({ + async run({send, message, channel, guild, author, member, client, args, combined}) { + return send( + figlet.textSync(combined, { horizontalLayout: "full" - }) + - "```" - ); - } + }), + { + code: true + } + ); + } + }) }); diff --git a/src/commands/fun/modules/eco-core.ts b/src/commands/fun/modules/eco-core.ts index 4bb5808..390c268 100644 --- a/src/commands/fun/modules/eco-core.ts +++ b/src/commands/fun/modules/eco-core.ts @@ -1,4 +1,5 @@ -import {Command, NamedCommand, prompt} from "../../../core"; +import {GuildMember} from "discord.js"; +import {Command, getMemberByName, NamedCommand, prompt, RestCommand} from "../../../core"; import {pluralise} from "../../../lib"; import {Storage} from "../../../structures"; import {isAuthorized, getMoneyEmbed, getSendEmbed, ECO_EMBED_COLOR} from "./eco-utils"; @@ -140,8 +141,8 @@ export const PayCommand = new NamedCommand({ number: new Command({ run: "You must use the format `eco pay `!" }), - any: new Command({ - async run({send, args, author, channel, guild}) { + any: new RestCommand({ + async run({send, args, author, channel, guild, combined}) { if (isAuthorized(guild, channel)) { const last = args.pop(); @@ -156,18 +157,8 @@ export const PayCommand = new NamedCommand({ else if (!guild) return send("You have to use this in a server if you want to send Mons with a username!"); - const username = args.join(" "); - const member = ( - await guild.members.fetch({ - query: username, - limit: 1 - }) - ).first(); - - if (!member) - return send( - `Couldn't find a user by the name of \`${username}\`! If you want to send Mons to someone in a different server, you have to use their user ID!` - ); + const member = await getMemberByName(guild, combined); + if (!(member instanceof GuildMember)) return send(member); else if (member.user.id === author.id) return send("You can't send Mons to yourself!"); else if (member.user.bot && process.argv[2] !== "dev") return send("You can't send Mons to a bot!"); diff --git a/src/commands/fun/modules/eco-shop.ts b/src/commands/fun/modules/eco-shop.ts index b76c47d..acd0e4a 100644 --- a/src/commands/fun/modules/eco-shop.ts +++ b/src/commands/fun/modules/eco-shop.ts @@ -1,4 +1,4 @@ -import {Command, NamedCommand, paginate} from "../../../core"; +import {Command, NamedCommand, paginate, RestCommand} from "../../../core"; import {pluralise, split} from "../../../lib"; import {Storage, getPrefix} from "../../../structures"; import {isAuthorized, ECO_EMBED_COLOR} from "./eco-utils"; @@ -47,37 +47,37 @@ export const ShopCommand = new NamedCommand({ export const BuyCommand = new NamedCommand({ description: "Buys an item from the shop.", usage: "", - async run({send, guild, channel, args, message, author}) { - if (isAuthorized(guild, channel)) { - let found = false; + run: "You need to specify an item to buy.", + any: new RestCommand({ + async run({send, guild, channel, args, message, author, combined}) { + if (isAuthorized(guild, channel)) { + let found = false; + let amount = 1; // The amount the user is buying. - let amount = 1; // The amount the user is buying. + // For now, no shop items support being bought multiple times. Uncomment these 2 lines when it's supported/needed. + //if (/\d+/g.test(args[args.length - 1])) + //amount = parseInt(args.pop()); - // For now, no shop items support being bought multiple times. Uncomment these 2 lines when it's supported/needed. - //if (/\d+/g.test(args[args.length - 1])) - //amount = parseInt(args.pop()); + for (let item of ShopItems) { + if (item.usage === combined) { + const user = Storage.getUser(author.id); + const cost = item.cost * amount; - let requested = args.join(" "); // The item the user is buying. + if (cost > user.money) { + send("Not enough Mons!"); + } else { + user.money -= cost; + Storage.save(); + item.run(message, cost, amount); + } - for (let item of ShopItems) { - if (item.usage === requested) { - const user = Storage.getUser(author.id); - const cost = item.cost * amount; - - if (cost > user.money) { - send("Not enough Mons!"); - } else { - user.money -= cost; - Storage.save(); - item.run(message, cost, amount); + found = true; + break; } - - found = true; - break; } - } - if (!found) send(`There's no item in the shop that goes by \`${requested}\`!`); + if (!found) send(`There's no item in the shop that goes by \`${combined}\`!`); + } } - } + }) }); diff --git a/src/commands/fun/owoify.ts b/src/commands/fun/owoify.ts index e9e59d9..cbc4ccf 100644 --- a/src/commands/fun/owoify.ts +++ b/src/commands/fun/owoify.ts @@ -1,12 +1,15 @@ -import {Command, NamedCommand} from "../../core"; +import {Command, NamedCommand, RestCommand} from "../../core"; import {getContent} from "../../lib"; import {URL} from "url"; export default new NamedCommand({ description: "OwO-ifies the input.", - async run({send, message, channel, guild, author, member, client, args}) { - let url = new URL(`https://nekos.life/api/v2/owoify?text=${args.join(" ")}`); - const content = (await getContent(url.toString())) as any; // Apparently, the object in question is {owo: string}. - send(content.owo); - } + run: "You need to specify some text to owoify.", + any: new RestCommand({ + async run({send, message, channel, guild, author, member, client, args, combined}) { + let url = new URL(`https://nekos.life/api/v2/owoify?text=${combined}`); + const content = (await getContent(url.toString())) as any; // Apparently, the object in question is {owo: string}. + send(content.owo); + } + }) }); diff --git a/src/commands/fun/poll.ts b/src/commands/fun/poll.ts index 3d60e1e..5795db1 100644 --- a/src/commands/fun/poll.ts +++ b/src/commands/fun/poll.ts @@ -1,13 +1,13 @@ import {MessageEmbed} from "discord.js"; -import {Command, NamedCommand} from "../../core"; +import {Command, NamedCommand, RestCommand} from "../../core"; export default new NamedCommand({ description: "Create a poll.", usage: "", run: "Please provide a question.", - any: new Command({ + any: new RestCommand({ description: "Question for the poll.", - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, author, member, client, args, combined}) { const embed = new MessageEmbed() .setAuthor( `Poll created by ${message.author.username}`, @@ -15,7 +15,7 @@ export default new NamedCommand({ ) .setColor(0xffffff) .setFooter("React to vote.") - .setDescription(args.join(" ")); + .setDescription(combined); const msg = await send(embed); await msg.react("✅"); await msg.react("⛔"); diff --git a/src/commands/fun/urban.ts b/src/commands/fun/urban.ts index 44d5a64..928ca57 100644 --- a/src/commands/fun/urban.ts +++ b/src/commands/fun/urban.ts @@ -1,14 +1,14 @@ -import {Command, NamedCommand} from "../../core"; +import {Command, NamedCommand, RestCommand} from "../../core"; import {MessageEmbed} from "discord.js"; import urban from "relevant-urban"; export default new NamedCommand({ description: "Gives you a definition of the inputted word.", run: "Please input a word.", - any: new Command({ - async run({send, message, channel, guild, author, member, client, args}) { + any: new RestCommand({ + async run({send, message, channel, guild, author, member, client, args, combined}) { // [Bug Fix]: Use encodeURIComponent() when emojis are used: "TypeError [ERR_UNESCAPED_CHARACTERS]: Request path contains unescaped characters" - urban(encodeURIComponent(args.join(" "))) + urban(encodeURIComponent(combined)) .then((res) => { const embed = new MessageEmbed() .setColor(0x1d2439) diff --git a/src/commands/fun/vaporwave.ts b/src/commands/fun/vaporwave.ts index e2c1727..71ae065 100644 --- a/src/commands/fun/vaporwave.ts +++ b/src/commands/fun/vaporwave.ts @@ -1,4 +1,4 @@ -import {Command, NamedCommand} from "../../core"; +import {Command, NamedCommand, RestCommand} from "../../core"; const vaporwave = (() => { const map = new Map(); @@ -24,9 +24,9 @@ function getVaporwaveText(text: string): string { export default new NamedCommand({ description: "Transforms your text into vaporwave.", run: "You need to enter some text!", - any: new Command({ - async run({send, message, channel, guild, author, member, client, args}) { - const text = getVaporwaveText(args.join(" ")); + any: new RestCommand({ + async run({send, message, channel, guild, author, member, client, args, combined}) { + const text = getVaporwaveText(combined); if (text !== "") send(text); else send("Make sure to enter at least one valid character."); } diff --git a/src/commands/fun/weather.ts b/src/commands/fun/weather.ts index 6760286..44c82b7 100644 --- a/src/commands/fun/weather.ts +++ b/src/commands/fun/weather.ts @@ -1,15 +1,15 @@ -import {Command, NamedCommand} from "../../core"; +import {Command, NamedCommand, RestCommand} from "../../core"; import {MessageEmbed} from "discord.js"; import {find} from "weather-js"; export default new NamedCommand({ description: "Shows weather info of specified location.", run: "You need to provide a city.", - any: new Command({ - async run({send, message, channel, guild, author, member, client, args}) { + any: new RestCommand({ + async run({send, message, channel, guild, author, member, client, args, combined}) { find( { - search: args.join(" "), + search: combined, degreeType: "C" }, function (error, result) { diff --git a/src/commands/fun/whois.ts b/src/commands/fun/whois.ts index aea91c7..4ad9c97 100644 --- a/src/commands/fun/whois.ts +++ b/src/commands/fun/whois.ts @@ -1,5 +1,5 @@ import {User, GuildMember} from "discord.js"; -import {Command, NamedCommand, getMemberByName, CHANNEL_TYPE} from "../../core"; +import {Command, NamedCommand, getMemberByName, CHANNEL_TYPE, RestCommand} from "../../core"; // Quotes must be used here or the numbers will change const registry: {[id: string]: string} = { @@ -65,11 +65,10 @@ export default new NamedCommand({ } } }), - any: new Command({ + any: new RestCommand({ channelType: CHANNEL_TYPE.GUILD, - async run({send, message, channel, guild, author, client, args}) { - const query = args.join(" ") as string; - const member = await getMemberByName(guild!, query); + async run({send, message, channel, guild, author, client, args, combined}) { + const member = await getMemberByName(guild!, combined); if (member instanceof GuildMember) { if (member.id in registry) { diff --git a/src/commands/system/admin.ts b/src/commands/system/admin.ts index 6b2593a..b49e890 100644 --- a/src/commands/system/admin.ts +++ b/src/commands/system/admin.ts @@ -1,4 +1,12 @@ -import {Command, NamedCommand, botHasPermission, getPermissionLevel, getPermissionName, CHANNEL_TYPE} from "../../core"; +import { + Command, + NamedCommand, + botHasPermission, + getPermissionLevel, + getPermissionName, + CHANNEL_TYPE, + RestCommand +} from "../../core"; import {clean} from "../../lib"; import {Config, Storage} from "../../structures"; import {Permissions, TextChannel, User} from "discord.js"; @@ -119,12 +127,11 @@ export default new NamedCommand({ Storage.save(); send("Reset your server's welcome message to the default."); }, - any: new Command({ - async run({send, message, channel, guild, author, member, client, args}) { - const newMessage = args.join(" "); - Storage.getGuild(guild!.id).welcomeMessage = newMessage; + any: new RestCommand({ + async run({send, message, channel, guild, author, member, client, args, combined}) { + Storage.getGuild(guild!.id).welcomeMessage = combined; Storage.save(); - send(`Set your server's welcome message to \`${newMessage}\`.`); + send(`Set your server's welcome message to \`${combined}\`.`); } }) }) @@ -241,29 +248,32 @@ export default new NamedCommand({ description: "Evaluate code.", usage: "", permission: PERMISSIONS.BOT_OWNER, - // You have to bring everything into scope to use them. AFAIK, there isn't a more maintainable way to do this, but at least TS will let you know if anything gets removed. - async run({send, message, channel, guild, author, member, client, args}) { - try { - const code = args.join(" "); - let evaled = eval(code); - - if (typeof evaled !== "string") evaled = require("util").inspect(evaled); - send(clean(evaled), {code: "js", split: true}); - } catch (err) { - send(clean(err), {code: "js", split: true}); + run: "You have to enter some code to execute first.", + any: new RestCommand({ + // You have to bring everything into scope to use them. AFAIK, there isn't a more maintainable way to do this, but at least TS will let you know if anything gets removed. + async run({send, message, channel, guild, author, member, client, args, combined}) { + try { + let evaled = eval(combined); + if (typeof evaled !== "string") evaled = require("util").inspect(evaled); + send(clean(evaled), {code: "js", split: true}); + } catch (err) { + send(clean(err), {code: "js", split: true}); + } } - } + }) }), nick: new NamedCommand({ description: "Change the bot's nickname.", permission: PERMISSIONS.BOT_SUPPORT, channelType: CHANNEL_TYPE.GUILD, - async run({send, message, channel, guild, author, member, client, args}) { - const nickName = args.join(" "); - await guild!.me?.setNickname(nickName); - if (botHasPermission(guild, Permissions.FLAGS.MANAGE_MESSAGES)) message.delete({timeout: 5000}); - send(`Nickname set to \`${nickName}\``).then((m) => m.delete({timeout: 5000})); - } + run: "You have to specify a nickname to set for the bot", + any: new RestCommand({ + async run({send, message, channel, guild, author, member, client, args, combined}) { + await guild!.me?.setNickname(combined); + if (botHasPermission(guild, Permissions.FLAGS.MANAGE_MESSAGES)) message.delete({timeout: 5000}); + send(`Nickname set to \`${combined}\``).then((m) => m.delete({timeout: 5000})); + } + }) }), guilds: new NamedCommand({ description: "Shows a list of all guilds the bot is a member of.", diff --git a/src/commands/system/help.ts b/src/commands/system/help.ts index 8e32c35..7691a5a 100644 --- a/src/commands/system/help.ts +++ b/src/commands/system/help.ts @@ -53,7 +53,6 @@ export default new NamedCommand({ list.push(`❯ \`${header} ${type}${customUsage}\` - ${subcommand.description}`); } - if (result.hasRestCommand) list.push(`❯ \`${header} <...>\``); append = list.length > 0 ? list.join("\n") : "None"; } else { append = `\`${header} ${command.usage}\``; diff --git a/src/commands/utility/calc.ts b/src/commands/utility/calc.ts index e31940d..ac0727d 100644 --- a/src/commands/utility/calc.ts +++ b/src/commands/utility/calc.ts @@ -1,22 +1,24 @@ -import {Command, NamedCommand} from "../../core"; +import {Command, NamedCommand, RestCommand} from "../../core"; import * as math from "mathjs"; import {MessageEmbed} from "discord.js"; export default new NamedCommand({ description: "Calculates a specified math expression.", - async run({send, message, channel, guild, author, member, client, args}) { - if (!args[0]) return send("Please provide a calculation."); - let resp; - try { - resp = math.evaluate(args.join(" ")); - } catch (e) { - return send("Please provide a *valid* calculation."); + run: "Please provide a calculation.", + any: new RestCommand({ + async run({send, message, channel, guild, author, member, client, args, combined}) { + let resp; + try { + resp = math.evaluate(combined); + } catch (e) { + return send("Please provide a *valid* calculation."); + } + const embed = new MessageEmbed() + .setColor(0xffffff) + .setTitle("Math Calculation") + .addField("Input", `\`\`\`js\n${combined}\`\`\``) + .addField("Output", `\`\`\`js\n${resp}\`\`\``); + return send(embed); } - const embed = new MessageEmbed() - .setColor(0xffffff) - .setTitle("Math Calculation") - .addField("Input", `\`\`\`js\n${args.join("")}\`\`\``) - .addField("Output", `\`\`\`js\n${resp}\`\`\``); - return send(embed); - } + }) }); diff --git a/src/commands/utility/desc.ts b/src/commands/utility/desc.ts index 773b997..9930df3 100644 --- a/src/commands/utility/desc.ts +++ b/src/commands/utility/desc.ts @@ -1,19 +1,21 @@ -import {Command, NamedCommand} from "../../core"; +import {Command, NamedCommand, RestCommand} from "../../core"; export default new NamedCommand({ description: "Renames current voice channel.", usage: "", - async run({send, message, channel, guild, author, member, client, args}) { - const voiceChannel = message.member?.voice.channel; + run: "Please provide a new voice channel name.", + any: new RestCommand({ + async run({send, message, channel, guild, author, member, client, args, combined}) { + const voiceChannel = message.member?.voice.channel; - if (!voiceChannel) return send("You are not in a voice channel."); - if (!voiceChannel.guild.me?.hasPermission("MANAGE_CHANNELS")) - return send("I am lacking the required permissions to perform this action."); - if (args.length === 0) return send("Please provide a new voice channel name."); + if (!voiceChannel) return send("You are not in a voice channel."); + if (!voiceChannel.guild.me?.hasPermission("MANAGE_CHANNELS")) + return send("I am lacking the required permissions to perform this action."); - const prevName = voiceChannel.name; - const newName = args.join(" "); - await voiceChannel.setName(newName); - return await send(`Changed channel name from "${prevName}" to "${newName}".`); - } + const prevName = voiceChannel.name; + const newName = combined; + await voiceChannel.setName(newName); + return await send(`Changed channel name from "${prevName}" to "${newName}".`); + } + }) }); diff --git a/src/commands/utility/info.ts b/src/commands/utility/info.ts index fff17c5..5e51363 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, getMemberByName, CHANNEL_TYPE, getGuildByName} from "../../core"; +import {Command, NamedCommand, getMemberByName, CHANNEL_TYPE, getGuildByName, RestCommand} from "../../core"; import {formatBytes, trimArray} from "../../lib"; import {verificationLevels, filterLevels, regions} from "../../defs/info"; import moment, {utc} from "moment"; @@ -30,12 +30,11 @@ export default new NamedCommand({ ); } }), - any: new Command({ + any: new RestCommand({ description: "Shows another user's avatar by searching their name", channelType: CHANNEL_TYPE.GUILD, - async run({send, message, channel, guild, author, client, args}) { - const name = args.join(" "); - const member = await getMemberByName(guild!, name); + async run({send, message, channel, guild, author, client, args, combined}) { + const member = await getMemberByName(guild!, combined); if (member instanceof GuildMember) { send( @@ -106,10 +105,10 @@ export default new NamedCommand({ send(await getGuildInfo(targetGuild, guild)); } }), - any: new Command({ + any: new RestCommand({ description: "Display info about a guild by finding its name.", - async run({send, message, channel, guild, author, member, client, args}) { - const targetGuild = getGuildByName(args.join(" ")); + async run({send, message, channel, guild, author, member, client, args, combined}) { + const targetGuild = getGuildByName(combined); if (targetGuild instanceof Guild) { send(await getGuildInfo(targetGuild, guild)); diff --git a/src/commands/utility/say.ts b/src/commands/utility/say.ts index 02067a1..0ffe639 100644 --- a/src/commands/utility/say.ts +++ b/src/commands/utility/say.ts @@ -1,13 +1,13 @@ -import {Command, NamedCommand} from "../../core"; +import {Command, NamedCommand, RestCommand} from "../../core"; export default new NamedCommand({ description: "Repeats your message.", usage: "", run: "Please provide a message for me to say!", - any: new Command({ + any: new RestCommand({ description: "Message to repeat.", - async run({send, message, channel, guild, author, member, client, args}) { - send(`*${author} says:*\n${args.join(" ")}`); + async run({send, message, channel, guild, author, member, client, args, combined}) { + send(`*${author} says:*\n${combined}`); } }) }); diff --git a/src/commands/utility/time.ts b/src/commands/utility/time.ts index 1196ea5..7598f5a 100644 --- a/src/commands/utility/time.ts +++ b/src/commands/utility/time.ts @@ -1,4 +1,13 @@ -import {Command, NamedCommand, ask, askYesOrNo, askMultipleChoice, prompt, getMemberByName} from "../../core"; +import { + Command, + NamedCommand, + ask, + askYesOrNo, + askMultipleChoice, + prompt, + getMemberByName, + RestCommand +} from "../../core"; import {Storage} from "../../structures"; import {User, GuildMember} from "discord.js"; import moment from "moment"; @@ -381,10 +390,10 @@ export default new NamedCommand({ send(getTimeEmbed(args[0])); } }), - any: new Command({ + any: new RestCommand({ description: "See what time it is for someone else (by their username).", - async run({send, channel, args, guild}) { - const member = await getMemberByName(guild!, args.join(" ")); + async run({send, channel, args, guild, combined}) { + const member = await getMemberByName(guild!, combined); if (member instanceof GuildMember) send(getTimeEmbed(member.user)); else send(member); } diff --git a/src/commands/utility/todo.ts b/src/commands/utility/todo.ts index 57961c0..9057166 100644 --- a/src/commands/utility/todo.ts +++ b/src/commands/utility/todo.ts @@ -1,4 +1,4 @@ -import {Command, NamedCommand} from "../../core"; +import {Command, NamedCommand, RestCommand} from "../../core"; import moment from "moment"; import {Storage} from "../../structures"; import {MessageEmbed} from "discord.js"; @@ -21,34 +21,38 @@ export default new NamedCommand({ }, subcommands: { add: new NamedCommand({ - async run({send, message, channel, guild, author, member, client, args}) { - const user = Storage.getUser(author.id); - const note = args.join(" "); - user.todoList[Date.now().toString()] = note; - console.debug(user.todoList); - Storage.save(); - send(`Successfully added \`${note}\` to your todo list.`); - } + run: "You need to specify a note to add.", + any: new RestCommand({ + async run({send, message, channel, guild, author, member, client, args, combined}) { + const user = Storage.getUser(author.id); + user.todoList[Date.now().toString()] = combined; + console.debug(user.todoList); + Storage.save(); + send(`Successfully added \`${combined}\` to your todo list.`); + } + }) }), remove: new NamedCommand({ - async run({send, message, channel, guild, author, member, client, args}) { - const user = Storage.getUser(author.id); - const note = args.join(" "); - let isFound = false; + run: "You need to specify a note to remove.", + any: new RestCommand({ + async run({send, message, channel, guild, author, member, client, args, combined}) { + const user = Storage.getUser(author.id); + let isFound = false; - for (const timestamp in user.todoList) { - const selectedNote = user.todoList[timestamp]; + for (const timestamp in user.todoList) { + const selectedNote = user.todoList[timestamp]; - if (selectedNote === note) { - delete user.todoList[timestamp]; - Storage.save(); - isFound = true; - send(`Removed \`${note}\` from your todo list.`); + if (selectedNote === combined) { + delete user.todoList[timestamp]; + Storage.save(); + isFound = true; + send(`Removed \`${combined}\` from your todo list.`); + } } - } - if (!isFound) send("That item couldn't be found."); - } + if (!isFound) send("That item couldn't be found."); + } + }) }), clear: new NamedCommand({ async run({send, message, channel, guild, author, member, client, args}) { diff --git a/src/core/command.ts b/src/core/command.ts index 703ca9d..c818986 100644 --- a/src/core/command.ts +++ b/src/core/command.ts @@ -82,7 +82,6 @@ interface CommandOptionsBase { interface CommandOptionsEndpoint { readonly endpoint: true; - readonly rest?: RestCommand; readonly run?: (($: CommandMenu) => Promise) | string; } @@ -91,6 +90,7 @@ interface CommandOptionsEndpoint { // Role pings, maybe not, but it's not a big deal. interface CommandOptionsNonEndpoint { readonly endpoint?: false; + readonly run?: (($: CommandMenu) => Promise) | string; readonly subcommands?: {[key: string]: NamedCommand}; readonly channel?: Command; readonly role?: Command; @@ -100,9 +100,7 @@ interface CommandOptionsNonEndpoint { readonly guild?: Command; // Only available if an ID is set to reroute to it. readonly id?: ID; readonly number?: Command; - readonly any?: Command; - readonly rest?: undefined; // Redeclare it here as undefined to prevent its use otherwise. - readonly run?: (($: CommandMenu) => Promise) | string; + readonly any?: Command | RestCommand; } type CommandOptions = CommandOptionsBase & (CommandOptionsEndpoint | CommandOptionsNonEndpoint); @@ -123,9 +121,8 @@ interface ExecuteCommandMetadata { export interface CommandInfo { readonly type: "info"; readonly command: BaseCommand; - readonly subcommandInfo: Collection; - readonly keyedSubcommandInfo: Collection; - readonly hasRestCommand: boolean; + readonly subcommandInfo: Collection; + readonly keyedSubcommandInfo: Collection; readonly permission: number; readonly nsfw: boolean; readonly channelType: CHANNEL_TYPE; @@ -181,8 +178,7 @@ export class Command extends BaseCommand { private id: Command | null; private idType: ID | null; private number: Command | null; - private any: Command | null; - private rest: RestCommand | null; + private any: Command | RestCommand | null; constructor(options?: CommandOptions) { super(options); @@ -199,7 +195,6 @@ export class Command extends BaseCommand { this.idType = null; this.number = null; this.any = null; - this.rest = null; if (options && !options.endpoint) { if (options.channel) this.channel = options.channel; @@ -263,8 +258,6 @@ export class Command extends BaseCommand { } } } - } else if (options && options.endpoint) { - if (options.rest) this.rest = options.rest; } } @@ -327,14 +320,7 @@ export class Command extends BaseCommand { } // If the current command is an endpoint but there are still some arguments left, don't continue unless there's a RestCommand. - if (this.endpoint) { - if (this.rest) { - args.unshift(param); - return this.rest.execute(args.join(" "), menu, metadata); - } else { - return {content: "Too many arguments!"}; - } - } + if (this.endpoint) return {content: "Too many arguments!"}; // Resolve the value of the current command's argument (adding it to the resolved args), // then pass the thread of execution to whichever subcommand is valid (if any). @@ -513,10 +499,14 @@ export class Command extends BaseCommand { metadata.symbolicArgs.push(""); menu.args.push(Number(param)); return this.number.execute(args, menu, metadata); - } else if (this.any) { + } else if (this.any instanceof Command) { metadata.symbolicArgs.push(""); menu.args.push(param); return this.any.execute(args, menu, metadata); + } else if (this.any instanceof RestCommand) { + metadata.symbolicArgs.push("<...>"); + args.unshift(param); + return this.any.execute(args.join(" "), menu, metadata); } else { // Continue adding on the rest of the arguments if there's no valid subcommand. menu.args.push(param); @@ -553,8 +543,8 @@ export class Command extends BaseCommand { // If there are no arguments left, return the data or an error message. if (param === undefined) { - const keyedSubcommandInfo = new Collection(); - const subcommandInfo = new Collection(); + const keyedSubcommandInfo = new Collection(); + const subcommandInfo = new Collection(); // Get all the subcommands of the current command but without aliases. for (const [tag, command] of this.subcommands.entries()) { @@ -572,14 +562,18 @@ export class Command extends BaseCommand { if (this.user) subcommandInfo.set("", this.user); if (this.id) subcommandInfo.set(`>`, this.id); if (this.number) subcommandInfo.set("", this.number); - if (this.any) subcommandInfo.set("", this.any); + + // The special case for a possible rest command. + if (this.any) { + if (this.any instanceof Command) subcommandInfo.set("", this.any); + else subcommandInfo.set("<...>", this.any); + } return { type: "info", command: this, keyedSubcommandInfo, subcommandInfo, - hasRestCommand: !!this.rest, ...metadata }; } @@ -640,16 +634,16 @@ export class Command extends BaseCommand { return invalidSubcommandGenerator(); } } else if (param === "") { - if (this.any) { + if (this.any instanceof Command) { metadata.args.push(""); return this.any.resolveInfoInternal(args, metadata); } else { return invalidSubcommandGenerator(); } } else if (param === "<...>") { - if (this.rest) { + if (this.any instanceof RestCommand) { metadata.args.push("<...>"); - return this.rest.resolveInfoFinale(metadata); + return this.any.resolveInfoFinale(metadata); } else { return invalidSubcommandGenerator(); } @@ -755,9 +749,8 @@ export class RestCommand extends BaseCommand { return { type: "info", command: this, - keyedSubcommandInfo: new Collection(), - subcommandInfo: new Collection(), - hasRestCommand: false, + keyedSubcommandInfo: new Collection(), + subcommandInfo: new Collection(), ...metadata }; } From 3798c27df9f595952e5c42533b155cc600bc3089 Mon Sep 17 00:00:00 2001 From: WatDuhHekBro <44940783+WatDuhHekBro@users.noreply.github.com> Date: Sat, 10 Apr 2021 14:08:36 -0500 Subject: [PATCH 10/14] Removed lenient command handling --- CHANGELOG.md | 3 +- src/commands/fun/8ball.ts | 1 - src/commands/fun/modules/eco-bet.ts | 2 +- src/commands/fun/modules/eco-core.ts | 2 +- src/commands/fun/thonk.ts | 17 ++- src/commands/system/admin.ts | 2 +- src/commands/template.ts | 2 +- src/commands/utility/emote.ts | 4 +- src/commands/utility/lsemotes.ts | 4 +- src/commands/utility/react.ts | 177 ++++++++++++++------------- src/commands/utility/streaminfo.ts | 29 ++++- src/commands/utility/translate.ts | 64 +++++----- src/core/command.ts | 113 ++++++++--------- src/core/handler.ts | 9 +- 14 files changed, 228 insertions(+), 201 deletions(-) diff --git a/CHANGELOG.md b/CHANGELOG.md index c366d51..34a876a 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -11,7 +11,8 @@ - Various changes to core - Added `guild` subcommand type (only accessible when `id: "guild"`) - Further reduced `channel.send()` to `send()` because it's used in *every, single, command* - - Added `rest` subcommand type (only available when `endpoint: true`), declaratively states that the following command will do `args.join(" ")`, preventing any other subcommands from being added + - Added a `RestCommand` type, declaratively states that the following command will do `args.join(" ")`, preventing any other subcommands from being added + - Is no longer lenient to arguments when no proper subcommand fits (now it doesn't silently fail anymore), you now have to explicitly declare a `RestCommand` to get an arbitrary number of arguments # 3.2.0 - Internal refactor, more subcommand types, and more command type guards (2021-04-09) - The custom logger changed: `$.log` no longer exists, it's just `console.log`. Now you don't have to do `import $ from "../core/lib"` at the top of every file that uses the custom logger. diff --git a/src/commands/fun/8ball.ts b/src/commands/fun/8ball.ts index afc1f23..898eafe 100644 --- a/src/commands/fun/8ball.ts +++ b/src/commands/fun/8ball.ts @@ -26,7 +26,6 @@ const responses = [ export default new NamedCommand({ description: "Answers your question in an 8-ball manner.", - endpoint: false, usage: "", run: "Please provide a question.", any: new Command({ diff --git a/src/commands/fun/modules/eco-bet.ts b/src/commands/fun/modules/eco-bet.ts index 9095124..ecda43c 100644 --- a/src/commands/fun/modules/eco-bet.ts +++ b/src/commands/fun/modules/eco-bet.ts @@ -62,7 +62,7 @@ export const BetCommand = new NamedCommand({ // handle invalid target if (target.id == author.id) return send("You can't bet Mons with yourself!"); - else if (target.bot && process.argv[2] !== "dev") return send("You can't bet Mons with a bot!"); + else if (target.bot && !IS_DEV_MODE) return send("You can't bet Mons with a bot!"); // handle invalid amount if (amount <= 0) return send("You must bet at least one Mon!"); diff --git a/src/commands/fun/modules/eco-core.ts b/src/commands/fun/modules/eco-core.ts index 390c268..ec3d8b6 100644 --- a/src/commands/fun/modules/eco-core.ts +++ b/src/commands/fun/modules/eco-core.ts @@ -128,7 +128,7 @@ export const PayCommand = new NamedCommand({ else if (sender.money < amount) return send("You don't have enough Mons for that.", getMoneyEmbed(author)); else if (target.id === author.id) return send("You can't send Mons to yourself!"); - else if (target.bot && process.argv[2] !== "dev") return send("You can't send Mons to a bot!"); + else if (target.bot && !IS_DEV_MODE) return send("You can't send Mons to a bot!"); sender.money -= amount; receiver.money += amount; diff --git a/src/commands/fun/thonk.ts b/src/commands/fun/thonk.ts index cdc3afe..de09d20 100644 --- a/src/commands/fun/thonk.ts +++ b/src/commands/fun/thonk.ts @@ -1,4 +1,4 @@ -import {Command, NamedCommand} from "../../core"; +import {Command, NamedCommand, RestCommand} from "../../core"; const letters: {[letter: string]: string[]} = { a: "aáàảãạâấầẩẫậăắằẳẵặ".split(""), @@ -35,7 +35,6 @@ export default new NamedCommand({ description: "Transforms your text into vietnamese.", usage: "thonk ([text])", async run({send, message, channel, guild, author, member, client, args}) { - if (args.length > 0) phrase = args.join(" "); const msg = await send(transform(phrase)); msg.createReactionCollector( (reaction, user) => { @@ -44,5 +43,17 @@ export default new NamedCommand({ }, {time: 60000} ); - } + }, + any: new RestCommand({ + async run({send, message, channel, guild, author, member, client, args, combined}) { + const msg = await send(transform(combined)); + msg.createReactionCollector( + (reaction, user) => { + if (user.id === author.id && reaction.emoji.name === "❌") msg.delete(); + return false; + }, + {time: 60000} + ); + } + }) }); diff --git a/src/commands/system/admin.ts b/src/commands/system/admin.ts index b49e890..a57d37c 100644 --- a/src/commands/system/admin.ts +++ b/src/commands/system/admin.ts @@ -293,7 +293,7 @@ export default new NamedCommand({ }); send("Activity set to default."); }, - any: new Command({ + any: new RestCommand({ description: `Select an activity type to set. Available levels: \`[${activities.join(", ")}]\``, async run({send, message, channel, guild, author, member, client, args}) { const type = args[0]; diff --git a/src/commands/template.ts b/src/commands/template.ts index bc7770e..9ea84bf 100644 --- a/src/commands/template.ts +++ b/src/commands/template.ts @@ -1,4 +1,4 @@ -import {Command, NamedCommand} from "../core"; +import {Command, NamedCommand, RestCommand} from "../core"; export default new NamedCommand({ async run({send, message, channel, guild, author, member, client, args}) { diff --git a/src/commands/utility/emote.ts b/src/commands/utility/emote.ts index 4f5e933..4d79446 100644 --- a/src/commands/utility/emote.ts +++ b/src/commands/utility/emote.ts @@ -1,11 +1,11 @@ -import {Command, NamedCommand} from "../../core"; +import {Command, NamedCommand, RestCommand} from "../../core"; import {processEmoteQueryFormatted} from "./modules/emote-utils"; export default new NamedCommand({ description: "Send the specified emote list. Enter + to move an emote list to the next line, - to add a space, and _ to add a zero-width space.", run: "Please provide a list of emotes.", - any: new Command({ + any: new RestCommand({ description: "The emote(s) to send.", usage: "", async run({send, guild, channel, message, args}) { diff --git a/src/commands/utility/lsemotes.ts b/src/commands/utility/lsemotes.ts index 6633d63..37840b9 100644 --- a/src/commands/utility/lsemotes.ts +++ b/src/commands/utility/lsemotes.ts @@ -1,5 +1,5 @@ import {GuildEmoji, MessageEmbed, User} from "discord.js"; -import {Command, NamedCommand, paginate, SendFunction} from "../../core"; +import {Command, NamedCommand, RestCommand, paginate, SendFunction} from "../../core"; import {split} from "../../lib"; import vm from "vm"; @@ -11,7 +11,7 @@ export default new NamedCommand({ async run({send, message, channel, guild, author, member, client, args}) { displayEmoteList(client.emojis.cache.array(), send, author); }, - any: new Command({ + any: new RestCommand({ description: "Filters emotes by via a regular expression. Flags can be added by adding a dash at the end. For example, to do a case-insensitive search, do %prefix%lsemotes somepattern -i", async run({send, message, channel, guild, author, member, client, args}) { diff --git a/src/commands/utility/react.ts b/src/commands/utility/react.ts index c4150c2..ef1aebb 100644 --- a/src/commands/utility/react.ts +++ b/src/commands/utility/react.ts @@ -1,4 +1,4 @@ -import {Command, NamedCommand} from "../../core"; +import {Command, NamedCommand, RestCommand} from "../../core"; import {Message, Channel, TextChannel} from "discord.js"; import {processEmoteQueryArray} from "./modules/emote-utils"; @@ -6,109 +6,112 @@ export default new NamedCommand({ description: "Reacts to the a previous message in your place. You have to react with the same emote before the bot removes that reaction.", usage: 'react ()', - async run({send, message, channel, guild, author, member, client, args}) { - let target: Message | undefined; - let distance = 1; + run: "You need to enter some emotes first.", + any: new RestCommand({ + async run({send, message, channel, guild, author, member, client, args}) { + let target: Message | undefined; + let distance = 1; - if (message.reference) { - // If the command message is a reply to another message, use that as the react target. - target = await channel.messages.fetch(message.reference.messageID!); - } - // handles reacts by message id/distance - else if (args.length >= 2) { - const last = args[args.length - 1]; // Because this is optional, do not .pop() unless you're sure it's a message link indicator. - const URLPattern = /^(?:https:\/\/discord.com\/channels\/(\d{17,})\/(\d{17,})\/(\d{17,}))$/; - const copyIDPattern = /^(?:(\d{17,})-(\d{17,}))$/; + if (message.reference) { + // If the command message is a reply to another message, use that as the react target. + target = await channel.messages.fetch(message.reference.messageID!); + } + // handles reacts by message id/distance + else if (args.length >= 2) { + const last = args[args.length - 1]; // Because this is optional, do not .pop() unless you're sure it's a message link indicator. + const URLPattern = /^(?:https:\/\/discord.com\/channels\/(\d{17,})\/(\d{17,})\/(\d{17,}))$/; + const copyIDPattern = /^(?:(\d{17,})-(\d{17,}))$/; - // https://discord.com/channels/// ("Copy Message Link" Button) - if (URLPattern.test(last)) { - const match = URLPattern.exec(last)!; - const guildID = match[1]; - const channelID = match[2]; - const messageID = match[3]; - let tmpChannel: Channel | undefined = channel; + // https://discord.com/channels/// ("Copy Message Link" Button) + if (URLPattern.test(last)) { + const match = URLPattern.exec(last)!; + const guildID = match[1]; + const channelID = match[2]; + const messageID = match[3]; + let tmpChannel: Channel | undefined = channel; - if (guild?.id !== guildID) { - try { - guild = await client.guilds.fetch(guildID); - } catch { - return send(`\`${guildID}\` is an invalid guild ID!`); + if (guild?.id !== guildID) { + try { + guild = await client.guilds.fetch(guildID); + } catch { + return send(`\`${guildID}\` is an invalid guild ID!`); + } } - } - if (tmpChannel.id !== channelID) tmpChannel = guild.channels.cache.get(channelID); - if (!tmpChannel) return send(`\`${channelID}\` is an invalid channel ID!`); + if (tmpChannel.id !== channelID) tmpChannel = guild.channels.cache.get(channelID); + if (!tmpChannel) return send(`\`${channelID}\` is an invalid channel ID!`); - if (message.id !== messageID) { - try { - target = await (tmpChannel as TextChannel).messages.fetch(messageID); - } catch { - return send(`\`${messageID}\` is an invalid message ID!`); + if (message.id !== messageID) { + try { + target = await (tmpChannel as TextChannel).messages.fetch(messageID); + } catch { + return send(`\`${messageID}\` is an invalid message ID!`); + } } + + args.pop(); } + // - ("Copy ID" Button) + else if (copyIDPattern.test(last)) { + const match = copyIDPattern.exec(last)!; + const channelID = match[1]; + const messageID = match[2]; + let tmpChannel: Channel | undefined = channel; - args.pop(); - } - // - ("Copy ID" Button) - else if (copyIDPattern.test(last)) { - const match = copyIDPattern.exec(last)!; - const channelID = match[1]; - const messageID = match[2]; - let tmpChannel: Channel | undefined = channel; + if (tmpChannel.id !== channelID) tmpChannel = guild?.channels.cache.get(channelID); + if (!tmpChannel) return send(`\`${channelID}\` is an invalid channel ID!`); - if (tmpChannel.id !== channelID) tmpChannel = guild?.channels.cache.get(channelID); - if (!tmpChannel) return send(`\`${channelID}\` is an invalid channel ID!`); - - if (message.id !== messageID) { - try { - target = await (tmpChannel as TextChannel).messages.fetch(messageID); - } catch { - return send(`\`${messageID}\` is an invalid message ID!`); + if (message.id !== messageID) { + try { + target = await (tmpChannel as TextChannel).messages.fetch(messageID); + } catch { + return send(`\`${messageID}\` is an invalid message ID!`); + } } + + args.pop(); } + // + else if (/^\d{17,}$/.test(last)) { + try { + target = await channel.messages.fetch(last); + } catch { + return send(`No valid message found by the ID \`${last}\`!`); + } - args.pop(); - } - // - else if (/^\d{17,}$/.test(last)) { - try { - target = await channel.messages.fetch(last); - } catch { - return send(`No valid message found by the ID \`${last}\`!`); + args.pop(); } + // The entire string has to be a number for this to match. Prevents leaCheeseAmerican1 from triggering this. + else if (/^\d+$/.test(last)) { + distance = parseInt(last); - args.pop(); + if (distance >= 0 && distance <= 99) args.pop(); + else return send("Your distance must be between 0 and 99!"); + } } - // The entire string has to be a number for this to match. Prevents leaCheeseAmerican1 from triggering this. - else if (/^\d+$/.test(last)) { - distance = parseInt(last); - if (distance >= 0 && distance <= 99) args.pop(); - else return send("Your distance must be between 0 and 99!"); + if (!target) { + // Messages are ordered from latest to earliest. + // You also have to add 1 as well because fetchMessages includes your own message. + target = ( + await message.channel.messages.fetch({ + limit: distance + 1 + }) + ).last(); } + + for (const emote of processEmoteQueryArray(args)) { + // Even though the bot will always grab *some* emote, the user can choose not to keep that emote there if it isn't what they want + const reaction = await target!.react(emote); + + // This part is called with a promise because you don't want to wait 5 seconds between each reaction. + setTimeout(() => { + // This reason for this null assertion is that by the time you use this command, the client is going to be loaded. + reaction.users.remove(client.user!); + }, 5000); + } + + return; } - - if (!target) { - // Messages are ordered from latest to earliest. - // You also have to add 1 as well because fetchMessages includes your own message. - target = ( - await message.channel.messages.fetch({ - limit: distance + 1 - }) - ).last(); - } - - for (const emote of processEmoteQueryArray(args)) { - // Even though the bot will always grab *some* emote, the user can choose not to keep that emote there if it isn't what they want - const reaction = await target!.react(emote); - - // This part is called with a promise because you don't want to wait 5 seconds between each reaction. - setTimeout(() => { - // This reason for this null assertion is that by the time you use this command, the client is going to be loaded. - reaction.users.remove(client.user!); - }, 5000); - } - - return; - } + }) }); diff --git a/src/commands/utility/streaminfo.ts b/src/commands/utility/streaminfo.ts index c1cff8a..c122b8b 100644 --- a/src/commands/utility/streaminfo.ts +++ b/src/commands/utility/streaminfo.ts @@ -1,4 +1,4 @@ -import {Command, NamedCommand} from "../../core"; +import {Command, NamedCommand, RestCommand} from "../../core"; import {streamList} from "../../modules/streamNotifications"; export default new NamedCommand({ @@ -8,12 +8,11 @@ export default new NamedCommand({ if (streamList.has(userID)) { const stream = streamList.get(userID)!; - const description = args.join(" ") || "No description set."; - stream.description = description; + stream.description = "No description set."; stream.update(); send(`Successfully set the stream description to:`, { embed: { - description, + description: "No description set.", color: member!.displayColor } }); @@ -21,5 +20,25 @@ export default new NamedCommand({ // Alternatively, I could make descriptions last outside of just one stream. send("You can only use this command when streaming."); } - } + }, + any: new RestCommand({ + async run({send, message, channel, guild, author, member, client, args, combined}) { + const userID = author.id; + + if (streamList.has(userID)) { + const stream = streamList.get(userID)!; + stream.description = combined; + stream.update(); + send(`Successfully set the stream description to:`, { + embed: { + description: stream.description, + color: member!.displayColor + } + }); + } else { + // Alternatively, I could make descriptions last outside of just one stream. + send("You can only use this command when streaming."); + } + } + }) }); diff --git a/src/commands/utility/translate.ts b/src/commands/utility/translate.ts index c026cfb..d4a1aab 100644 --- a/src/commands/utility/translate.ts +++ b/src/commands/utility/translate.ts @@ -1,35 +1,43 @@ -import {Command, NamedCommand} from "../../core"; +import {Command, NamedCommand, RestCommand} from "../../core"; import translate from "translate-google"; export default new NamedCommand({ description: "Translates your input.", usage: " ", - async run({send, message, channel, guild, author, member, client, args}) { - const lang = args[0]; - const input = args.slice(1).join(" "); - translate(input, { - to: lang - }) - .then((res) => { - send({ - embed: { - title: "Translation", - fields: [ - { - name: "Input", - value: `\`\`\`${input}\`\`\`` - }, - { - name: "Output", - value: `\`\`\`${res}\`\`\`` + run: "You need to specify a language to translate to.", + any: new Command({ + run: "You need to enter some text to translate.", + any: new RestCommand({ + async run({send, message, channel, guild, author, member, client, args}) { + const lang = args[0]; + const input = args.slice(1).join(" "); + translate(input, { + to: lang + }) + .then((res) => { + send({ + embed: { + title: "Translation", + fields: [ + { + name: "Input", + value: `\`\`\`${input}\`\`\`` + }, + { + name: "Output", + value: `\`\`\`${res}\`\`\`` + } + ] } - ] - } - }); - }) - .catch((error) => { - console.error(error); - send(`${error}\nPlease use the following list: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes`); - }); - } + }); + }) + .catch((error) => { + console.error(error); + send( + `${error}\nPlease use the following list: https://en.wikipedia.org/wiki/List_of_ISO_639-1_codes` + ); + }); + } + }) + }) }); diff --git a/src/core/command.ts b/src/core/command.ts index c818986..cded03c 100644 --- a/src/core/command.ts +++ b/src/core/command.ts @@ -73,23 +73,15 @@ interface CommandMenu { interface CommandOptionsBase { readonly description?: string; - readonly endpoint?: boolean; readonly usage?: string; readonly permission?: number; readonly nsfw?: boolean; readonly channelType?: CHANNEL_TYPE; } -interface CommandOptionsEndpoint { - readonly endpoint: true; - readonly run?: (($: CommandMenu) => Promise) | string; -} - -// Prevents subcommands from being added by compile-time. // Also, contrary to what you might think, channel pings do still work in DM channels. // Role pings, maybe not, but it's not a big deal. -interface CommandOptionsNonEndpoint { - readonly endpoint?: false; +interface CommandOptions extends CommandOptionsBase { readonly run?: (($: CommandMenu) => Promise) | string; readonly subcommands?: {[key: string]: NamedCommand}; readonly channel?: Command; @@ -103,11 +95,14 @@ interface CommandOptionsNonEndpoint { readonly any?: Command | RestCommand; } -type CommandOptions = CommandOptionsBase & (CommandOptionsEndpoint | CommandOptionsNonEndpoint); -type NamedCommandOptions = CommandOptions & {aliases?: string[]; nameOverride?: string}; -type RestCommandOptions = CommandOptionsBase & { - run?: (($: CommandMenu & {readonly combined: string}) => Promise) | string; -}; +interface NamedCommandOptions extends CommandOptions { + readonly aliases?: string[]; + readonly nameOverride?: string; +} + +interface RestCommandOptions extends CommandOptionsBase { + readonly run?: (($: CommandMenu & {readonly combined: string}) => Promise) | string; +} interface ExecuteCommandMetadata { readonly header: string; @@ -164,7 +159,6 @@ abstract class BaseCommand { // Each Command instance represents a block that links other Command instances under it. export class Command extends BaseCommand { - public readonly endpoint: boolean; // The execute and subcommand properties are restricted to the class because subcommand recursion could easily break when manually handled. // The class will handle checking for null fields. private run: (($: CommandMenu) => Promise) | string; @@ -182,31 +176,20 @@ export class Command extends BaseCommand { constructor(options?: CommandOptions) { super(options); - this.endpoint = !!options?.endpoint; this.run = options?.run || "No action was set on this command!"; this.subcommands = new Collection(); // Populate this collection after setting subcommands. - this.channel = null; - this.role = null; - this.emote = null; - this.message = null; - this.user = null; - this.guild = null; + this.channel = options?.channel || null; + this.role = options?.role || null; + this.emote = options?.emote || null; + this.message = options?.message || null; + this.user = options?.user || null; + this.guild = options?.guild || null; this.id = null; - this.idType = null; - this.number = null; - this.any = null; - - if (options && !options.endpoint) { - if (options.channel) this.channel = options.channel; - if (options.role) this.role = options.role; - if (options.emote) this.emote = options.emote; - if (options.message) this.message = options.message; - if (options.user) this.user = options.user; - if (options.guild) this.guild = options.guild; - if (options.number) this.number = options.number; - if (options.any) this.any = options.any; - if (options.id) this.idType = options.id; + this.idType = options?.id || null; + this.number = options?.number || null; + this.any = options?.any || null; + if (options) switch (options.id) { case "channel": this.id = this.channel; @@ -232,30 +215,29 @@ export class Command extends BaseCommand { requireAllCasesHandledFor(options.id); } - if (options.subcommands) { - const baseSubcommands = Object.keys(options.subcommands); + if (options?.subcommands) { + const baseSubcommands = Object.keys(options.subcommands); - // Loop once to set the base subcommands. - for (const name in options.subcommands) this.subcommands.set(name, options.subcommands[name]); + // Loop once to set the base subcommands. + for (const name in options.subcommands) this.subcommands.set(name, options.subcommands[name]); - // Then loop again to make aliases point to the base subcommands and warn if something's not right. - // This shouldn't be a problem because I'm hoping that JS stores these as references that point to the same object. - for (const name in options.subcommands) { - const subcmd = options.subcommands[name]; - subcmd.name = name; - const aliases = subcmd.aliases; + // Then loop again to make aliases point to the base subcommands and warn if something's not right. + // This shouldn't be a problem because I'm hoping that JS stores these as references that point to the same object. + for (const name in options.subcommands) { + const subcmd = options.subcommands[name]; + subcmd.name = name; + const aliases = subcmd.aliases; - for (const alias of aliases) { - if (baseSubcommands.includes(alias)) - console.warn( - `"${alias}" in subcommand "${name}" was attempted to be declared as an alias but it already exists in the base commands! (Look at the next "Loading Command" line to see which command is affected.)` - ); - else if (this.subcommands.has(alias)) - console.warn( - `Duplicate alias "${alias}" at subcommand "${name}"! (Look at the next "Loading Command" line to see which command is affected.)` - ); - else this.subcommands.set(alias, subcmd); - } + for (const alias of aliases) { + if (baseSubcommands.includes(alias)) + console.warn( + `"${alias}" in subcommand "${name}" was attempted to be declared as an alias but it already exists in the base commands! (Look at the next "Loading Command" line to see which command is affected.)` + ); + else if (this.subcommands.has(alias)) + console.warn( + `Duplicate alias "${alias}" at subcommand "${name}"! (Look at the next "Loading Command" line to see which command is affected.)` + ); + else this.subcommands.set(alias, subcmd); } } } @@ -319,9 +301,6 @@ export class Command extends BaseCommand { return null; } - // If the current command is an endpoint but there are still some arguments left, don't continue unless there's a RestCommand. - if (this.endpoint) return {content: "Too many arguments!"}; - // Resolve the value of the current command's argument (adding it to the resolved args), // then pass the thread of execution to whichever subcommand is valid (if any). const isMessageLink = patterns.messageLink.test(param); @@ -506,11 +485,15 @@ export class Command extends BaseCommand { } else if (this.any instanceof RestCommand) { metadata.symbolicArgs.push("<...>"); args.unshift(param); + menu.args.push(...args); return this.any.execute(args.join(" "), menu, metadata); } else { - // Continue adding on the rest of the arguments if there's no valid subcommand. - menu.args.push(param); - return this.execute(args, menu, metadata); + metadata.symbolicArgs.push(`"${param}"`); + return { + content: `No valid command sequence matching \`${metadata.header} ${metadata.symbolicArgs.join( + " " + )}\` found.` + }; } // Note: Do NOT add a return statement here. In case one of the other sections is missing @@ -726,7 +709,9 @@ export class RestCommand extends BaseCommand { } else { // Then capture any potential errors. try { - await this.run({...menu, combined}); + // Args will still be kept intact. A common pattern is popping some parameters off the end then doing some branching. + // That way, you can still declaratively mark an argument list as continuing while also handling the individual args. + await this.run({...menu, args: menu.args, combined}); } catch (error) { const errorMessage = error.stack ?? error; console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`); diff --git a/src/core/handler.ts b/src/core/handler.ts index 52fdc80..c93f217 100644 --- a/src/core/handler.ts +++ b/src/core/handler.ts @@ -21,8 +21,7 @@ const lastCommandInfo: { const defaultMetadata = { permission: 0, nsfw: false, - channelType: 0, // CHANNEL_TYPE.ANY, apparently isn't initialized at this point yet - symbolicArgs: [] + channelType: 0 // CHANNEL_TYPE.ANY, apparently isn't initialized at this point yet }; // Note: client.user is only undefined before the bot logs in, so by this point, client.user cannot be undefined. @@ -67,7 +66,8 @@ export function attachMessageHandlerToClient(client: Client) { const result = await command.execute(args, menu, { header, args: [...args], - ...defaultMetadata + ...defaultMetadata, + symbolicArgs: [] }); // If something went wrong, let the user know (like if they don't have permission to use a command). @@ -104,7 +104,8 @@ export function attachMessageHandlerToClient(client: Client) { const result = await command.execute(args, menu, { header, args: [...args], - ...defaultMetadata + ...defaultMetadata, + symbolicArgs: [] }); // If something went wrong, let the user know (like if they don't have permission to use a command). From c980a182f869b0a960bd1104184042f3b11a25bf Mon Sep 17 00:00:00 2001 From: WatDuhHekBro <44940783+WatDuhHekBro@users.noreply.github.com> Date: Sun, 11 Apr 2021 03:02:56 -0500 Subject: [PATCH 11/14] Updated library functions --- docs/Documentation.md | 27 +-- src/commands/fun/eco.ts | 3 +- src/commands/fun/modules/eco-bet.ts | 147 ++++++------ src/commands/fun/modules/eco-core.ts | 55 ++--- src/commands/fun/modules/eco-shop.ts | 17 +- src/commands/fun/modules/eco-utils.ts | 4 +- src/commands/fun/whois.ts | 4 +- src/commands/system/help.ts | 25 +- src/commands/utility/info.ts | 4 +- src/commands/utility/lsemotes.ts | 23 +- src/commands/utility/time.ts | 326 +++++++++++++------------- src/core/command.ts | 94 +++----- src/core/libd.ts | 288 ++++++++++------------- src/modules/messageEmbed.ts | 4 +- 14 files changed, 479 insertions(+), 542 deletions(-) diff --git a/docs/Documentation.md b/docs/Documentation.md index 1907bc9..28c2c32 100644 --- a/docs/Documentation.md +++ b/docs/Documentation.md @@ -75,27 +75,24 @@ Because versions are assigned to batches of changes rather than single changes ( ```ts const pages = ["one", "two", "three"]; -paginate(channel, author.id, pages.length, (page) => { - return { - content: pages[page] - }; -}); +paginate(send, page => { + return {content: pages[page]}; +}, pages.length, author.id); ``` -`prompt()` +`confirm()` ```ts -const msg = await channel.send('Are you sure you want to delete this?'); - -prompt(msg, author.id, () => { - //... -}); +const result = await confirm(await send("Are you sure you want to delete this?"), author.id); // boolean | null ``` -`callMemberByUsername()` +`askMultipleChoice()` ```ts -callMemberByUsername(message, args.join(" "), (member) => { - channel.send(`Your nickname is ${member.nickname}.`); -}); +const result = await askMultipleChoice(await send("Which of the following numbers is your favorite?"), author.id, 4, 10000); // number (0 to 3) | null +``` + +`askForReply()` +```ts +const reply = await askForReply(await send("What is your favorite thing to do?"), author.id, 10000); // Message | null ``` ## [src/lib](../src/lib.ts) - General utility functions diff --git a/src/commands/fun/eco.ts b/src/commands/fun/eco.ts index 96c1b43..241411d 100644 --- a/src/commands/fun/eco.ts +++ b/src/commands/fun/eco.ts @@ -4,7 +4,6 @@ import {DailyCommand, PayCommand, GuildCommand, LeaderboardCommand} from "./modu 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.", @@ -38,7 +37,7 @@ export default new NamedCommand({ async run({send, guild, channel, args, message, combined}) { if (isAuthorized(guild, channel)) { const member = await getMemberByName(guild!, combined); - if (member instanceof GuildMember) send(getMoneyEmbed(member.user)); + if (typeof member !== "string") send(getMoneyEmbed(member.user)); else send(member); } } diff --git a/src/commands/fun/modules/eco-bet.ts b/src/commands/fun/modules/eco-bet.ts index ecda43c..7b38485 100644 --- a/src/commands/fun/modules/eco-bet.ts +++ b/src/commands/fun/modules/eco-bet.ts @@ -1,7 +1,7 @@ -import {Command, NamedCommand, askYesOrNo} from "../../../core"; +import {Command, NamedCommand, confirm} from "../../../core"; import {pluralise} from "../../../lib"; import {Storage} from "../../../structures"; -import {isAuthorized, getMoneyEmbed, getSendEmbed, ECO_EMBED_COLOR} from "./eco-utils"; +import {isAuthorized, getMoneyEmbed} from "./eco-utils"; import {User} from "discord.js"; export const BetCommand = new NamedCommand({ @@ -79,88 +79,89 @@ export const BetCommand = new NamedCommand({ return send(`Bet duration is too long, maximum duration is ${durationBounds.max}`); // Ask target whether or not they want to take the bet. - const takeBet = await askYesOrNo( + const takeBet = await confirm( await send( `<@${target.id}>, do you want to take this bet of ${pluralise(amount, "Mon", "s")}` ), target.id ); - if (takeBet) { - // [MEDIUM PRIORITY: bet persistence to prevent losses in case of shutdown.] - // Remove amount money from both parts at the start to avoid duplication of money. - sender.money -= amount; - receiver.money -= amount; - // Very hacky solution for persistence but better than no solution, backup returns runs during the bot's setup code. - sender.ecoBetInsurance += amount; - receiver.ecoBetInsurance += amount; - Storage.save(); + if (!takeBet) return send(`<@${target.id}> has rejected your bet, <@${author.id}>`); - // Notify both users. - await send( - `<@${target.id}> has taken <@${author.id}>'s bet, the bet amount of ${pluralise( - amount, - "Mon", - "s" - )} has been deducted from each of them.` + // [MEDIUM PRIORITY: bet persistence to prevent losses in case of shutdown.] + // Remove amount money from both parts at the start to avoid duplication of money. + sender.money -= amount; + receiver.money -= amount; + // Very hacky solution for persistence but better than no solution, backup returns runs during the bot's setup code. + sender.ecoBetInsurance += amount; + receiver.ecoBetInsurance += amount; + Storage.save(); + + // Notify both users. + send( + `<@${target.id}> has taken <@${author.id}>'s bet, the bet amount of ${pluralise( + amount, + "Mon", + "s" + )} has been deducted from each of them.` + ); + + // Wait for the duration of the bet. + return client.setTimeout(async () => { + // In debug mode, saving the storage will break the references, so you have to redeclare sender and receiver for it to actually save. + const sender = Storage.getUser(author.id); + const receiver = Storage.getUser(target.id); + // [TODO: when D.JSv13 comes out, inline reply to clean up.] + // When bet is over, give a vote to ask people their thoughts. + const voteMsg = await send( + `VOTE: do you think that <@${ + target.id + }> has won the bet?\nhttps://discord.com/channels/${guild!.id}/${channel.id}/${ + message.id + }` ); + await voteMsg.react("✅"); + await voteMsg.react("❌"); - // Wait for the duration of the bet. - return client.setTimeout(async () => { - // In debug mode, saving the storage will break the references, so you have to redeclare sender and receiver for it to actually save. - const sender = Storage.getUser(author.id); - const receiver = Storage.getUser(target.id); - // [TODO: when D.JSv13 comes out, inline reply to clean up.] - // When bet is over, give a vote to ask people their thoughts. - const voteMsg = await send( - `VOTE: do you think that <@${ - target.id - }> has won the bet?\nhttps://discord.com/channels/${guild!.id}/${channel.id}/${ - message.id - }` - ); - await voteMsg.react("✅"); - await voteMsg.react("❌"); + // Filter reactions to only collect the pertinent ones. + voteMsg + .awaitReactions( + (reaction, user) => { + return ["✅", "❌"].includes(reaction.emoji.name); + }, + // [Pertinence to make configurable on the fly.] + {time: parseDuration("2m")} + ) + .then((reactions) => { + // Count votes + const okReaction = reactions.get("✅"); + const noReaction = reactions.get("❌"); + const ok = okReaction ? (okReaction.count ?? 1) - 1 : 0; + const no = noReaction ? (noReaction.count ?? 1) - 1 : 0; - // Filter reactions to only collect the pertinent ones. - voteMsg - .awaitReactions( - (reaction, user) => { - return ["✅", "❌"].includes(reaction.emoji.name); - }, - // [Pertinence to make configurable on the fly.] - {time: parseDuration("2m")} - ) - .then((reactions) => { - // Count votes - const okReaction = reactions.get("✅"); - const noReaction = reactions.get("❌"); - const ok = okReaction ? (okReaction.count ?? 1) - 1 : 0; - const no = noReaction ? (noReaction.count ?? 1) - 1 : 0; + if (ok > no) { + receiver.money += amount * 2; + send( + `By the people's votes, <@${target.id}> has won the bet that <@${author.id}> had sent them.` + ); + } else if (ok < no) { + sender.money += amount * 2; + send( + `By the people's votes, <@${target.id}> has lost the bet that <@${author.id}> had sent them.` + ); + } else { + sender.money += amount; + receiver.money += amount; + send( + `By the people's votes, <@${target.id}> couldn't be determined to have won or lost the bet that <@${author.id}> had sent them.` + ); + } - if (ok > no) { - receiver.money += amount * 2; - send( - `By the people's votes, <@${target.id}> has won the bet that <@${author.id}> had sent them.` - ); - } else if (ok < no) { - sender.money += amount * 2; - send( - `By the people's votes, <@${target.id}> has lost the bet that <@${author.id}> had sent them.` - ); - } else { - sender.money += amount; - receiver.money += amount; - send( - `By the people's votes, <@${target.id}> couldn't be determined to have won or lost the bet that <@${author.id}> had sent them.` - ); - } - sender.ecoBetInsurance -= amount; - receiver.ecoBetInsurance -= amount; - Storage.save(); - }); - }, duration); - } else return await send(`<@${target.id}> has rejected your bet, <@${author.id}>`); + sender.ecoBetInsurance -= amount; + receiver.ecoBetInsurance -= amount; + Storage.save(); + }); + }, duration); } else return; } }) diff --git a/src/commands/fun/modules/eco-core.ts b/src/commands/fun/modules/eco-core.ts index ec3d8b6..4e0a520 100644 --- a/src/commands/fun/modules/eco-core.ts +++ b/src/commands/fun/modules/eco-core.ts @@ -1,5 +1,4 @@ -import {GuildMember} from "discord.js"; -import {Command, getMemberByName, NamedCommand, prompt, RestCommand} from "../../../core"; +import {Command, getMemberByName, NamedCommand, confirm, RestCommand} from "../../../core"; import {pluralise} from "../../../lib"; import {Storage} from "../../../structures"; import {isAuthorized, getMoneyEmbed, getSendEmbed, ECO_EMBED_COLOR} from "./eco-utils"; @@ -90,7 +89,7 @@ export const LeaderboardCommand = new NamedCommand({ const user = await client.users.fetch(id); fields.push({ - name: `#${i + 1}. ${user.username}#${user.discriminator}`, + name: `#${i + 1}. ${user.tag}`, value: pluralise(users[id].money, "Mon", "s") }); } @@ -158,42 +157,38 @@ export const PayCommand = new NamedCommand({ return send("You have to use this in a server if you want to send Mons with a username!"); const member = await getMemberByName(guild, combined); - if (!(member instanceof GuildMember)) return send(member); + if (typeof member === "string") return send(member); else if (member.user.id === author.id) return send("You can't send Mons to yourself!"); else if (member.user.bot && process.argv[2] !== "dev") return send("You can't send Mons to a bot!"); const target = member.user; - return prompt( - await send( - `Are you sure you want to send ${pluralise( - amount, - "Mon", - "s" - )} to this person?\n*(This message will automatically be deleted after 10 seconds.)*`, - { - embed: { - color: ECO_EMBED_COLOR, - author: { - name: `${target.username}#${target.discriminator}`, - icon_url: target.displayAvatarURL({ - format: "png", - dynamic: true - }) - } + const result = await confirm( + await send(`Are you sure you want to send ${pluralise(amount, "Mon", "s")} to this person?`, { + embed: { + color: ECO_EMBED_COLOR, + author: { + name: target.tag, + icon_url: target.displayAvatarURL({ + format: "png", + dynamic: true + }) } } - ), - author.id, - () => { - const receiver = Storage.getUser(target.id); - sender.money -= amount; - receiver.money += amount; - Storage.save(); - send(getSendEmbed(author, target, amount)); - } + }), + author.id ); + + if (result) { + const receiver = Storage.getUser(target.id); + sender.money -= amount; + receiver.money += amount; + Storage.save(); + send(getSendEmbed(author, target, amount)); + } } + + return; } }) }); diff --git a/src/commands/fun/modules/eco-shop.ts b/src/commands/fun/modules/eco-shop.ts index acd0e4a..91ac71d 100644 --- a/src/commands/fun/modules/eco-shop.ts +++ b/src/commands/fun/modules/eco-shop.ts @@ -34,12 +34,17 @@ export const ShopCommand = new NamedCommand({ const shopPages = split(ShopItems, 5); const pageAmount = shopPages.length; - paginate(send, author.id, pageAmount, (page, hasMultiplePages) => { - return getShopEmbed( - shopPages[page], - hasMultiplePages ? `Shop (Page ${page + 1} of ${pageAmount})` : "Shop" - ); - }); + paginate( + send, + (page, hasMultiplePages) => { + return getShopEmbed( + shopPages[page], + hasMultiplePages ? `Shop (Page ${page + 1} of ${pageAmount})` : "Shop" + ); + }, + pageAmount, + author.id + ); } } }); diff --git a/src/commands/fun/modules/eco-utils.ts b/src/commands/fun/modules/eco-utils.ts index 327abdb..d014374 100644 --- a/src/commands/fun/modules/eco-utils.ts +++ b/src/commands/fun/modules/eco-utils.ts @@ -42,11 +42,11 @@ export function getSendEmbed(sender: User, receiver: User, amount: number): obje description: `${sender.toString()} has sent ${pluralise(amount, "Mon", "s")} to ${receiver.toString()}!`, fields: [ { - name: `Sender: ${sender.username}#${sender.discriminator}`, + name: `Sender: ${sender.tag}`, value: pluralise(Storage.getUser(sender.id).money, "Mon", "s") }, { - name: `Receiver: ${receiver.username}#${receiver.discriminator}`, + name: `Receiver: ${receiver.tag}`, value: pluralise(Storage.getUser(receiver.id).money, "Mon", "s") } ], diff --git a/src/commands/fun/whois.ts b/src/commands/fun/whois.ts index 4ad9c97..507c14f 100644 --- a/src/commands/fun/whois.ts +++ b/src/commands/fun/whois.ts @@ -1,4 +1,4 @@ -import {User, GuildMember} from "discord.js"; +import {User} from "discord.js"; import {Command, NamedCommand, getMemberByName, CHANNEL_TYPE, RestCommand} from "../../core"; // Quotes must be used here or the numbers will change @@ -70,7 +70,7 @@ export default new NamedCommand({ async run({send, message, channel, guild, author, client, args, combined}) { const member = await getMemberByName(guild!, combined); - if (member instanceof GuildMember) { + if (typeof member !== "string") { if (member.id in registry) { send(`\`${member.nickname ?? member.user.username}\` - ${registry[member.id]}`); } else { diff --git a/src/commands/system/help.ts b/src/commands/system/help.ts index 7691a5a..7e5139c 100644 --- a/src/commands/system/help.ts +++ b/src/commands/system/help.ts @@ -20,16 +20,21 @@ export default new NamedCommand({ const commands = await getCommandList(); const categoryArray = commands.keyArray(); - paginate(send, author.id, categoryArray.length, (page, hasMultiplePages) => { - const category = categoryArray[page]; - const commandList = commands.get(category)!; - let output = `Legend: \`\`, \`[list/of/stuff]\`, \`(optional)\`, \`()\`, \`([optional/list/...])\`\n`; - for (const command of commandList) output += `\n❯ \`${command.name}\`: ${command.description}`; - return new MessageEmbed() - .setTitle(hasMultiplePages ? `${category} (Page ${page + 1} of ${categoryArray.length})` : category) - .setDescription(output) - .setColor(EMBED_COLOR); - }); + paginate( + send, + (page, hasMultiplePages) => { + const category = categoryArray[page]; + const commandList = commands.get(category)!; + let output = `Legend: \`\`, \`[list/of/stuff]\`, \`(optional)\`, \`()\`, \`([optional/list/...])\`\n`; + for (const command of commandList) output += `\n❯ \`${command.name}\`: ${command.description}`; + return new MessageEmbed() + .setTitle(hasMultiplePages ? `${category} (Page ${page + 1} of ${categoryArray.length})` : category) + .setDescription(output) + .setColor(EMBED_COLOR); + }, + categoryArray.length, + author.id + ); }, any: new Command({ async run({send, message, channel, guild, author, member, client, args}) { diff --git a/src/commands/utility/info.ts b/src/commands/utility/info.ts index 5e51363..c9368e6 100644 --- a/src/commands/utility/info.ts +++ b/src/commands/utility/info.ts @@ -36,7 +36,7 @@ export default new NamedCommand({ async run({send, message, channel, guild, author, client, args, combined}) { const member = await getMemberByName(guild!, combined); - if (member instanceof GuildMember) { + if (typeof member !== "string") { send( member.user.displayAvatarURL({ dynamic: true, @@ -110,7 +110,7 @@ export default new NamedCommand({ async run({send, message, channel, guild, author, member, client, args, combined}) { const targetGuild = getGuildByName(combined); - if (targetGuild instanceof Guild) { + if (typeof targetGuild !== "string") { send(await getGuildInfo(targetGuild, guild)); } else { send(targetGuild); diff --git a/src/commands/utility/lsemotes.ts b/src/commands/utility/lsemotes.ts index 37840b9..7e1e2d9 100644 --- a/src/commands/utility/lsemotes.ts +++ b/src/commands/utility/lsemotes.ts @@ -90,17 +90,22 @@ async function displayEmoteList(emotes: GuildEmoji[], send: SendFunction, author // Gather the first page (if it even exists, which it might not if there no valid emotes appear) if (pages > 0) { - paginate(send, author.id, pages, (page, hasMultiplePages) => { - embed.setTitle(hasMultiplePages ? `**Emotes** (Page ${page + 1} of ${pages})` : "**Emotes**"); + paginate( + send, + (page, hasMultiplePages) => { + embed.setTitle(hasMultiplePages ? `**Emotes** (Page ${page + 1} of ${pages})` : "**Emotes**"); - let desc = ""; - for (const emote of sections[page]) { - desc += `${emote} ${emote.name} (**${emote.guild.name}**)\n`; - } - embed.setDescription(desc); + let desc = ""; + for (const emote of sections[page]) { + desc += `${emote} ${emote.name} (**${emote.guild.name}**)\n`; + } + embed.setDescription(desc); - return embed; - }); + return embed; + }, + pages, + author.id + ); } else { send("No valid emotes found by that query."); } diff --git a/src/commands/utility/time.ts b/src/commands/utility/time.ts index 7598f5a..9852b86 100644 --- a/src/commands/utility/time.ts +++ b/src/commands/utility/time.ts @@ -1,15 +1,6 @@ -import { - Command, - NamedCommand, - ask, - askYesOrNo, - askMultipleChoice, - prompt, - getMemberByName, - RestCommand -} from "../../core"; +import {Command, NamedCommand, askForReply, confirm, askMultipleChoice, getMemberByName, RestCommand} from "../../core"; import {Storage} from "../../structures"; -import {User, GuildMember} from "discord.js"; +import {User} from "discord.js"; import moment from "moment"; const DATE_FORMAT = "D MMMM YYYY"; @@ -178,183 +169,184 @@ function getTimeEmbed(user: User) { export default new NamedCommand({ description: "Show others what time it is for you.", aliases: ["tz"], - async run({send, channel, author}) { + async run({send, author}) { send(getTimeEmbed(author)); }, subcommands: { // Welcome to callback hell. We hope you enjoy your stay here! setup: new NamedCommand({ description: "Registers your timezone information for the bot.", - async run({send, author, channel}) { + async run({send, author}) { const profile = Storage.getUser(author.id); profile.timezone = null; profile.daylightSavingsRegion = null; - let hour: number; - ask( + // Parse and validate reply + const reply = await askForReply( await send( "What hour (0 to 23) is it for you right now?\n*(Note: Make sure to use Discord's inline reply feature or this won't work!)*" ), author.id, - (reply) => { - hour = parseInt(reply); - - if (isNaN(hour)) { - return false; - } - - return hour >= 0 && hour <= 23; - }, - async () => { - // You need to also take into account whether or not it's the same day in UTC or not. - // The problem this setup avoids is messing up timezones by 24 hours. - // For example, the old logic was just (hour - hourUTC). When I setup my timezone (UTC-6) at 18:00, it was UTC 00:00. - // That means that that formula was doing (18 - 0) getting me UTC+18 instead of UTC-6 because that naive formula didn't take into account a difference in days. - - // (day * 24 + hour) - (day * 24 + hour) - // Since the timezones will be restricted to -12 to +14, you'll be given three options. - // The end of the month should be calculated automatically, you should have enough information at that point. - - // But after mapping out the edge cases, I figured out that you can safely gather accurate information just based on whether the UTC day matches the user's day. - // 21:xx (-12, -d) -- 09:xx (+0, 0d) -- 23:xx (+14, 0d) - // 22:xx (-12, -d) -- 10:xx (+0, 0d) -- 00:xx (+14, +d) - // 23:xx (-12, -d) -- 11:xx (+0, 0d) -- 01:xx (+14, +d) - // 00:xx (-12, 0d) -- 12:xx (+0, 0d) -- 02:xx (+14, +d) - - // For example, 23:xx (-12) - 01:xx (+14) shows that even the edge cases of UTC-12 and UTC+14 cannot overlap, so the dataset can be reduced to a yes or no option. - // - 23:xx same day = +0, 23:xx diff day = -1 - // - 00:xx same day = +0, 00:xx diff day = +1 - // - 01:xx same day = +0, 01:xx diff day = +1 - - // First, create a tuple list matching each possible hour-dayOffset-timezoneOffset combination. In the above example, it'd go something like this: - // [[23, -1, -12], [0, 0, -11], ..., [23, 0, 12], [0, 1, 13], [1, 1, 14]] - // Then just find the matching one by filtering through dayOffset (equals zero or not), then the hour from user input. - // Remember that you don't know where the difference in day might be at this point, so you can't do (hour - hourUTC) safely. - // In terms of the equation, you're missing a variable in (--> day <-- * 24 + hour) - (day * 24 + hour). That's what the loop is for. - - // Now you might be seeing a problem with setting this up at the end/beginning of a month, but that actually isn't a problem. - // Let's say that it's 00:xx of the first UTC day of a month. hourSumUTC = 24 - // UTC-12 --> hourSumLowerBound (hourSumUTC - 12) = 12 - // UTC+14 --> hourSumUpperBound (hourSumUTC + 14) = 38 - // Remember, the nice thing about making these bounds relative to the UTC hour sum is that there can't be any conflicts even at the edges of months. - // And remember that we aren't using this question: (day * 24 + hour) - (day * 24 + hour). We're drawing from a list which does not store its data in absolute terms. - // That means there's no 31st, 1st, 2nd, it's -1, 0, +1. I just need to make sure to calculate an offset to subtract from the hour sums. - - const date = new Date(); // e.g. 2021-05-01 @ 05:00 - const day = date.getUTCDate(); // e.g. 1 - const hourSumUTC = day * 24 + date.getUTCHours(); // e.g. 29 - const timezoneTupleList: [number, number, number][] = []; - const uniques: number[] = []; // only for temporary use - const duplicates = []; - - // Setup the tuple list in a separate block. - for (let timezoneOffset = -12; timezoneOffset <= 14; timezoneOffset++) { - const hourSum = hourSumUTC + timezoneOffset; // e.g. 23, UTC-6 (17 to 43) - const hour = hourSum % 24; // e.g. 23 - // This works because you get the # of days w/o hours minus UTC days without hours. - // Since it's all relative to UTC, it'll end up being -1, 0, or 1. - const dayOffset = Math.floor(hourSum / 24) - day; // e.g. -1 - timezoneTupleList.push([hour, dayOffset, timezoneOffset]); - - if (uniques.includes(hour)) { - duplicates.push(hour); - } else { - uniques.push(hour); - } - } - - // I calculate the list beforehand and check for duplicates to reduce unnecessary asking. - if (duplicates.includes(hour)) { - const isSameDay = await askYesOrNo( - await send( - `Is the current day of the month the ${moment().utc().format("Do")} for you?` - ), - author.id - ); - - // Filter through isSameDay (aka !hasDayOffset) then hour from user-generated input. - // isSameDay is checked first to reduce the amount of conditionals per loop. - if (isSameDay) { - for (const [hourPoint, dayOffset, timezoneOffset] of timezoneTupleList) { - if (dayOffset === 0 && hour === hourPoint) { - profile.timezone = timezoneOffset; - } - } - } else { - for (const [hourPoint, dayOffset, timezoneOffset] of timezoneTupleList) { - if (dayOffset !== 0 && hour === hourPoint) { - profile.timezone = timezoneOffset; - } - } - } - } else { - // If it's a unique hour, just search through the tuple list and find the matching entry. - for (const [hourPoint, _dayOffset, timezoneOffset] of timezoneTupleList) { - if (hour === hourPoint) { - profile.timezone = timezoneOffset; - } - } - } - - // I should note that error handling should be added sometime because await throws an exception on Promise.reject. - const hasDST = await askYesOrNo( - await send("Does your timezone change based on daylight savings?"), - author.id - ); - - const finalize = () => { - Storage.save(); - send( - "You've finished setting up your timezone! Just check to see if this looks right, and if it doesn't, run this setup again.", - getTimeEmbed(author) - ); - }; - - if (hasDST) { - const finalizeDST = (region: DST) => { - profile.daylightSavingsRegion = region; - - // If daylight savings is active, subtract the timezone offset by one to store the standard time. - if (hasDaylightSavings(region)) { - profile.timezone!--; - } - - finalize(); - }; - - askMultipleChoice(await send(DST_NOTE_SETUP), author.id, [ - () => finalizeDST("na"), - () => finalizeDST("eu"), - () => finalizeDST("sh") - ]); - } else { - finalize(); - } - }, - () => "you need to enter in a valid integer between 0 to 23" + 30000 ); + if (reply === null) return send("Message timed out."); + const hour = parseInt(reply.content); + const isValidHour = !isNaN(hour) && hour >= 0 && hour <= 23; + if (!isValidHour) return reply.reply("you need to enter in a valid integer between 0 to 23"); + + // You need to also take into account whether or not it's the same day in UTC or not. + // The problem this setup avoids is messing up timezones by 24 hours. + // For example, the old logic was just (hour - hourUTC). When I setup my timezone (UTC-6) at 18:00, it was UTC 00:00. + // That means that that formula was doing (18 - 0) getting me UTC+18 instead of UTC-6 because that naive formula didn't take into account a difference in days. + + // (day * 24 + hour) - (day * 24 + hour) + // Since the timezones will be restricted to -12 to +14, you'll be given three options. + // The end of the month should be calculated automatically, you should have enough information at that point. + + // But after mapping out the edge cases, I figured out that you can safely gather accurate information just based on whether the UTC day matches the user's day. + // 21:xx (-12, -d) -- 09:xx (+0, 0d) -- 23:xx (+14, 0d) + // 22:xx (-12, -d) -- 10:xx (+0, 0d) -- 00:xx (+14, +d) + // 23:xx (-12, -d) -- 11:xx (+0, 0d) -- 01:xx (+14, +d) + // 00:xx (-12, 0d) -- 12:xx (+0, 0d) -- 02:xx (+14, +d) + + // For example, 23:xx (-12) - 01:xx (+14) shows that even the edge cases of UTC-12 and UTC+14 cannot overlap, so the dataset can be reduced to a yes or no option. + // - 23:xx same day = +0, 23:xx diff day = -1 + // - 00:xx same day = +0, 00:xx diff day = +1 + // - 01:xx same day = +0, 01:xx diff day = +1 + + // First, create a tuple list matching each possible hour-dayOffset-timezoneOffset combination. In the above example, it'd go something like this: + // [[23, -1, -12], [0, 0, -11], ..., [23, 0, 12], [0, 1, 13], [1, 1, 14]] + // Then just find the matching one by filtering through dayOffset (equals zero or not), then the hour from user input. + // Remember that you don't know where the difference in day might be at this point, so you can't do (hour - hourUTC) safely. + // In terms of the equation, you're missing a variable in (--> day <-- * 24 + hour) - (day * 24 + hour). That's what the loop is for. + + // Now you might be seeing a problem with setting this up at the end/beginning of a month, but that actually isn't a problem. + // Let's say that it's 00:xx of the first UTC day of a month. hourSumUTC = 24 + // UTC-12 --> hourSumLowerBound (hourSumUTC - 12) = 12 + // UTC+14 --> hourSumUpperBound (hourSumUTC + 14) = 38 + // Remember, the nice thing about making these bounds relative to the UTC hour sum is that there can't be any conflicts even at the edges of months. + // And remember that we aren't using this question: (day * 24 + hour) - (day * 24 + hour). We're drawing from a list which does not store its data in absolute terms. + // That means there's no 31st, 1st, 2nd, it's -1, 0, +1. I just need to make sure to calculate an offset to subtract from the hour sums. + + const date = new Date(); // e.g. 2021-05-01 @ 05:00 + const day = date.getUTCDate(); // e.g. 1 + const hourSumUTC = day * 24 + date.getUTCHours(); // e.g. 29 + const timezoneTupleList: [number, number, number][] = []; + const uniques: number[] = []; // only for temporary use + const duplicates = []; + + // Setup the tuple list in a separate block. + for (let timezoneOffset = -12; timezoneOffset <= 14; timezoneOffset++) { + const hourSum = hourSumUTC + timezoneOffset; // e.g. 23, UTC-6 (17 to 43) + const hour = hourSum % 24; // e.g. 23 + // This works because you get the # of days w/o hours minus UTC days without hours. + // Since it's all relative to UTC, it'll end up being -1, 0, or 1. + const dayOffset = Math.floor(hourSum / 24) - day; // e.g. -1 + timezoneTupleList.push([hour, dayOffset, timezoneOffset]); + + if (uniques.includes(hour)) { + duplicates.push(hour); + } else { + uniques.push(hour); + } + } + + // I calculate the list beforehand and check for duplicates to reduce unnecessary asking. + if (duplicates.includes(hour)) { + const isSameDay = await confirm( + await send(`Is the current day of the month the ${moment().utc().format("Do")} for you?`), + author.id + ); + + // Filter through isSameDay (aka !hasDayOffset) then hour from user-generated input. + // isSameDay is checked first to reduce the amount of conditionals per loop. + if (isSameDay) { + for (const [hourPoint, dayOffset, timezoneOffset] of timezoneTupleList) { + if (dayOffset === 0 && hour === hourPoint) { + profile.timezone = timezoneOffset; + } + } + } else { + for (const [hourPoint, dayOffset, timezoneOffset] of timezoneTupleList) { + if (dayOffset !== 0 && hour === hourPoint) { + profile.timezone = timezoneOffset; + } + } + } + } else { + // If it's a unique hour, just search through the tuple list and find the matching entry. + for (const [hourPoint, _dayOffset, timezoneOffset] of timezoneTupleList) { + if (hour === hourPoint) { + profile.timezone = timezoneOffset; + } + } + } + + // I should note that error handling should be added sometime because await throws an exception on Promise.reject. + const hasDST = await confirm( + await send("Does your timezone change based on daylight savings?"), + author.id + ); + + const finalize = () => { + Storage.save(); + send( + "You've finished setting up your timezone! Just check to see if this looks right, and if it doesn't, run this setup again.", + getTimeEmbed(author) + ); + }; + + if (hasDST) { + const finalizeDST = (region: DST) => { + profile.daylightSavingsRegion = region; + + // If daylight savings is active, subtract the timezone offset by one to store the standard time. + if (hasDaylightSavings(region)) { + profile.timezone!--; + } + + finalize(); + }; + + const index = await askMultipleChoice(await send(DST_NOTE_SETUP), author.id, 3); + + switch (index) { + case 0: + finalizeDST("na"); + break; + case 1: + finalizeDST("eu"); + break; + case 2: + finalizeDST("sh"); + break; + } + } else { + finalize(); + } + + return; } }), delete: new NamedCommand({ description: "Delete your timezone information.", - async run({send, channel, author}) { - prompt( - await send( - "Are you sure you want to delete your timezone information?\n*(This message will automatically be deleted after 10 seconds.)*" - ), - author.id, - () => { - const profile = Storage.getUser(author.id); - profile.timezone = null; - profile.daylightSavingsRegion = null; - Storage.save(); - } + async run({send, author}) { + const result = await confirm( + await send("Are you sure you want to delete your timezone information?"), + author.id ); + + if (result) { + const profile = Storage.getUser(author.id); + profile.timezone = null; + profile.daylightSavingsRegion = null; + Storage.save(); + } } }), utc: new NamedCommand({ description: "Displays UTC time.", - async run({send, channel}) { + async run({send}) { const time = moment().utc(); send({ @@ -386,15 +378,15 @@ export default new NamedCommand({ id: "user", user: new Command({ description: "See what time it is for someone else.", - async run({send, channel, args}) { + async run({send, args}) { send(getTimeEmbed(args[0])); } }), any: new RestCommand({ description: "See what time it is for someone else (by their username).", - async run({send, channel, args, guild, combined}) { + async run({send, guild, combined}) { const member = await getMemberByName(guild!, combined); - if (member instanceof GuildMember) send(getTimeEmbed(member.user)); + if (typeof member !== "string") send(getTimeEmbed(member.user)); else send(member); } }) diff --git a/src/core/command.ts b/src/core/command.ts index cded03c..7e024da 100644 --- a/src/core/command.ts +++ b/src/core/command.ts @@ -11,7 +11,7 @@ import { GuildChannel, Channel } from "discord.js"; -import {getChannelByID, getGuildByID, getMessageByID, getUserByID, SingleMessageOptions, SendFunction} from "./libd"; +import {getChannelByID, getGuildByID, getMessageByID, getUserByID, SendFunction} from "./libd"; import {hasPermission, getPermissionLevel, getPermissionName} from "./permissions"; import {getPrefix} from "./interface"; import {parseVars, requireAllCasesHandledFor} from "../lib"; @@ -244,18 +244,14 @@ export class Command extends BaseCommand { } // Go through the arguments provided and find the right subcommand, then execute with the given arguments. - // Will return null if it successfully executes, SingleMessageOptions if there's an error (to let the user know what it is). + // Will return null if it successfully executes, string if there's an error (to let the user know what it is). // // Calls the resulting subcommand's execute method in order to make more modular code, basically pushing the chain of execution to the subcommand. // For example, a numeric subcommand would accept args of [4] then execute on it. // // Because each Command instance is isolated from others, it becomes practically impossible to predict the total amount of subcommands when isolating the code to handle each individual layer of recursion. // Therefore, if a Command is declared as a rest type, any typed args that come at the end must be handled manually. - public async execute( - args: string[], - menu: CommandMenu, - metadata: ExecuteCommandMetadata - ): Promise { + public async execute(args: string[], menu: CommandMenu, metadata: ExecuteCommandMetadata): Promise { // Update inherited properties if the current command specifies a property. // In case there are no initial arguments, these should go first so that it can register. if (this.permission !== -1) metadata.permission = this.permission; @@ -292,9 +288,7 @@ export class Command extends BaseCommand { const errorMessage = error.stack ?? error; console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`); - return { - content: `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\`` - }; + return `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\``; } } @@ -313,15 +307,13 @@ export class Command extends BaseCommand { const id = patterns.channel.exec(param)![1]; const channel = await getChannelByID(id); - if (channel instanceof Channel) { + if (typeof channel !== "string") { 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!` - }; + return `\`${id}\` is not a valid text channel!`; } } else { return channel; @@ -330,9 +322,7 @@ export class Command extends BaseCommand { const id = patterns.role.exec(param)![1]; if (!menu.guild) { - return { - content: "You can't use role parameters in DM channels!" - }; + return "You can't use role parameters in DM channels!"; } const role = menu.guild.roles.cache.get(id); @@ -342,9 +332,7 @@ export class Command extends BaseCommand { menu.args.push(role); return this.role.execute(args, menu, metadata); } else { - return { - content: `\`${id}\` is not a valid role in this server!` - }; + return `\`${id}\` is not a valid role in this server!`; } } else if (this.emote && patterns.emote.test(param)) { const id = patterns.emote.exec(param)![1]; @@ -355,9 +343,7 @@ export class Command extends BaseCommand { menu.args.push(emote); return this.emote.execute(args, menu, metadata); } else { - return { - content: `\`${id}\` isn't a valid emote!` - }; + return `\`${id}\` isn't a valid emote!`; } } else if (this.message && (isMessageLink || isMessagePair)) { let channelID = ""; @@ -375,7 +361,7 @@ export class Command extends BaseCommand { const message = await getMessageByID(channelID, messageID); - if (message instanceof Message) { + if (typeof message !== "string") { metadata.symbolicArgs.push(""); menu.args.push(message); return this.message.execute(args, menu, metadata); @@ -386,7 +372,7 @@ export class Command extends BaseCommand { const id = patterns.user.exec(param)![1]; const user = await getUserByID(id); - if (user instanceof User) { + if (typeof user !== "string") { metadata.symbolicArgs.push(""); menu.args.push(user); return this.user.execute(args, menu, metadata); @@ -403,24 +389,20 @@ export class Command extends BaseCommand { case "channel": const channel = await getChannelByID(id); - if (channel instanceof Channel) { + if (typeof channel !== "string") { 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!` - }; + return `\`${id}\` is not a valid text channel!`; } } else { return channel; } case "role": if (!menu.guild) { - return { - content: "You can't use role parameters in DM channels!" - }; + return "You can't use role parameters in DM channels!"; } const role = menu.guild.roles.cache.get(id); @@ -429,9 +411,7 @@ export class Command extends BaseCommand { menu.args.push(role); return this.id.execute(args, menu, metadata); } else { - return { - content: `\`${id}\` isn't a valid role in this server!` - }; + return `\`${id}\` isn't a valid role in this server!`; } case "emote": const emote = menu.client.emojis.cache.get(id); @@ -440,14 +420,12 @@ export class Command extends BaseCommand { menu.args.push(emote); return this.id.execute(args, menu, metadata); } else { - return { - content: `\`${id}\` isn't a valid emote!` - }; + return `\`${id}\` isn't a valid emote!`; } case "message": const message = await getMessageByID(menu.channel, id); - if (message instanceof Message) { + if (typeof message !== "string") { menu.args.push(message); return this.id.execute(args, menu, metadata); } else { @@ -456,7 +434,7 @@ export class Command extends BaseCommand { case "user": const user = await getUserByID(id); - if (user instanceof User) { + if (typeof user !== "string") { menu.args.push(user); return this.id.execute(args, menu, metadata); } else { @@ -465,7 +443,7 @@ export class Command extends BaseCommand { case "guild": const guild = getGuildByID(id); - if (guild instanceof Guild) { + if (typeof guild !== "string") { menu.args.push(guild); return this.id.execute(args, menu, metadata); } else { @@ -489,11 +467,9 @@ export class Command extends BaseCommand { return this.any.execute(args.join(" "), menu, metadata); } else { metadata.symbolicArgs.push(`"${param}"`); - return { - content: `No valid command sequence matching \`${metadata.header} ${metadata.symbolicArgs.join( - " " - )}\` found.` - }; + return `No valid command sequence matching \`${metadata.header} ${metadata.symbolicArgs.join( + " " + )}\` found.`; } // Note: Do NOT add a return statement here. In case one of the other sections is missing @@ -682,7 +658,7 @@ export class RestCommand extends BaseCommand { combined: string, menu: CommandMenu, metadata: ExecuteCommandMetadata - ): Promise { + ): Promise { // Update inherited properties if the current command specifies a property. // In case there are no initial arguments, these should go first so that it can register. if (this.permission !== -1) metadata.permission = this.permission; @@ -716,9 +692,7 @@ export class RestCommand extends BaseCommand { const errorMessage = error.stack ?? error; console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`); - return { - content: `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\`` - }; + return `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\``; } } @@ -743,36 +717,34 @@ export class RestCommand extends BaseCommand { // See if there is anything that'll prevent the user from executing the command. // Returns null if successful, otherwise returns a message with the error. -function canExecute(menu: CommandMenu, metadata: ExecuteCommandMetadata): SingleMessageOptions | null { +function canExecute(menu: CommandMenu, metadata: ExecuteCommandMetadata): string | null { // 1. Does this command specify a required channel type? If so, does the channel type match? if ( metadata.channelType === CHANNEL_TYPE.GUILD && (!(menu.channel instanceof GuildChannel) || menu.guild === null || menu.member === null) ) { - return {content: "This command must be executed in a server."}; + return "This command must be executed in a server."; } else if ( metadata.channelType === CHANNEL_TYPE.DM && (menu.channel.type !== "dm" || menu.guild !== null || menu.member !== null) ) { - return {content: "This command must be executed as a direct message."}; + return "This command must be executed as a direct message."; } // 2. Is this an NSFW command where the channel prevents such use? (DM channels bypass this requirement.) if (metadata.nsfw && menu.channel.type !== "dm" && !menu.channel.nsfw) { - return {content: "This command must be executed in either an NSFW channel or as a direct message."}; + return "This command must be executed in either an NSFW channel or as a direct message."; } // 3. Does the user have permission to execute the command? if (!hasPermission(menu.author, menu.member, metadata.permission)) { const userPermLevel = getPermissionLevel(menu.author, menu.member); - return { - content: `You don't have access to this command! Your permission level is \`${getPermissionName( - userPermLevel - )}\` (${userPermLevel}), but this command requires a permission level of \`${getPermissionName( - metadata.permission - )}\` (${metadata.permission}).` - }; + return `You don't have access to this command! Your permission level is \`${getPermissionName( + userPermLevel + )}\` (${userPermLevel}), but this command requires a permission level of \`${getPermissionName( + metadata.permission + )}\` (${metadata.permission}).`; } return null; diff --git a/src/core/libd.ts b/src/core/libd.ts index c9d5bcf..58338a5 100644 --- a/src/core/libd.ts +++ b/src/core/libd.ts @@ -15,7 +15,9 @@ import { MessageAdditions, SplitOptions, APIMessage, - StringResolvable + StringResolvable, + EmojiIdentifierResolvable, + MessageReaction } from "discord.js"; import {unreactEventListeners, replyEventListeners} from "./eventListeners"; import {client} from "./interface"; @@ -31,19 +33,6 @@ export type SendFunction = (( ((content: StringResolvable, options: MessageOptions & {split: true | SplitOptions}) => Promise) & ((content: StringResolvable, options: MessageOptions) => Promise); -/** - * Tests if a bot has a certain permission in a specified guild. - */ -export function botHasPermission(guild: Guild | null, permission: number): boolean { - 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. -// 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 = "➡️"; @@ -56,34 +45,35 @@ const FIVE_FORWARDS_EMOJI = "⏩"; */ export async function paginate( send: SendFunction, - senderID: string, - total: number, - callback: (page: number, hasMultiplePages: boolean) => SingleMessageOptions, + onTurnPage: (page: number, hasMultiplePages: boolean) => SingleMessageOptions, + totalPages: number, + listenTo: string | null = null, duration = 60000 -) { - const hasMultiplePages = total > 1; - const message = await send(callback(0, hasMultiplePages)); +): Promise { + const hasMultiplePages = totalPages > 1; + const message = await send(onTurnPage(0, hasMultiplePages)); if (hasMultiplePages) { let page = 0; const turn = (amount: number) => { page += amount; - if (page >= total) { - page %= total; + if (page >= totalPages) { + page %= totalPages; } else if (page < 0) { // Assuming 3 total pages, it's a bit tricker, but if we just take the modulo of the absolute value (|page| % total), we get (1 2 0 ...), and we just need the pattern (2 1 0 ...). It needs to reverse order except for when it's 0. I want to find a better solution, but for the time being... total - (|page| % total) unless (|page| % total) = 0, then return 0. - const flattened = Math.abs(page) % total; - if (flattened !== 0) page = total - flattened; + const flattened = Math.abs(page) % totalPages; + if (flattened !== 0) page = totalPages - flattened; } - message.edit(callback(page, true)); + message.edit(onTurnPage(page, true)); }; const handle = (emote: string, reacterID: string) => { - if (senderID === reacterID) { + if (reacterID === listenTo || listenTo === null) { + collector.resetTimer(); // The timer refresh MUST be present in both react and unreact. switch (emote) { case FIVE_BACKWARDS_EMOJI: - if (total > 5) turn(-5); + if (totalPages > 5) turn(-5); break; case BACKWARDS_EMOJI: turn(-1); @@ -92,28 +82,28 @@ export async function paginate( turn(1); break; case FIVE_FORWARDS_EMOJI: - if (total > 5) turn(5); + if (totalPages > 5) turn(5); break; } } }; // Listen for reactions and call the handler. - let backwardsReactionFive = total > 5 ? await message.react(FIVE_BACKWARDS_EMOJI) : null; + let backwardsReactionFive = totalPages > 5 ? await message.react(FIVE_BACKWARDS_EMOJI) : null; let backwardsReaction = await message.react(BACKWARDS_EMOJI); let forwardsReaction = await message.react(FORWARDS_EMOJI); - let forwardsReactionFive = total > 5 ? await message.react(FIVE_FORWARDS_EMOJI) : null; + let forwardsReactionFive = totalPages > 5 ? await message.react(FIVE_FORWARDS_EMOJI) : null; unreactEventListeners.set(message.id, handle); const collector = message.createReactionCollector( (reaction, user) => { - if (user.id === senderID) { + // This check is actually redundant because of handle(). + if (user.id === listenTo || listenTo === null) { // The reason this is inside the call is because it's possible to switch a user's permissions halfway and suddenly throw an error. // This will dynamically adjust for that, switching modes depending on whether it currently has the "Manage Messages" permission. const canDeleteEmotes = botHasPermission(message.guild, Permissions.FLAGS.MANAGE_MESSAGES); handle(reaction.emoji.name, user.id); if (canDeleteEmotes) reaction.users.remove(user); - collector.resetTimer(); } return false; @@ -134,100 +124,73 @@ export async function paginate( } } -// Waits for the sender to either confirm an action or let it pass (and delete the message). -// This should probably be renamed to "confirm" now that I think of it, "prompt" is better used elsewhere. -// Append "\n*(This message will automatically be deleted after 10 seconds.)*" in the future? -/** - * Prompts the user about a decision before following through. - */ -export async function prompt(message: Message, senderID: string, onConfirm: () => void, duration = 10000) { - let isDeleted = false; +//export function generateMulti +// paginate after generateonetimeprompt - message.react("✅"); - await message.awaitReactions( - (reaction, user) => { - if (user.id === senderID) { - if (reaction.emoji.name === "✅") { - onConfirm(); - isDeleted = true; - message.delete(); - } - } - - // CollectorFilter requires a boolean to be returned. - // My guess is that the return value of awaitReactions can be altered by making a boolean filter. - // However, because that's not my concern with this command, I don't have to worry about it. - // May as well just set it to false because I'm not concerned with collecting any reactions. - return false; - }, - {time: duration} - ); - - if (!isDeleted) message.delete(); -} - -// Asks the user for some input using the inline reply feature. The message here is a message you send beforehand. -// If the reply is rejected, reply with an error message (when stable support comes from discord.js). -// Append "\n*(Note: Make sure to use Discord's inline reply feature or this won't work!)*" in the future? And also the "you can now reply to this message" edit. -export function ask( +// Returns null if timed out, otherwise, returns the value. +export function generateOneTimePrompt( message: Message, - senderID: string, - condition: (reply: string) => boolean, - onSuccess: () => void, - onReject: () => string, - timeout = 60000 -) { - const referenceID = `${message.channel.id}-${message.id}`; + stack: {[emote: string]: T}, + listenTo: string | null = null, + duration = 60000 +): Promise { + return new Promise(async (resolve) => { + // First, start reacting to the message in order. + reactInOrder(message, Object.keys(stack)); - replyEventListeners.set(referenceID, (reply) => { - if (reply.author.id === senderID) { - if (condition(reply.content)) { - onSuccess(); - replyEventListeners.delete(referenceID); - } else { - reply.reply(onReject()); - } - } - }); - - setTimeout(() => { - replyEventListeners.set(referenceID, (reply) => { - reply.reply("that action timed out, try using the command again"); - replyEventListeners.delete(referenceID); - }); - }, timeout); -} - -export function askYesOrNo(message: Message, senderID: string, timeout = 30000): Promise { - return new Promise(async (resolve, reject) => { - let isDeleted = false; - - await message.react("✅"); - message.react("❌"); + // Then setup the reaction listener in parallel. await message.awaitReactions( - (reaction, user) => { - if (user.id === senderID) { - const isCheckReacted = reaction.emoji.name === "✅"; + (reaction: MessageReaction, user: User) => { + if (user.id === listenTo || listenTo === null) { + const emote = reaction.emoji.name; - if (isCheckReacted || reaction.emoji.name === "❌") { - resolve(isCheckReacted); - isDeleted = true; + if (emote in stack) { + resolve(stack[emote]); message.delete(); } } + // CollectorFilter requires a boolean to be returned. + // My guess is that the return value of awaitReactions can be altered by making a boolean filter. + // However, because that's not my concern with this command, I don't have to worry about it. + // May as well just set it to false because I'm not concerned with collecting any reactions. return false; }, - {time: timeout} + {time: duration} ); - if (!isDeleted) { + if (!message.deleted) { message.delete(); - reject("Prompt timed out."); + resolve(null); } }); } +// Start a parallel chain of ordered reactions, allowing a collector to end early. +// Check if the collector ended early by seeing if the message is already deleted. +// Though apparently, message.deleted doesn't seem to update fast enough, so just put a try catch block on message.react(). +async function reactInOrder(message: Message, emotes: EmojiIdentifierResolvable[]): Promise { + for (const emote of emotes) { + try { + await message.react(emote); + } catch { + return; + } + } +} + +export function confirm(message: Message, senderID: string, timeout = 30000): Promise { + return generateOneTimePrompt( + message, + { + "✅": true, + "❌": false + }, + senderID, + timeout + ); +} + // This MUST be split into an array. These emojis are made up of several characters each, adding up to 29 in length. const multiNumbers = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6️⃣", "7️⃣", "8️⃣", "9️⃣", "🔟"]; @@ -236,40 +199,47 @@ const multiNumbers = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6 export async function askMultipleChoice( message: Message, senderID: string, - callbackStack: (() => void)[], + choices: number, timeout = 90000 -) { - if (callbackStack.length > multiNumbers.length) { - message.channel.send( - `\`ERROR: The amount of callbacks in "askMultipleChoice" must not exceed the total amount of allowed options (${multiNumbers.length})!\`` +): Promise { + if (choices > multiNumbers.length) + throw new Error( + `askMultipleChoice only accepts up to ${multiNumbers.length} options, ${choices} was provided.` ); - return; - } + const numbers: {[emote: string]: number} = {}; + for (let i = 0; i < choices; i++) numbers[multiNumbers[i]] = i; + return generateOneTimePrompt(message, numbers, senderID, timeout); +} - let isDeleted = false; +// Asks the user for some input using the inline reply feature. The message here is a message you send beforehand. +// If the reply is rejected, reply with an error message (when stable support comes from discord.js). +export function askForReply(message: Message, listenTo: string, timeout?: number): Promise { + return new Promise((resolve) => { + const referenceID = `${message.channel.id}-${message.id}`; - for (let i = 0; i < callbackStack.length; i++) { - await message.react(multiNumbers[i]); - } - - await message.awaitReactions( - (reaction, user) => { - if (user.id === senderID) { - const index = multiNumbers.indexOf(reaction.emoji.name); - - if (index !== -1) { - callbackStack[index](); - isDeleted = true; - message.delete(); - } + replyEventListeners.set(referenceID, (reply) => { + if (reply.author.id === listenTo) { + message.delete(); + replyEventListeners.delete(referenceID); + resolve(reply); } + }); - return false; - }, - {time: timeout} - ); + if (timeout) { + client.setTimeout(() => { + if (!message.deleted) message.delete(); + replyEventListeners.delete(referenceID); + resolve(null); + }, timeout); + } + }); +} - if (!isDeleted) message.delete(); +/** + * Tests if a bot has a certain permission in a specified guild. + */ +export function botHasPermission(guild: Guild | null, permission: number): boolean { + return !!guild?.me?.hasPermission(permission); } // For "get x by y" methods: @@ -277,79 +247,75 @@ export async function askMultipleChoice( // 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 { +export function getGuildByID(id: string): Guild | string { 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 `The guild \`${guild.name}\` (ID: \`${id}\`) is unavailable due to an outage.`; } else { - return { - content: `No guild found by the ID of \`${id}\`!` - }; + return `No guild found by the ID of \`${id}\`!`; } } -export function getGuildByName(name: string): Guild | SingleMessageOptions { +export function getGuildByName(name: string): Guild | string { 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 `The guild \`${guild.name}\` (ID: \`${guild.id}\`) is unavailable due to an outage.`; } else { - return { - content: `No guild found by the name of \`${name}\`!` - }; + return `No guild found by the name of \`${name}\`!`; } } -export async function getChannelByID(id: string): Promise { +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}\`!`}; + return `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 { +export function getChannelByName(name: string): GuildChannel | string { 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}\`!`}; + else return `No channel found by the name of \`${name}\`!`; } export async function getMessageByID( channel: TextChannel | DMChannel | NewsChannel | string, id: string -): Promise { +): 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 if (targetChannel instanceof Channel) return `\`${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}!`}; + return `\`${id}\` isn't a valid message of the channel ${channel}!`; } } -export async function getUserByID(id: string): Promise { +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}\`!`}; + return `No user found by the ID of \`${id}\`!`; } } // Also check tags (if provided) to narrow down users. -export function getUserByName(name: string): User | SingleMessageOptions { +export function getUserByName(name: string): User | string { let query = name.toLowerCase(); const tagMatch = /^(.+?)#(\d{4})$/.exec(name); let tag: string | null = null; @@ -366,19 +332,19 @@ export function getUserByName(name: string): User | SingleMessageOptions { }); if (user) return user; - else return {content: `No user found by the name of \`${name}\`!`}; + else return `No user found by the name of \`${name}\`!`; } -export async function getMemberByID(guild: Guild, id: string): Promise { +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}\`!`}; + return `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 { +export async function getMemberByName(guild: Guild, name: string): Promise { const member = ( await guild.members.fetch({ query: name, @@ -395,9 +361,9 @@ export async function getMemberByName(guild: Guild, name: string): Promise { const linkMessage = await getMessageByID(channelID, messageID); // If it's an invalid link (or the bot doesn't have access to it). - if (!(linkMessage instanceof Message)) { + if (typeof linkMessage === "string") { return message.channel.send("I don't have access to that channel!"); } From 51d19d5787ea79e34d01da7c0224ceb28f65af2b Mon Sep 17 00:00:00 2001 From: Keanu Date: Sun, 11 Apr 2021 10:58:06 +0200 Subject: [PATCH 12/14] Added Discord.JS Docs command. --- src/commands/utility/docs.ts | 17 +++++++++++++++++ 1 file changed, 17 insertions(+) create mode 100644 src/commands/utility/docs.ts diff --git a/src/commands/utility/docs.ts b/src/commands/utility/docs.ts new file mode 100644 index 0000000..8c1d74b --- /dev/null +++ b/src/commands/utility/docs.ts @@ -0,0 +1,17 @@ +import {NamedCommand, RestCommand} from "../../core"; +import {URL} from "url"; +import {getContent} from "../../lib"; + +export default new NamedCommand({ + description: "Provides you with info from the Discord.JS docs.", + run: "You need to specify a term to query the docs with.", + any: new RestCommand({ + description: "What to query the docs with.", + async run({send, args}) { + var queryString = args[0]; + let url = new URL(`https://djsdocs.sorta.moe/v2/embed?src=master&q=${queryString}`); + const content = await getContent(url.toString()); + return send({embed: content}); + } + }) +}); From 0a265dcd5c2f0784f421cbddba0f60ea612caf1e Mon Sep 17 00:00:00 2001 From: Keanu Date: Sun, 11 Apr 2021 11:11:21 +0200 Subject: [PATCH 13/14] Removed unused run args and Command imports. --- src/commands/fun/8ball.ts | 2 +- src/commands/fun/cookie.ts | 4 ++-- src/commands/fun/eco.ts | 2 +- src/commands/fun/figlet.ts | 4 ++-- src/commands/fun/insult.ts | 4 ++-- src/commands/fun/love.ts | 4 ++-- src/commands/fun/neko.ts | 4 ++-- src/commands/fun/ok.ts | 4 ++-- src/commands/fun/owoify.ts | 4 ++-- src/commands/fun/party.ts | 4 ++-- src/commands/fun/poll.ts | 4 ++-- src/commands/fun/ravi.ts | 4 ++-- src/commands/fun/thonk.ts | 6 +++--- src/commands/fun/urban.ts | 4 ++-- src/commands/fun/vaporwave.ts | 4 ++-- src/commands/fun/weather.ts | 4 ++-- src/commands/fun/whois.ts | 6 +++--- src/commands/utility/calc.ts | 4 ++-- src/commands/utility/code.ts | 2 +- src/commands/utility/desc.ts | 4 ++-- src/commands/utility/emote.ts | 4 ++-- src/commands/utility/info.ts | 18 +++++++++--------- src/commands/utility/invite.ts | 4 ++-- src/commands/utility/lsemotes.ts | 6 +++--- src/commands/utility/react.ts | 4 ++-- src/commands/utility/say.ts | 4 ++-- src/commands/utility/scanemotes.ts | 6 +++--- src/commands/utility/shorten.ts | 2 +- src/commands/utility/streaminfo.ts | 6 +++--- src/commands/utility/todo.ts | 10 +++++----- src/commands/utility/translate.ts | 2 +- 31 files changed, 72 insertions(+), 72 deletions(-) diff --git a/src/commands/fun/8ball.ts b/src/commands/fun/8ball.ts index 898eafe..f79a2c0 100644 --- a/src/commands/fun/8ball.ts +++ b/src/commands/fun/8ball.ts @@ -30,7 +30,7 @@ export default new NamedCommand({ run: "Please provide a question.", any: new Command({ description: "Question to ask the 8-ball.", - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, message}) { const sender = message.author; send(`${random(responses)} <@${sender.id}>`); } diff --git a/src/commands/fun/cookie.ts b/src/commands/fun/cookie.ts index 2275c08..71d9c20 100644 --- a/src/commands/fun/cookie.ts +++ b/src/commands/fun/cookie.ts @@ -31,7 +31,7 @@ export default new NamedCommand({ run: ":cookie: Here's a cookie!", subcommands: { all: new NamedCommand({ - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, author}) { send(`${author} gave everybody a cookie!`); } }) @@ -39,7 +39,7 @@ export default new NamedCommand({ id: "user", user: new Command({ description: "User to give cookie to.", - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, author, args}) { const sender = author; const mention: User = args[0]; diff --git a/src/commands/fun/eco.ts b/src/commands/fun/eco.ts index 241411d..4644101 100644 --- a/src/commands/fun/eco.ts +++ b/src/commands/fun/eco.ts @@ -34,7 +34,7 @@ export default new NamedCommand({ }), any: new RestCommand({ description: "See how much money someone else has by using their username.", - async run({send, guild, channel, args, message, combined}) { + async run({send, guild, channel, combined}) { if (isAuthorized(guild, channel)) { const member = await getMemberByName(guild!, combined); if (typeof member !== "string") send(getMoneyEmbed(member.user)); diff --git a/src/commands/fun/figlet.ts b/src/commands/fun/figlet.ts index a25ed2a..b2f9bb0 100644 --- a/src/commands/fun/figlet.ts +++ b/src/commands/fun/figlet.ts @@ -1,11 +1,11 @@ -import {Command, NamedCommand, RestCommand} from "../../core"; +import {NamedCommand, RestCommand} from "../../core"; import figlet from "figlet"; export default new NamedCommand({ description: "Generates a figlet of your input.", run: "You have to provide input for me to create a figlet!", any: new RestCommand({ - async run({send, message, channel, guild, author, member, client, args, combined}) { + async run({send, combined}) { return send( figlet.textSync(combined, { horizontalLayout: "full" diff --git a/src/commands/fun/insult.ts b/src/commands/fun/insult.ts index 3cd982d..251444a 100644 --- a/src/commands/fun/insult.ts +++ b/src/commands/fun/insult.ts @@ -1,8 +1,8 @@ -import {Command, NamedCommand} from "../../core"; +import {NamedCommand} from "../../core"; export default new NamedCommand({ description: "Insult TravBot! >:D", - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, channel, author}) { channel.startTyping(); setTimeout(() => { send( diff --git a/src/commands/fun/love.ts b/src/commands/fun/love.ts index e7d2557..25a7c8a 100644 --- a/src/commands/fun/love.ts +++ b/src/commands/fun/love.ts @@ -1,9 +1,9 @@ -import {Command, NamedCommand, CHANNEL_TYPE} from "../../core"; +import {NamedCommand, CHANNEL_TYPE} from "../../core"; export default new NamedCommand({ description: "Chooses someone to love.", channelType: CHANNEL_TYPE.GUILD, - async run({send, message, channel, guild, author, client, args}) { + async run({send, guild}) { const member = guild!.members.cache.random(); send(`I love ${member.nickname ?? member.user.username}!`); } diff --git a/src/commands/fun/neko.ts b/src/commands/fun/neko.ts index 1722232..f430406 100644 --- a/src/commands/fun/neko.ts +++ b/src/commands/fun/neko.ts @@ -36,12 +36,12 @@ const endpoints: {sfw: {[key: string]: string}} = { export default new NamedCommand({ description: "Provides you with a random image with the selected argument.", - async run({send, message, channel, guild, author, member, client, args}) { + async run({send}) { send(`Please provide an image type. Available arguments:\n\`[${Object.keys(endpoints.sfw).join(", ")}]\`.`); }, any: new Command({ description: "Image type to send.", - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, args}) { const arg = args[0]; if (!(arg in endpoints.sfw)) return send("Couldn't find that endpoint!"); let url = new URL(`https://nekos.life/api/v2${endpoints.sfw[arg]}`); diff --git a/src/commands/fun/ok.ts b/src/commands/fun/ok.ts index 66bfa6b..13296a3 100644 --- a/src/commands/fun/ok.ts +++ b/src/commands/fun/ok.ts @@ -1,4 +1,4 @@ -import {Command, NamedCommand} from "../../core"; +import {NamedCommand} from "../../core"; import {random} from "../../lib"; const responses = [ @@ -61,7 +61,7 @@ const responses = [ export default new NamedCommand({ description: "Sends random ok message.", - async run({send, message, channel, guild, author, member, client, args}) { + async run({send}) { send(`ok ${random(responses)}`); } }); diff --git a/src/commands/fun/owoify.ts b/src/commands/fun/owoify.ts index cbc4ccf..3cc2dd5 100644 --- a/src/commands/fun/owoify.ts +++ b/src/commands/fun/owoify.ts @@ -1,4 +1,4 @@ -import {Command, NamedCommand, RestCommand} from "../../core"; +import {NamedCommand, RestCommand} from "../../core"; import {getContent} from "../../lib"; import {URL} from "url"; @@ -6,7 +6,7 @@ export default new NamedCommand({ description: "OwO-ifies the input.", run: "You need to specify some text to owoify.", any: new RestCommand({ - async run({send, message, channel, guild, author, member, client, args, combined}) { + async run({send, combined}) { let url = new URL(`https://nekos.life/api/v2/owoify?text=${combined}`); const content = (await getContent(url.toString())) as any; // Apparently, the object in question is {owo: string}. send(content.owo); diff --git a/src/commands/fun/party.ts b/src/commands/fun/party.ts index db4c0e3..ce58b13 100644 --- a/src/commands/fun/party.ts +++ b/src/commands/fun/party.ts @@ -1,8 +1,8 @@ -import {Command, NamedCommand} from "../../core"; +import {NamedCommand} from "../../core"; export default new NamedCommand({ description: "Initiates a celebratory stream from the bot.", - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, client}) { send("This calls for a celebration!"); client.user!.setActivity({ type: "STREAMING", diff --git a/src/commands/fun/poll.ts b/src/commands/fun/poll.ts index 5795db1..3909968 100644 --- a/src/commands/fun/poll.ts +++ b/src/commands/fun/poll.ts @@ -1,5 +1,5 @@ import {MessageEmbed} from "discord.js"; -import {Command, NamedCommand, RestCommand} from "../../core"; +import {NamedCommand, RestCommand} from "../../core"; export default new NamedCommand({ description: "Create a poll.", @@ -7,7 +7,7 @@ export default new NamedCommand({ run: "Please provide a question.", any: new RestCommand({ description: "Question for the poll.", - async run({send, message, channel, guild, author, member, client, args, combined}) { + async run({send, message, combined}) { const embed = new MessageEmbed() .setAuthor( `Poll created by ${message.author.username}`, diff --git a/src/commands/fun/ravi.ts b/src/commands/fun/ravi.ts index e0862e0..ff64376 100644 --- a/src/commands/fun/ravi.ts +++ b/src/commands/fun/ravi.ts @@ -4,7 +4,7 @@ import {Random} from "../../lib"; export default new NamedCommand({ description: "Ravioli ravioli...", usage: "[number from 1 to 9]", - async run({send, message, channel, guild, author, member, client, args}) { + async run({send}) { send({ embed: { title: "Ravioli ravioli...", @@ -18,7 +18,7 @@ export default new NamedCommand({ }); }, number: new Command({ - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, args}) { const arg: number = args[0]; if (arg >= 1 && arg <= 9) { diff --git a/src/commands/fun/thonk.ts b/src/commands/fun/thonk.ts index de09d20..821216f 100644 --- a/src/commands/fun/thonk.ts +++ b/src/commands/fun/thonk.ts @@ -1,4 +1,4 @@ -import {Command, NamedCommand, RestCommand} from "../../core"; +import {NamedCommand, RestCommand} from "../../core"; const letters: {[letter: string]: string[]} = { a: "aáàảãạâấầẩẫậăắằẳẵặ".split(""), @@ -34,7 +34,7 @@ let phrase = "I have no currently set phrase!"; export default new NamedCommand({ description: "Transforms your text into vietnamese.", usage: "thonk ([text])", - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, author}) { const msg = await send(transform(phrase)); msg.createReactionCollector( (reaction, user) => { @@ -45,7 +45,7 @@ export default new NamedCommand({ ); }, any: new RestCommand({ - async run({send, message, channel, guild, author, member, client, args, combined}) { + async run({send, author, combined}) { const msg = await send(transform(combined)); msg.createReactionCollector( (reaction, user) => { diff --git a/src/commands/fun/urban.ts b/src/commands/fun/urban.ts index 928ca57..9964008 100644 --- a/src/commands/fun/urban.ts +++ b/src/commands/fun/urban.ts @@ -1,4 +1,4 @@ -import {Command, NamedCommand, RestCommand} from "../../core"; +import {NamedCommand, RestCommand} from "../../core"; import {MessageEmbed} from "discord.js"; import urban from "relevant-urban"; @@ -6,7 +6,7 @@ export default new NamedCommand({ description: "Gives you a definition of the inputted word.", run: "Please input a word.", any: new RestCommand({ - async run({send, message, channel, guild, author, member, client, args, combined}) { + async run({send, combined}) { // [Bug Fix]: Use encodeURIComponent() when emojis are used: "TypeError [ERR_UNESCAPED_CHARACTERS]: Request path contains unescaped characters" urban(encodeURIComponent(combined)) .then((res) => { diff --git a/src/commands/fun/vaporwave.ts b/src/commands/fun/vaporwave.ts index 71ae065..6486c25 100644 --- a/src/commands/fun/vaporwave.ts +++ b/src/commands/fun/vaporwave.ts @@ -1,4 +1,4 @@ -import {Command, NamedCommand, RestCommand} from "../../core"; +import {NamedCommand, RestCommand} from "../../core"; const vaporwave = (() => { const map = new Map(); @@ -25,7 +25,7 @@ export default new NamedCommand({ description: "Transforms your text into vaporwave.", run: "You need to enter some text!", any: new RestCommand({ - async run({send, message, channel, guild, author, member, client, args, combined}) { + async run({send, combined}) { const text = getVaporwaveText(combined); if (text !== "") send(text); else send("Make sure to enter at least one valid character."); diff --git a/src/commands/fun/weather.ts b/src/commands/fun/weather.ts index 44c82b7..b30daa1 100644 --- a/src/commands/fun/weather.ts +++ b/src/commands/fun/weather.ts @@ -1,4 +1,4 @@ -import {Command, NamedCommand, RestCommand} from "../../core"; +import {NamedCommand, RestCommand} from "../../core"; import {MessageEmbed} from "discord.js"; import {find} from "weather-js"; @@ -6,7 +6,7 @@ export default new NamedCommand({ description: "Shows weather info of specified location.", run: "You need to provide a city.", any: new RestCommand({ - async run({send, message, channel, guild, author, member, client, args, combined}) { + async run({send, combined}) { find( { search: combined, diff --git a/src/commands/fun/whois.ts b/src/commands/fun/whois.ts index 507c14f..92d29d3 100644 --- a/src/commands/fun/whois.ts +++ b/src/commands/fun/whois.ts @@ -43,7 +43,7 @@ const registry: {[id: string]: string} = { export default new NamedCommand({ description: "Tells you who you or the specified user is.", aliases: ["whoami"], - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, author}) { const id = author.id; if (id in registry) { @@ -54,7 +54,7 @@ export default new NamedCommand({ }, id: "user", user: new Command({ - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, args}) { const user: User = args[0]; const id = user.id; @@ -67,7 +67,7 @@ export default new NamedCommand({ }), any: new RestCommand({ channelType: CHANNEL_TYPE.GUILD, - async run({send, message, channel, guild, author, client, args, combined}) { + async run({send, guild, combined}) { const member = await getMemberByName(guild!, combined); if (typeof member !== "string") { diff --git a/src/commands/utility/calc.ts b/src/commands/utility/calc.ts index ac0727d..649ba1f 100644 --- a/src/commands/utility/calc.ts +++ b/src/commands/utility/calc.ts @@ -1,4 +1,4 @@ -import {Command, NamedCommand, RestCommand} from "../../core"; +import {NamedCommand, RestCommand} from "../../core"; import * as math from "mathjs"; import {MessageEmbed} from "discord.js"; @@ -6,7 +6,7 @@ export default new NamedCommand({ description: "Calculates a specified math expression.", run: "Please provide a calculation.", any: new RestCommand({ - async run({send, message, channel, guild, author, member, client, args, combined}) { + async run({send, combined}) { let resp; try { resp = math.evaluate(combined); diff --git a/src/commands/utility/code.ts b/src/commands/utility/code.ts index 39e41b7..5c73897 100644 --- a/src/commands/utility/code.ts +++ b/src/commands/utility/code.ts @@ -1,4 +1,4 @@ -import {Command, NamedCommand} from "../../core"; +import {NamedCommand} from "../../core"; export default new NamedCommand({ description: "Gives you the Github link.", diff --git a/src/commands/utility/desc.ts b/src/commands/utility/desc.ts index 9930df3..4307469 100644 --- a/src/commands/utility/desc.ts +++ b/src/commands/utility/desc.ts @@ -1,11 +1,11 @@ -import {Command, NamedCommand, RestCommand} from "../../core"; +import {NamedCommand, RestCommand} from "../../core"; export default new NamedCommand({ description: "Renames current voice channel.", usage: "", run: "Please provide a new voice channel name.", any: new RestCommand({ - async run({send, message, channel, guild, author, member, client, args, combined}) { + async run({send, message, combined}) { const voiceChannel = message.member?.voice.channel; if (!voiceChannel) return send("You are not in a voice channel."); diff --git a/src/commands/utility/emote.ts b/src/commands/utility/emote.ts index 4d79446..0cb48a2 100644 --- a/src/commands/utility/emote.ts +++ b/src/commands/utility/emote.ts @@ -1,4 +1,4 @@ -import {Command, NamedCommand, RestCommand} from "../../core"; +import {NamedCommand, RestCommand} from "../../core"; import {processEmoteQueryFormatted} from "./modules/emote-utils"; export default new NamedCommand({ @@ -8,7 +8,7 @@ export default new NamedCommand({ any: new RestCommand({ description: "The emote(s) to send.", usage: "", - async run({send, guild, channel, message, args}) { + async run({send, args}) { const output = processEmoteQueryFormatted(args); if (output.length > 0) send(output); } diff --git a/src/commands/utility/info.ts b/src/commands/utility/info.ts index c9368e6..b490bcd 100644 --- a/src/commands/utility/info.ts +++ b/src/commands/utility/info.ts @@ -8,20 +8,20 @@ import moment, {utc} from "moment"; export default new NamedCommand({ description: "Command to provide all sorts of info about the current server, a user, etc.", - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, author, member}) { send(await getUserInfo(author, member)); }, subcommands: { avatar: new NamedCommand({ description: "Shows your own, or another user's avatar.", usage: "()", - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, author}) { send(author.displayAvatarURL({dynamic: true, size: 2048})); }, id: "user", user: new Command({ description: "Shows your own, or another user's avatar.", - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, args}) { send( args[0].displayAvatarURL({ dynamic: true, @@ -33,7 +33,7 @@ export default new NamedCommand({ any: new RestCommand({ description: "Shows another user's avatar by searching their name", channelType: CHANNEL_TYPE.GUILD, - async run({send, message, channel, guild, author, client, args, combined}) { + async run({send, guild, combined}) { const member = await getMemberByName(guild!, combined); if (typeof member !== "string") { @@ -51,7 +51,7 @@ export default new NamedCommand({ }), bot: new NamedCommand({ description: "Displays info about the bot.", - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, guild, client}) { const core = os.cpus()[0]; const embed = new MessageEmbed() .setColor(guild?.me?.displayHexColor || "BLUE") @@ -94,20 +94,20 @@ export default new NamedCommand({ description: "Displays info about the current guild or another guild.", usage: "(/)", channelType: CHANNEL_TYPE.GUILD, - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, guild}) { send(await getGuildInfo(guild!, guild)); }, id: "guild", guild: new Command({ description: "Display info about a guild by its ID.", - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, guild, args}) { const targetGuild = args[0] as Guild; send(await getGuildInfo(targetGuild, guild)); } }), any: new RestCommand({ description: "Display info about a guild by finding its name.", - async run({send, message, channel, guild, author, member, client, args, combined}) { + async run({send, guild, combined}) { const targetGuild = getGuildByName(combined); if (typeof targetGuild !== "string") { @@ -122,7 +122,7 @@ export default new NamedCommand({ id: "user", user: new Command({ description: "Displays info about mentioned user.", - async run({send, message, channel, guild, author, client, args}) { + async run({send, guild, args}) { const user = args[0] as User; // Transforms the User object into a GuildMember object of the current guild. const member = guild?.members.resolve(args[0]); diff --git a/src/commands/utility/invite.ts b/src/commands/utility/invite.ts index 9dc0fa3..58b38d8 100644 --- a/src/commands/utility/invite.ts +++ b/src/commands/utility/invite.ts @@ -1,8 +1,8 @@ -import {Command, NamedCommand} from "../../core"; +import {NamedCommand} from "../../core"; export default new NamedCommand({ description: "Gives you the invite link.", - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, client, args}) { send( `https://discordapp.com/api/oauth2/authorize?client_id=${client.user!.id}&permissions=${ args[0] || 8 diff --git a/src/commands/utility/lsemotes.ts b/src/commands/utility/lsemotes.ts index 7e1e2d9..955328a 100644 --- a/src/commands/utility/lsemotes.ts +++ b/src/commands/utility/lsemotes.ts @@ -1,5 +1,5 @@ import {GuildEmoji, MessageEmbed, User} from "discord.js"; -import {Command, NamedCommand, RestCommand, paginate, SendFunction} from "../../core"; +import {NamedCommand, RestCommand, paginate, SendFunction} from "../../core"; import {split} from "../../lib"; import vm from "vm"; @@ -8,13 +8,13 @@ const REGEX_TIMEOUT_MS = 1000; export default new NamedCommand({ description: "Lists all emotes the bot has in it's registry,", usage: " (-flags)", - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, author, client}) { displayEmoteList(client.emojis.cache.array(), send, author); }, any: new RestCommand({ description: "Filters emotes by via a regular expression. Flags can be added by adding a dash at the end. For example, to do a case-insensitive search, do %prefix%lsemotes somepattern -i", - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, author, client, args}) { // If a guild ID is provided, filter all emotes by that guild (but only if there aren't any arguments afterward) if (args.length === 1 && /^\d{17,}$/.test(args[0])) { const guildID: string = args[0]; diff --git a/src/commands/utility/react.ts b/src/commands/utility/react.ts index ef1aebb..39301c9 100644 --- a/src/commands/utility/react.ts +++ b/src/commands/utility/react.ts @@ -1,4 +1,4 @@ -import {Command, NamedCommand, RestCommand} from "../../core"; +import {NamedCommand, RestCommand} from "../../core"; import {Message, Channel, TextChannel} from "discord.js"; import {processEmoteQueryArray} from "./modules/emote-utils"; @@ -8,7 +8,7 @@ export default new NamedCommand({ usage: 'react ()', run: "You need to enter some emotes first.", any: new RestCommand({ - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild, client, args}) { let target: Message | undefined; let distance = 1; diff --git a/src/commands/utility/say.ts b/src/commands/utility/say.ts index 0ffe639..5d8805a 100644 --- a/src/commands/utility/say.ts +++ b/src/commands/utility/say.ts @@ -1,4 +1,4 @@ -import {Command, NamedCommand, RestCommand} from "../../core"; +import {NamedCommand, RestCommand} from "../../core"; export default new NamedCommand({ description: "Repeats your message.", @@ -6,7 +6,7 @@ export default new NamedCommand({ run: "Please provide a message for me to say!", any: new RestCommand({ description: "Message to repeat.", - async run({send, message, channel, guild, author, member, client, args, combined}) { + async run({send, author, combined}) { send(`*${author} says:*\n${combined}`); } }) diff --git a/src/commands/utility/scanemotes.ts b/src/commands/utility/scanemotes.ts index 08fb74f..e9fbcd0 100644 --- a/src/commands/utility/scanemotes.ts +++ b/src/commands/utility/scanemotes.ts @@ -1,4 +1,4 @@ -import {Command, NamedCommand, CHANNEL_TYPE} from "../../core"; +import {NamedCommand, CHANNEL_TYPE} from "../../core"; import {pluralise} from "../../lib"; import moment from "moment"; import {Collection, TextChannel} from "discord.js"; @@ -9,7 +9,7 @@ export default new NamedCommand({ description: "Scans all text channels in the current guild and returns the number of times each emoji specific to the guild has been used. Has a cooldown of 24 hours per guild.", channelType: CHANNEL_TYPE.GUILD, - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, message, channel, guild}) { // Test if the command is on cooldown. This isn't the strictest cooldown possible, because in the event that the bot crashes, the cooldown will be reset. But for all intends and purposes, it's a good enough cooldown. It's a per-server cooldown. const startTime = Date.now(); const cooldown = 86400000; // 24 hours @@ -185,7 +185,7 @@ export default new NamedCommand({ forcereset: new NamedCommand({ description: "Forces the cooldown timer to reset.", permission: PERMISSIONS.BOT_SUPPORT, - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, guild}) { lastUsedTimestamps.set(guild!.id, 0); send("Reset the cooldown on `scanemotes`."); } diff --git a/src/commands/utility/shorten.ts b/src/commands/utility/shorten.ts index 9d74544..c8a4618 100644 --- a/src/commands/utility/shorten.ts +++ b/src/commands/utility/shorten.ts @@ -5,7 +5,7 @@ export default new NamedCommand({ description: "Shortens a given URL.", run: "Please provide a URL.", any: new Command({ - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, args}) { https.get("https://is.gd/create.php?format=simple&url=" + encodeURIComponent(args[0]), function (res) { var body = ""; res.on("data", function (chunk) { diff --git a/src/commands/utility/streaminfo.ts b/src/commands/utility/streaminfo.ts index c122b8b..b58b1f8 100644 --- a/src/commands/utility/streaminfo.ts +++ b/src/commands/utility/streaminfo.ts @@ -1,9 +1,9 @@ -import {Command, NamedCommand, RestCommand} from "../../core"; +import {NamedCommand, RestCommand} from "../../core"; import {streamList} from "../../modules/streamNotifications"; export default new NamedCommand({ description: "Sets the description of your stream. You can embed links by writing `[some name](some link)`", - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, author, member}) { const userID = author.id; if (streamList.has(userID)) { @@ -22,7 +22,7 @@ export default new NamedCommand({ } }, any: new RestCommand({ - async run({send, message, channel, guild, author, member, client, args, combined}) { + async run({send, author, member, combined}) { const userID = author.id; if (streamList.has(userID)) { diff --git a/src/commands/utility/todo.ts b/src/commands/utility/todo.ts index 9057166..3962126 100644 --- a/src/commands/utility/todo.ts +++ b/src/commands/utility/todo.ts @@ -1,11 +1,11 @@ -import {Command, NamedCommand, RestCommand} from "../../core"; +import {NamedCommand, RestCommand} from "../../core"; import moment from "moment"; import {Storage} from "../../structures"; import {MessageEmbed} from "discord.js"; export default new NamedCommand({ description: "Keep and edit your personal todo list.", - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, author}) { const user = Storage.getUser(author.id); const embed = new MessageEmbed().setTitle(`Todo list for ${author.tag}`).setColor("BLUE"); @@ -23,7 +23,7 @@ export default new NamedCommand({ add: new NamedCommand({ run: "You need to specify a note to add.", any: new RestCommand({ - async run({send, message, channel, guild, author, member, client, args, combined}) { + async run({send, author, combined}) { const user = Storage.getUser(author.id); user.todoList[Date.now().toString()] = combined; console.debug(user.todoList); @@ -35,7 +35,7 @@ export default new NamedCommand({ remove: new NamedCommand({ run: "You need to specify a note to remove.", any: new RestCommand({ - async run({send, message, channel, guild, author, member, client, args, combined}) { + async run({send, author, combined}) { const user = Storage.getUser(author.id); let isFound = false; @@ -55,7 +55,7 @@ export default new NamedCommand({ }) }), clear: new NamedCommand({ - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, author}) { const user = Storage.getUser(author.id); user.todoList = {}; Storage.save(); diff --git a/src/commands/utility/translate.ts b/src/commands/utility/translate.ts index d4a1aab..e3ca9ed 100644 --- a/src/commands/utility/translate.ts +++ b/src/commands/utility/translate.ts @@ -8,7 +8,7 @@ export default new NamedCommand({ any: new Command({ run: "You need to enter some text to translate.", any: new RestCommand({ - async run({send, message, channel, guild, author, member, client, args}) { + async run({send, args}) { const lang = args[0]; const input = args.slice(1).join(" "); translate(input, { From a493536a23edcfc7124302f75917216dd1e3a58d Mon Sep 17 00:00:00 2001 From: WatDuhHekBro <44940783+WatDuhHekBro@users.noreply.github.com> Date: Sun, 11 Apr 2021 05:45:50 -0500 Subject: [PATCH 14/14] Refactored paginate and added poll to library --- docs/Documentation.md | 11 +- src/commands/fun/modules/eco-bet.ts | 79 ++++---- src/commands/fun/modules/eco-shop.ts | 17 +- src/commands/system/help.ts | 25 +-- src/commands/utility/lsemotes.ts | 23 +-- src/commands/utility/todo.ts | 1 - src/core/eventListeners.ts | 26 ++- src/core/libd.ts | 271 +++++++++++++++------------ 8 files changed, 231 insertions(+), 222 deletions(-) diff --git a/docs/Documentation.md b/docs/Documentation.md index 28c2c32..173f686 100644 --- a/docs/Documentation.md +++ b/docs/Documentation.md @@ -75,9 +75,16 @@ Because versions are assigned to batches of changes rather than single changes ( ```ts const pages = ["one", "two", "three"]; -paginate(send, page => { +paginate(send, author.id, pages.length, page => { return {content: pages[page]}; -}, pages.length, author.id); +}); +``` + +`poll()` +```ts +const results = await poll(await send("Do you agree with this decision?"), ["✅", "❌"]); +results["✅"]; // number +results["❌"]; // number ``` `confirm()` diff --git a/src/commands/fun/modules/eco-bet.ts b/src/commands/fun/modules/eco-bet.ts index 7b38485..3bcb103 100644 --- a/src/commands/fun/modules/eco-bet.ts +++ b/src/commands/fun/modules/eco-bet.ts @@ -1,4 +1,4 @@ -import {Command, NamedCommand, confirm} from "../../../core"; +import {Command, NamedCommand, confirm, poll} from "../../../core"; import {pluralise} from "../../../lib"; import {Storage} from "../../../structures"; import {isAuthorized, getMoneyEmbed} from "./eco-utils"; @@ -113,54 +113,41 @@ export const BetCommand = new NamedCommand({ const receiver = Storage.getUser(target.id); // [TODO: when D.JSv13 comes out, inline reply to clean up.] // When bet is over, give a vote to ask people their thoughts. - const voteMsg = await send( - `VOTE: do you think that <@${ - target.id - }> has won the bet?\nhttps://discord.com/channels/${guild!.id}/${channel.id}/${ - message.id - }` - ); - await voteMsg.react("✅"); - await voteMsg.react("❌"); - // Filter reactions to only collect the pertinent ones. - voteMsg - .awaitReactions( - (reaction, user) => { - return ["✅", "❌"].includes(reaction.emoji.name); - }, - // [Pertinence to make configurable on the fly.] - {time: parseDuration("2m")} - ) - .then((reactions) => { - // Count votes - const okReaction = reactions.get("✅"); - const noReaction = reactions.get("❌"); - const ok = okReaction ? (okReaction.count ?? 1) - 1 : 0; - const no = noReaction ? (noReaction.count ?? 1) - 1 : 0; + const results = await poll( + await send( + `VOTE: do you think that <@${ + target.id + }> has won the bet?\nhttps://discord.com/channels/${guild!.id}/${channel.id}/${ + message.id + }` + ), + ["✅", "❌"], + // [Pertinence to make configurable on the fly.] + parseDuration("2m") + ); - if (ok > no) { - receiver.money += amount * 2; - send( - `By the people's votes, <@${target.id}> has won the bet that <@${author.id}> had sent them.` - ); - } else if (ok < no) { - sender.money += amount * 2; - send( - `By the people's votes, <@${target.id}> has lost the bet that <@${author.id}> had sent them.` - ); - } else { - sender.money += amount; - receiver.money += amount; - send( - `By the people's votes, <@${target.id}> couldn't be determined to have won or lost the bet that <@${author.id}> had sent them.` - ); - } + // Count votes + const ok = results["✅"]; + const no = results["❌"]; - sender.ecoBetInsurance -= amount; - receiver.ecoBetInsurance -= amount; - Storage.save(); - }); + if (ok > no) { + receiver.money += amount * 2; + send(`By the people's votes, ${target} has won the bet that ${author} had sent them.`); + } else if (ok < no) { + sender.money += amount * 2; + send(`By the people's votes, ${target} has lost the bet that ${author} had sent them.`); + } else { + sender.money += amount; + receiver.money += amount; + send( + `By the people's votes, ${target} couldn't be determined to have won or lost the bet that ${author} had sent them.` + ); + } + + sender.ecoBetInsurance -= amount; + receiver.ecoBetInsurance -= amount; + Storage.save(); }, duration); } else return; } diff --git a/src/commands/fun/modules/eco-shop.ts b/src/commands/fun/modules/eco-shop.ts index 91ac71d..acd0e4a 100644 --- a/src/commands/fun/modules/eco-shop.ts +++ b/src/commands/fun/modules/eco-shop.ts @@ -34,17 +34,12 @@ export const ShopCommand = new NamedCommand({ const shopPages = split(ShopItems, 5); const pageAmount = shopPages.length; - paginate( - send, - (page, hasMultiplePages) => { - return getShopEmbed( - shopPages[page], - hasMultiplePages ? `Shop (Page ${page + 1} of ${pageAmount})` : "Shop" - ); - }, - pageAmount, - author.id - ); + paginate(send, author.id, pageAmount, (page, hasMultiplePages) => { + return getShopEmbed( + shopPages[page], + hasMultiplePages ? `Shop (Page ${page + 1} of ${pageAmount})` : "Shop" + ); + }); } } }); diff --git a/src/commands/system/help.ts b/src/commands/system/help.ts index 7e5139c..7691a5a 100644 --- a/src/commands/system/help.ts +++ b/src/commands/system/help.ts @@ -20,21 +20,16 @@ export default new NamedCommand({ const commands = await getCommandList(); const categoryArray = commands.keyArray(); - paginate( - send, - (page, hasMultiplePages) => { - const category = categoryArray[page]; - const commandList = commands.get(category)!; - let output = `Legend: \`\`, \`[list/of/stuff]\`, \`(optional)\`, \`()\`, \`([optional/list/...])\`\n`; - for (const command of commandList) output += `\n❯ \`${command.name}\`: ${command.description}`; - return new MessageEmbed() - .setTitle(hasMultiplePages ? `${category} (Page ${page + 1} of ${categoryArray.length})` : category) - .setDescription(output) - .setColor(EMBED_COLOR); - }, - categoryArray.length, - author.id - ); + paginate(send, author.id, categoryArray.length, (page, hasMultiplePages) => { + const category = categoryArray[page]; + const commandList = commands.get(category)!; + let output = `Legend: \`\`, \`[list/of/stuff]\`, \`(optional)\`, \`()\`, \`([optional/list/...])\`\n`; + for (const command of commandList) output += `\n❯ \`${command.name}\`: ${command.description}`; + return new MessageEmbed() + .setTitle(hasMultiplePages ? `${category} (Page ${page + 1} of ${categoryArray.length})` : category) + .setDescription(output) + .setColor(EMBED_COLOR); + }); }, any: new Command({ async run({send, message, channel, guild, author, member, client, args}) { diff --git a/src/commands/utility/lsemotes.ts b/src/commands/utility/lsemotes.ts index 7e1e2d9..37840b9 100644 --- a/src/commands/utility/lsemotes.ts +++ b/src/commands/utility/lsemotes.ts @@ -90,22 +90,17 @@ async function displayEmoteList(emotes: GuildEmoji[], send: SendFunction, author // Gather the first page (if it even exists, which it might not if there no valid emotes appear) if (pages > 0) { - paginate( - send, - (page, hasMultiplePages) => { - embed.setTitle(hasMultiplePages ? `**Emotes** (Page ${page + 1} of ${pages})` : "**Emotes**"); + paginate(send, author.id, pages, (page, hasMultiplePages) => { + embed.setTitle(hasMultiplePages ? `**Emotes** (Page ${page + 1} of ${pages})` : "**Emotes**"); - let desc = ""; - for (const emote of sections[page]) { - desc += `${emote} ${emote.name} (**${emote.guild.name}**)\n`; - } - embed.setDescription(desc); + let desc = ""; + for (const emote of sections[page]) { + desc += `${emote} ${emote.name} (**${emote.guild.name}**)\n`; + } + embed.setDescription(desc); - return embed; - }, - pages, - author.id - ); + return embed; + }); } else { send("No valid emotes found by that query."); } diff --git a/src/commands/utility/todo.ts b/src/commands/utility/todo.ts index 9057166..2761db7 100644 --- a/src/commands/utility/todo.ts +++ b/src/commands/utility/todo.ts @@ -26,7 +26,6 @@ export default new NamedCommand({ async run({send, message, channel, guild, author, member, client, args, combined}) { const user = Storage.getUser(author.id); user.todoList[Date.now().toString()] = combined; - console.debug(user.todoList); Storage.save(); send(`Successfully added \`${combined}\` to your todo list.`); } diff --git a/src/core/eventListeners.ts b/src/core/eventListeners.ts index 705647e..bad74cd 100644 --- a/src/core/eventListeners.ts +++ b/src/core/eventListeners.ts @@ -1,22 +1,32 @@ -import {Client, Permissions, Message} from "discord.js"; +import {Client, Permissions, Message, MessageReaction, User, PartialUser} from "discord.js"; import {botHasPermission} from "./libd"; // A list of message ID and callback pairs. You get the emote name and ID of the user reacting. -export const unreactEventListeners: Map void> = new Map(); +// This will handle removing reactions automatically (if the bot has the right permission). +export const reactEventListeners = new Map void>(); +export const emptyReactEventListeners = new Map void>(); // A list of "channel-message" and callback pairs. Also, I imagine that the callback will be much more maintainable when discord.js v13 comes out with a dedicated message.referencedMessage property. -// Also, I'm defining it here instead of the message event because the load order screws up if you export it from there. Yeah... I'm starting to notice just how much technical debt has been built up. The command handler needs to be modularized and refactored sooner rather than later. Define all constants in one area then grab from there. export const replyEventListeners = new Map void>(); export function attachEventListenersToClient(client: Client) { - // Attached to the client, there can be one event listener attached to a message ID which is executed if present. + client.on("messageReactionAdd", (reaction, user) => { + // The reason this is inside the call is because it's possible to switch a user's permissions halfway and suddenly throw an error. + // This will dynamically adjust for that, switching modes depending on whether it currently has the "Manage Messages" permission. + const canDeleteEmotes = botHasPermission(reaction.message.guild, Permissions.FLAGS.MANAGE_MESSAGES); + reactEventListeners.get(reaction.message.id)?.(reaction, user); + if (canDeleteEmotes && !user.partial) reaction.users.remove(user); + }); + client.on("messageReactionRemove", (reaction, user) => { const canDeleteEmotes = botHasPermission(reaction.message.guild, Permissions.FLAGS.MANAGE_MESSAGES); + if (!canDeleteEmotes) reactEventListeners.get(reaction.message.id)?.(reaction, user); + }); - if (!canDeleteEmotes) { - const callback = unreactEventListeners.get(reaction.message.id); - callback && callback(reaction.emoji.name, user.id); - } + client.on("messageReactionRemoveAll", (message) => { + reactEventListeners.delete(message.id); + emptyReactEventListeners.get(message.id)?.(); + emptyReactEventListeners.delete(message.id); }); client.on("message", (message) => { diff --git a/src/core/libd.ts b/src/core/libd.ts index 58338a5..aed4a07 100644 --- a/src/core/libd.ts +++ b/src/core/libd.ts @@ -3,7 +3,6 @@ import { Message, Guild, GuildMember, - Permissions, TextChannel, DMChannel, NewsChannel, @@ -17,9 +16,10 @@ import { APIMessage, StringResolvable, EmojiIdentifierResolvable, - MessageReaction + MessageReaction, + PartialUser } from "discord.js"; -import {unreactEventListeners, replyEventListeners} from "./eventListeners"; +import {reactEventListeners, emptyReactEventListeners, replyEventListeners} from "./eventListeners"; import {client} from "./interface"; export type SingleMessageOptions = MessageOptions & {split?: false}; @@ -33,150 +33,119 @@ export type SendFunction = (( ((content: StringResolvable, options: MessageOptions & {split: true | SplitOptions}) => Promise) & ((content: StringResolvable, options: MessageOptions) => Promise); -const FIVE_BACKWARDS_EMOJI = "⏪"; -const BACKWARDS_EMOJI = "⬅️"; -const FORWARDS_EMOJI = "➡️"; -const FIVE_FORWARDS_EMOJI = "⏩"; +interface PaginateOptions { + multiPageSize?: number; + idleTimeout?: number; +} // 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. + * + * Returns the page number the user left off on in case you want to implement a return to page function. */ -export async function paginate( +export function paginate( send: SendFunction, - onTurnPage: (page: number, hasMultiplePages: boolean) => SingleMessageOptions, + listenTo: string | null, totalPages: number, - listenTo: string | null = null, - duration = 60000 -): Promise { - const hasMultiplePages = totalPages > 1; - const message = await send(onTurnPage(0, hasMultiplePages)); + onTurnPage: (page: number, hasMultiplePages: boolean) => SingleMessageOptions, + options?: PaginateOptions +): Promise { + if (totalPages < 1) throw new Error(`totalPages on paginate() must be 1 or more, ${totalPages} given.`); - if (hasMultiplePages) { - let page = 0; - const turn = (amount: number) => { - page += amount; - - if (page >= totalPages) { - page %= totalPages; - } else if (page < 0) { - // Assuming 3 total pages, it's a bit tricker, but if we just take the modulo of the absolute value (|page| % total), we get (1 2 0 ...), and we just need the pattern (2 1 0 ...). It needs to reverse order except for when it's 0. I want to find a better solution, but for the time being... total - (|page| % total) unless (|page| % total) = 0, then return 0. - const flattened = Math.abs(page) % totalPages; - if (flattened !== 0) page = totalPages - flattened; - } - - message.edit(onTurnPage(page, true)); - }; - const handle = (emote: string, reacterID: string) => { - if (reacterID === listenTo || listenTo === null) { - collector.resetTimer(); // The timer refresh MUST be present in both react and unreact. - switch (emote) { - case FIVE_BACKWARDS_EMOJI: - if (totalPages > 5) turn(-5); - break; - case BACKWARDS_EMOJI: - turn(-1); - break; - case FORWARDS_EMOJI: - turn(1); - break; - case FIVE_FORWARDS_EMOJI: - if (totalPages > 5) turn(5); - break; - } - } - }; - - // Listen for reactions and call the handler. - let backwardsReactionFive = totalPages > 5 ? await message.react(FIVE_BACKWARDS_EMOJI) : null; - let backwardsReaction = await message.react(BACKWARDS_EMOJI); - let forwardsReaction = await message.react(FORWARDS_EMOJI); - let forwardsReactionFive = totalPages > 5 ? await message.react(FIVE_FORWARDS_EMOJI) : null; - unreactEventListeners.set(message.id, handle); - - const collector = message.createReactionCollector( - (reaction, user) => { - // This check is actually redundant because of handle(). - if (user.id === listenTo || listenTo === null) { - // The reason this is inside the call is because it's possible to switch a user's permissions halfway and suddenly throw an error. - // This will dynamically adjust for that, switching modes depending on whether it currently has the "Manage Messages" permission. - const canDeleteEmotes = botHasPermission(message.guild, Permissions.FLAGS.MANAGE_MESSAGES); - handle(reaction.emoji.name, user.id); - if (canDeleteEmotes) reaction.users.remove(user); - } - - return false; - }, - // Apparently, regardless of whether you put "time" or "idle", it won't matter to the collector. - // In order to actually reset the timer, you have to do it manually via collector.resetTimer(). - {time: duration} - ); - - // When time's up, remove the bot's own reactions. - collector.on("end", () => { - unreactEventListeners.delete(message.id); - backwardsReactionFive?.users.remove(message.author); - backwardsReaction.users.remove(message.author); - forwardsReaction.users.remove(message.author); - forwardsReactionFive?.users.remove(message.author); - }); - } -} - -//export function generateMulti -// paginate after generateonetimeprompt - -// Returns null if timed out, otherwise, returns the value. -export function generateOneTimePrompt( - message: Message, - stack: {[emote: string]: T}, - listenTo: string | null = null, - duration = 60000 -): Promise { return new Promise(async (resolve) => { - // First, start reacting to the message in order. - reactInOrder(message, Object.keys(stack)); + const hasMultiplePages = totalPages > 1; + const message = await send(onTurnPage(0, hasMultiplePages)); - // Then setup the reaction listener in parallel. - await message.awaitReactions( - (reaction: MessageReaction, user: User) => { - if (user.id === listenTo || listenTo === null) { - const emote = reaction.emoji.name; + if (hasMultiplePages) { + const multiPageSize = options?.multiPageSize ?? 5; + const idleTimeout = options?.idleTimeout ?? 60000; + let page = 0; - if (emote in stack) { - resolve(stack[emote]); - message.delete(); - } + const turn = (amount: number) => { + page += amount; + + if (page >= totalPages) { + page %= totalPages; + } else if (page < 0) { + // Assuming 3 total pages, it's a bit tricker, but if we just take the modulo of the absolute value (|page| % total), we get (1 2 0 ...), and we just need the pattern (2 1 0 ...). It needs to reverse order except for when it's 0. I want to find a better solution, but for the time being... total - (|page| % total) unless (|page| % total) = 0, then return 0. + const flattened = Math.abs(page) % totalPages; + if (flattened !== 0) page = totalPages - flattened; } - // CollectorFilter requires a boolean to be returned. - // My guess is that the return value of awaitReactions can be altered by making a boolean filter. - // However, because that's not my concern with this command, I don't have to worry about it. - // May as well just set it to false because I'm not concerned with collecting any reactions. - return false; - }, - {time: duration} - ); + message.edit(onTurnPage(page, true)); + }; - if (!message.deleted) { - message.delete(); - resolve(null); + let stack: {[emote: string]: number} = { + "⬅️": -1, + "➡️": 1 + }; + + if (totalPages > multiPageSize) { + stack = { + "⏪": -multiPageSize, + ...stack, + "⏩": multiPageSize + }; + } + + const handle = (reaction: MessageReaction, user: User | PartialUser) => { + if (user.id === listenTo || (listenTo === null && user.id !== client.user!.id)) { + // Turn the page + const emote = reaction.emoji.name; + if (emote in stack) turn(stack[emote]); + + // Reset the timer + client.clearTimeout(timeout); + timeout = client.setTimeout(destroy, idleTimeout); + } + }; + + // When time's up, remove the bot's own reactions. + const destroy = () => { + reactEventListeners.delete(message.id); + for (const emote of message.reactions.cache.values()) emote.users.remove(message.author); + resolve(page); + }; + + // Start the reactions and call the handler. + reactInOrder(message, Object.keys(stack)); + reactEventListeners.set(message.id, handle); + emptyReactEventListeners.set(message.id, destroy); + let timeout = client.setTimeout(destroy, idleTimeout); } }); } -// Start a parallel chain of ordered reactions, allowing a collector to end early. -// Check if the collector ended early by seeing if the message is already deleted. -// Though apparently, message.deleted doesn't seem to update fast enough, so just put a try catch block on message.react(). -async function reactInOrder(message: Message, emotes: EmojiIdentifierResolvable[]): Promise { +export async function poll(message: Message, emotes: string[], duration = 60000): Promise<{[emote: string]: number}> { + if (emotes.length === 0) throw new Error("poll() was called without any emotes."); + + reactInOrder(message, emotes); + const reactions = await message.awaitReactions( + (reaction: MessageReaction) => emotes.includes(reaction.emoji.name), + {time: duration} + ); + const reactionsByCount: {[emote: string]: number} = {}; + for (const emote of emotes) { - try { - await message.react(emote); - } catch { - return; + const reaction = reactions.get(emote); + + if (reaction) { + const hasBot = reaction.users.cache.has(client.user!.id); // Apparently, reaction.me doesn't work properly. + + if (reaction.count !== null) { + const difference = hasBot ? 1 : 0; + reactionsByCount[emote] = reaction.count - difference; + } else { + reactionsByCount[emote] = 0; + } + } else { + reactionsByCount[emote] = 0; } } + + return reactionsByCount; } export function confirm(message: Message, senderID: string, timeout = 30000): Promise { @@ -235,6 +204,58 @@ export function askForReply(message: Message, listenTo: string, timeout?: number }); } +// Returns null if timed out, otherwise, returns the value. +export function generateOneTimePrompt( + message: Message, + stack: {[emote: string]: T}, + listenTo: string | null = null, + duration = 60000 +): Promise { + return new Promise(async (resolve) => { + // First, start reacting to the message in order. + reactInOrder(message, Object.keys(stack)); + + // Then setup the reaction listener in parallel. + await message.awaitReactions( + (reaction: MessageReaction, user: User) => { + if (user.id === listenTo || listenTo === null) { + const emote = reaction.emoji.name; + + if (emote in stack) { + resolve(stack[emote]); + message.delete(); + } + } + + // CollectorFilter requires a boolean to be returned. + // My guess is that the return value of awaitReactions can be altered by making a boolean filter. + // However, because that's not my concern with this command, I don't have to worry about it. + // May as well just set it to false because I'm not concerned with collecting any reactions. + return false; + }, + {time: duration} + ); + + if (!message.deleted) { + message.delete(); + resolve(null); + } + }); +} + +// Start a parallel chain of ordered reactions, allowing a collector to end early. +// Check if the collector ended early by seeing if the message is already deleted. +// Though apparently, message.deleted doesn't seem to update fast enough, so just put a try catch block on message.react(). +async function reactInOrder(message: Message, emotes: EmojiIdentifierResolvable[]): Promise { + for (const emote of emotes) { + try { + await message.react(emote); + } catch { + return; + } + } +} + /** * Tests if a bot has a certain permission in a specified guild. */