diff --git a/docs/Documentation.md b/docs/Documentation.md index 28c2c32..1907bc9 100644 --- a/docs/Documentation.md +++ b/docs/Documentation.md @@ -75,24 +75,27 @@ Because versions are assigned to batches of changes rather than single changes ( ```ts const pages = ["one", "two", "three"]; -paginate(send, page => { - return {content: pages[page]}; -}, pages.length, author.id); +paginate(channel, author.id, pages.length, (page) => { + return { + content: pages[page] + }; +}); ``` -`confirm()` +`prompt()` ```ts -const result = await confirm(await send("Are you sure you want to delete this?"), author.id); // boolean | null +const msg = await channel.send('Are you sure you want to delete this?'); + +prompt(msg, author.id, () => { + //... +}); ``` -`askMultipleChoice()` +`callMemberByUsername()` ```ts -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 +callMemberByUsername(message, args.join(" "), (member) => { + channel.send(`Your nickname is ${member.nickname}.`); +}); ``` ## [src/lib](../src/lib.ts) - General utility functions diff --git a/src/commands/fun/8ball.ts b/src/commands/fun/8ball.ts index f79a2c0..898eafe 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}) { + async run({send, message, channel, guild, author, member, client, args}) { const sender = message.author; send(`${random(responses)} <@${sender.id}>`); } diff --git a/src/commands/fun/cookie.ts b/src/commands/fun/cookie.ts index 71d9c20..2275c08 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, author}) { + async run({send, message, channel, guild, author, member, client, args}) { 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, author, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const sender = author; const mention: User = args[0]; diff --git a/src/commands/fun/eco.ts b/src/commands/fun/eco.ts index 4644101..96c1b43 100644 --- a/src/commands/fun/eco.ts +++ b/src/commands/fun/eco.ts @@ -4,6 +4,7 @@ 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.", @@ -34,10 +35,10 @@ export default new NamedCommand({ }), any: new RestCommand({ description: "See how much money someone else has by using their username.", - async run({send, guild, channel, combined}) { + async run({send, guild, channel, args, message, combined}) { if (isAuthorized(guild, channel)) { const member = await getMemberByName(guild!, combined); - if (typeof member !== "string") send(getMoneyEmbed(member.user)); + 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 b2f9bb0..a25ed2a 100644 --- a/src/commands/fun/figlet.ts +++ b/src/commands/fun/figlet.ts @@ -1,11 +1,11 @@ -import {NamedCommand, RestCommand} from "../../core"; +import {Command, 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, combined}) { + async run({send, message, channel, guild, author, member, client, args, combined}) { return send( figlet.textSync(combined, { horizontalLayout: "full" diff --git a/src/commands/fun/insult.ts b/src/commands/fun/insult.ts index 251444a..3cd982d 100644 --- a/src/commands/fun/insult.ts +++ b/src/commands/fun/insult.ts @@ -1,8 +1,8 @@ -import {NamedCommand} from "../../core"; +import {Command, NamedCommand} from "../../core"; export default new NamedCommand({ description: "Insult TravBot! >:D", - async run({send, channel, author}) { + async run({send, message, channel, guild, author, member, client, args}) { channel.startTyping(); setTimeout(() => { send( diff --git a/src/commands/fun/love.ts b/src/commands/fun/love.ts index 25a7c8a..e7d2557 100644 --- a/src/commands/fun/love.ts +++ b/src/commands/fun/love.ts @@ -1,9 +1,9 @@ -import {NamedCommand, CHANNEL_TYPE} from "../../core"; +import {Command, NamedCommand, CHANNEL_TYPE} from "../../core"; export default new NamedCommand({ description: "Chooses someone to love.", channelType: CHANNEL_TYPE.GUILD, - async run({send, guild}) { + async run({send, message, channel, guild, author, client, args}) { const member = guild!.members.cache.random(); 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 7b38485..ecda43c 100644 --- a/src/commands/fun/modules/eco-bet.ts +++ b/src/commands/fun/modules/eco-bet.ts @@ -1,7 +1,7 @@ -import {Command, NamedCommand, confirm} from "../../../core"; +import {Command, NamedCommand, askYesOrNo} from "../../../core"; import {pluralise} from "../../../lib"; import {Storage} from "../../../structures"; -import {isAuthorized, getMoneyEmbed} from "./eco-utils"; +import {isAuthorized, getMoneyEmbed, getSendEmbed, ECO_EMBED_COLOR} from "./eco-utils"; import {User} from "discord.js"; export const BetCommand = new NamedCommand({ @@ -79,89 +79,88 @@ 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 confirm( + const takeBet = await askYesOrNo( await send( `<@${target.id}>, do you want to take this bet of ${pluralise(amount, "Mon", "s")}` ), target.id ); - if (!takeBet) return send(`<@${target.id}> has rejected your bet, <@${author.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(); - // [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 - }` + // 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.` ); - 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; + // 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("❌"); - 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.` - ); - } + // 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; - sender.ecoBetInsurance -= amount; - receiver.ecoBetInsurance -= amount; - Storage.save(); - }); - }, duration); + 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}>`); } else return; } }) diff --git a/src/commands/fun/modules/eco-core.ts b/src/commands/fun/modules/eco-core.ts index 4e0a520..ec3d8b6 100644 --- a/src/commands/fun/modules/eco-core.ts +++ b/src/commands/fun/modules/eco-core.ts @@ -1,4 +1,5 @@ -import {Command, getMemberByName, NamedCommand, confirm, RestCommand} 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"; @@ -89,7 +90,7 @@ export const LeaderboardCommand = new NamedCommand({ const user = await client.users.fetch(id); fields.push({ - name: `#${i + 1}. ${user.tag}`, + name: `#${i + 1}. ${user.username}#${user.discriminator}`, value: pluralise(users[id].money, "Mon", "s") }); } @@ -157,38 +158,42 @@ 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 (typeof member === "string") return send(member); + 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!"); const target = member.user; - 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 - }) + 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 + }) + } } } - }), - author.id + ), + author.id, + () => { + const receiver = Storage.getUser(target.id); + sender.money -= amount; + receiver.money += amount; + Storage.save(); + send(getSendEmbed(author, target, amount)); + } ); - - 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 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/fun/modules/eco-utils.ts b/src/commands/fun/modules/eco-utils.ts index d014374..327abdb 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.tag}`, + name: `Sender: ${sender.username}#${sender.discriminator}`, value: pluralise(Storage.getUser(sender.id).money, "Mon", "s") }, { - name: `Receiver: ${receiver.tag}`, + name: `Receiver: ${receiver.username}#${receiver.discriminator}`, value: pluralise(Storage.getUser(receiver.id).money, "Mon", "s") } ], diff --git a/src/commands/fun/neko.ts b/src/commands/fun/neko.ts index f430406..1722232 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}) { + 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({send, args}) { + async run({send, message, channel, guild, author, member, client, 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 13296a3..66bfa6b 100644 --- a/src/commands/fun/ok.ts +++ b/src/commands/fun/ok.ts @@ -1,4 +1,4 @@ -import {NamedCommand} from "../../core"; +import {Command, 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}) { + 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 3cc2dd5..cbc4ccf 100644 --- a/src/commands/fun/owoify.ts +++ b/src/commands/fun/owoify.ts @@ -1,4 +1,4 @@ -import {NamedCommand, RestCommand} from "../../core"; +import {Command, 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, combined}) { + 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/party.ts b/src/commands/fun/party.ts index ce58b13..db4c0e3 100644 --- a/src/commands/fun/party.ts +++ b/src/commands/fun/party.ts @@ -1,8 +1,8 @@ -import {NamedCommand} from "../../core"; +import {Command, NamedCommand} from "../../core"; export default new NamedCommand({ description: "Initiates a celebratory stream from the bot.", - async run({send, client}) { + async run({send, message, channel, guild, author, member, client, args}) { 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 3909968..5795db1 100644 --- a/src/commands/fun/poll.ts +++ b/src/commands/fun/poll.ts @@ -1,5 +1,5 @@ import {MessageEmbed} from "discord.js"; -import {NamedCommand, RestCommand} from "../../core"; +import {Command, 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, combined}) { + async run({send, message, channel, guild, author, member, client, args, 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 ff64376..e0862e0 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}) { + async run({send, message, channel, guild, author, member, client, args}) { send({ embed: { title: "Ravioli ravioli...", @@ -18,7 +18,7 @@ export default new NamedCommand({ }); }, number: new Command({ - async run({send, args}) { + async run({send, message, channel, guild, author, member, client, 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 821216f..de09d20 100644 --- a/src/commands/fun/thonk.ts +++ b/src/commands/fun/thonk.ts @@ -1,4 +1,4 @@ -import {NamedCommand, RestCommand} from "../../core"; +import {Command, 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, author}) { + async run({send, message, channel, guild, author, member, client, args}) { const msg = await send(transform(phrase)); msg.createReactionCollector( (reaction, user) => { @@ -45,7 +45,7 @@ export default new NamedCommand({ ); }, any: new RestCommand({ - async run({send, author, combined}) { + async run({send, message, channel, guild, author, member, client, args, 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 9964008..928ca57 100644 --- a/src/commands/fun/urban.ts +++ b/src/commands/fun/urban.ts @@ -1,4 +1,4 @@ -import {NamedCommand, RestCommand} from "../../core"; +import {Command, 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, combined}) { + 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(combined)) .then((res) => { diff --git a/src/commands/fun/vaporwave.ts b/src/commands/fun/vaporwave.ts index 6486c25..71ae065 100644 --- a/src/commands/fun/vaporwave.ts +++ b/src/commands/fun/vaporwave.ts @@ -1,4 +1,4 @@ -import {NamedCommand, RestCommand} from "../../core"; +import {Command, 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, combined}) { + 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 b30daa1..44c82b7 100644 --- a/src/commands/fun/weather.ts +++ b/src/commands/fun/weather.ts @@ -1,4 +1,4 @@ -import {NamedCommand, RestCommand} from "../../core"; +import {Command, 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, combined}) { + async run({send, message, channel, guild, author, member, client, args, combined}) { find( { search: combined, diff --git a/src/commands/fun/whois.ts b/src/commands/fun/whois.ts index 92d29d3..4ad9c97 100644 --- a/src/commands/fun/whois.ts +++ b/src/commands/fun/whois.ts @@ -1,4 +1,4 @@ -import {User} from "discord.js"; +import {User, GuildMember} from "discord.js"; import {Command, NamedCommand, getMemberByName, CHANNEL_TYPE, RestCommand} from "../../core"; // Quotes must be used here or the numbers will change @@ -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, author}) { + async run({send, message, channel, guild, author, member, client, args}) { const id = author.id; if (id in registry) { @@ -54,7 +54,7 @@ export default new NamedCommand({ }, id: "user", user: new Command({ - async run({send, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const user: User = args[0]; const id = user.id; @@ -67,10 +67,10 @@ export default new NamedCommand({ }), any: new RestCommand({ channelType: CHANNEL_TYPE.GUILD, - async run({send, guild, combined}) { + async run({send, message, channel, guild, author, client, args, combined}) { const member = await getMemberByName(guild!, combined); - if (typeof member !== "string") { + if (member instanceof GuildMember) { 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 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/calc.ts b/src/commands/utility/calc.ts index 649ba1f..ac0727d 100644 --- a/src/commands/utility/calc.ts +++ b/src/commands/utility/calc.ts @@ -1,4 +1,4 @@ -import {NamedCommand, RestCommand} from "../../core"; +import {Command, 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, combined}) { + async run({send, message, channel, guild, author, member, client, args, combined}) { let resp; try { resp = math.evaluate(combined); diff --git a/src/commands/utility/code.ts b/src/commands/utility/code.ts index 5c73897..39e41b7 100644 --- a/src/commands/utility/code.ts +++ b/src/commands/utility/code.ts @@ -1,4 +1,4 @@ -import {NamedCommand} from "../../core"; +import {Command, 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 4307469..9930df3 100644 --- a/src/commands/utility/desc.ts +++ b/src/commands/utility/desc.ts @@ -1,11 +1,11 @@ -import {NamedCommand, RestCommand} from "../../core"; +import {Command, 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, combined}) { + 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."); diff --git a/src/commands/utility/docs.ts b/src/commands/utility/docs.ts deleted file mode 100644 index 8c1d74b..0000000 --- a/src/commands/utility/docs.ts +++ /dev/null @@ -1,17 +0,0 @@ -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}); - } - }) -}); diff --git a/src/commands/utility/emote.ts b/src/commands/utility/emote.ts index 0cb48a2..4d79446 100644 --- a/src/commands/utility/emote.ts +++ b/src/commands/utility/emote.ts @@ -1,4 +1,4 @@ -import {NamedCommand, RestCommand} from "../../core"; +import {Command, 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, args}) { + async run({send, guild, channel, message, 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 b490bcd..5e51363 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, 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({send, author}) { + 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({send, args}) { + async run({send, message, channel, guild, author, member, client, args}) { send( args[0].displayAvatarURL({ dynamic: true, @@ -33,10 +33,10 @@ export default new NamedCommand({ any: new RestCommand({ description: "Shows another user's avatar by searching their name", channelType: CHANNEL_TYPE.GUILD, - async run({send, guild, combined}) { + async run({send, message, channel, guild, author, client, args, combined}) { const member = await getMemberByName(guild!, combined); - if (typeof member !== "string") { + if (member instanceof GuildMember) { send( member.user.displayAvatarURL({ dynamic: true, @@ -51,7 +51,7 @@ export default new NamedCommand({ }), bot: new NamedCommand({ description: "Displays info about the bot.", - async run({send, guild, client}) { + async run({send, message, channel, guild, author, member, client, args}) { const core = os.cpus()[0]; const embed = new MessageEmbed() .setColor(guild?.me?.displayHexColor || "BLUE") @@ -94,23 +94,23 @@ export default new NamedCommand({ description: "Displays info about the current guild or another guild.", usage: "(/)", channelType: CHANNEL_TYPE.GUILD, - async run({send, 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({send, guild, args}) { + async run({send, message, channel, guild, author, member, client, 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, guild, combined}) { + async run({send, message, channel, guild, author, member, client, args, combined}) { const targetGuild = getGuildByName(combined); - if (typeof targetGuild !== "string") { + if (targetGuild instanceof Guild) { send(await getGuildInfo(targetGuild, guild)); } else { send(targetGuild); @@ -122,7 +122,7 @@ export default new NamedCommand({ id: "user", user: new Command({ description: "Displays info about mentioned user.", - async run({send, guild, 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]); diff --git a/src/commands/utility/invite.ts b/src/commands/utility/invite.ts index 58b38d8..9dc0fa3 100644 --- a/src/commands/utility/invite.ts +++ b/src/commands/utility/invite.ts @@ -1,8 +1,8 @@ -import {NamedCommand} from "../../core"; +import {Command, NamedCommand} from "../../core"; export default new NamedCommand({ description: "Gives you the invite link.", - async run({send, client, args}) { + 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 diff --git a/src/commands/utility/lsemotes.ts b/src/commands/utility/lsemotes.ts index 955328a..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 {NamedCommand, RestCommand, paginate, SendFunction} from "../../core"; +import {Command, 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, author, client}) { + async run({send, message, channel, guild, author, member, client, args}) { 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, author, 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]; @@ -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/react.ts b/src/commands/utility/react.ts index 39301c9..ef1aebb 100644 --- a/src/commands/utility/react.ts +++ b/src/commands/utility/react.ts @@ -1,4 +1,4 @@ -import {NamedCommand, RestCommand} from "../../core"; +import {Command, 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, client, args}) { + async run({send, message, channel, guild, author, member, client, args}) { let target: Message | undefined; let distance = 1; diff --git a/src/commands/utility/say.ts b/src/commands/utility/say.ts index 5d8805a..0ffe639 100644 --- a/src/commands/utility/say.ts +++ b/src/commands/utility/say.ts @@ -1,4 +1,4 @@ -import {NamedCommand, RestCommand} from "../../core"; +import {Command, 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, author, combined}) { + async run({send, message, channel, guild, author, member, client, args, combined}) { send(`*${author} says:*\n${combined}`); } }) diff --git a/src/commands/utility/scanemotes.ts b/src/commands/utility/scanemotes.ts index e9fbcd0..08fb74f 100644 --- a/src/commands/utility/scanemotes.ts +++ b/src/commands/utility/scanemotes.ts @@ -1,4 +1,4 @@ -import {NamedCommand, CHANNEL_TYPE} from "../../core"; +import {Command, 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}) { + 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 @@ -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, guild}) { + async run({send, message, channel, guild, author, member, client, args}) { 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 c8a4618..9d74544 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, 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) { diff --git a/src/commands/utility/streaminfo.ts b/src/commands/utility/streaminfo.ts index b58b1f8..c122b8b 100644 --- a/src/commands/utility/streaminfo.ts +++ b/src/commands/utility/streaminfo.ts @@ -1,9 +1,9 @@ -import {NamedCommand, RestCommand} from "../../core"; +import {Command, 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, author, member}) { + async run({send, message, channel, guild, author, member, client, args}) { const userID = author.id; if (streamList.has(userID)) { @@ -22,7 +22,7 @@ export default new NamedCommand({ } }, any: new RestCommand({ - async run({send, author, member, combined}) { + async run({send, message, channel, guild, author, member, client, args, combined}) { const userID = author.id; if (streamList.has(userID)) { diff --git a/src/commands/utility/time.ts b/src/commands/utility/time.ts index 9852b86..7598f5a 100644 --- a/src/commands/utility/time.ts +++ b/src/commands/utility/time.ts @@ -1,6 +1,15 @@ -import {Command, NamedCommand, askForReply, confirm, askMultipleChoice, getMemberByName, RestCommand} from "../../core"; +import { + Command, + NamedCommand, + ask, + askYesOrNo, + askMultipleChoice, + prompt, + getMemberByName, + RestCommand +} 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"; @@ -169,184 +178,183 @@ function getTimeEmbed(user: User) { export default new NamedCommand({ description: "Show others what time it is for you.", aliases: ["tz"], - async run({send, 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({send, author}) { + async run({send, author, channel}) { const profile = Storage.getUser(author.id); profile.timezone = null; profile.daylightSavingsRegion = null; + let hour: number; - // Parse and validate reply - const reply = await askForReply( + ask( 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, - 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"); + (reply) => { + hour = parseInt(reply); - // 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. + if (isNaN(hour)) { + return false; + } - // (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. + 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. - // 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) + // (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. - // 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 + // 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) - // 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. + // 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 - // 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. + // 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. - 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 = []; + // 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. - // 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]); + 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 = []; - if (uniques.includes(hour)) { - duplicates.push(hour); - } else { - uniques.push(hour); - } - } + // 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]); - // 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; + if (uniques.includes(hour)) { + duplicates.push(hour); + } else { + uniques.push(hour); } } - } else { - for (const [hourPoint, dayOffset, timezoneOffset] of timezoneTupleList) { - if (dayOffset !== 0 && hour === hourPoint) { - profile.timezone = timezoneOffset; + + // 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; + } } } - } - } 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 + // 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" ); - - 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, author}) { - const result = await confirm( - await send("Are you sure you want to delete your timezone information?"), - author.id + 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(); + } ); - - 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}) { + async run({send, channel}) { const time = moment().utc(); send({ @@ -378,15 +386,15 @@ export default new NamedCommand({ id: "user", user: new Command({ description: "See what time it is for someone else.", - async run({send, args}) { + async run({send, channel, args}) { send(getTimeEmbed(args[0])); } }), any: new RestCommand({ description: "See what time it is for someone else (by their username).", - async run({send, guild, combined}) { + async run({send, channel, args, guild, combined}) { const member = await getMemberByName(guild!, combined); - if (typeof member !== "string") send(getTimeEmbed(member.user)); + 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 3962126..9057166 100644 --- a/src/commands/utility/todo.ts +++ b/src/commands/utility/todo.ts @@ -1,11 +1,11 @@ -import {NamedCommand, RestCommand} from "../../core"; +import {Command, 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, author}) { + 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"); @@ -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, author, combined}) { + 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); @@ -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, author, combined}) { + async run({send, message, channel, guild, author, member, client, args, combined}) { const user = Storage.getUser(author.id); let isFound = false; @@ -55,7 +55,7 @@ export default new NamedCommand({ }) }), clear: new NamedCommand({ - async run({send, author}) { + async run({send, message, channel, guild, author, member, client, args}) { 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 e3ca9ed..d4a1aab 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, args}) { + async run({send, message, channel, guild, author, member, client, args}) { const lang = args[0]; const input = args.slice(1).join(" "); translate(input, { diff --git a/src/core/command.ts b/src/core/command.ts index 7e024da..cded03c 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, SendFunction} 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"; @@ -244,14 +244,18 @@ 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, string if there's an error (to let the user know what it is). + // Will return null if it successfully executes, SingleMessageOptions 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; @@ -288,7 +292,9 @@ export class Command extends BaseCommand { const errorMessage = error.stack ?? error; console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`); - return `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\``; + return { + content: `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\`` + }; } } @@ -307,13 +313,15 @@ export class Command extends BaseCommand { const id = patterns.channel.exec(param)![1]; const channel = await getChannelByID(id); - if (typeof channel !== "string") { + 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 `\`${id}\` is not a valid text channel!`; + return { + content: `\`${id}\` is not a valid text channel!` + }; } } else { return channel; @@ -322,7 +330,9 @@ export class Command extends BaseCommand { const id = patterns.role.exec(param)![1]; if (!menu.guild) { - return "You can't use role parameters in DM channels!"; + return { + content: "You can't use role parameters in DM channels!" + }; } const role = menu.guild.roles.cache.get(id); @@ -332,7 +342,9 @@ export class Command extends BaseCommand { menu.args.push(role); return this.role.execute(args, menu, metadata); } else { - return `\`${id}\` is not a valid role in this server!`; + return { + content: `\`${id}\` is not a valid role in this server!` + }; } } else if (this.emote && patterns.emote.test(param)) { const id = patterns.emote.exec(param)![1]; @@ -343,7 +355,9 @@ export class Command extends BaseCommand { menu.args.push(emote); return this.emote.execute(args, menu, metadata); } else { - return `\`${id}\` isn't a valid emote!`; + return { + content: `\`${id}\` isn't a valid emote!` + }; } } else if (this.message && (isMessageLink || isMessagePair)) { let channelID = ""; @@ -361,7 +375,7 @@ export class Command extends BaseCommand { const message = await getMessageByID(channelID, messageID); - if (typeof message !== "string") { + if (message instanceof Message) { metadata.symbolicArgs.push(""); menu.args.push(message); return this.message.execute(args, menu, metadata); @@ -372,7 +386,7 @@ export class Command extends BaseCommand { const id = patterns.user.exec(param)![1]; const user = await getUserByID(id); - if (typeof user !== "string") { + if (user instanceof User) { metadata.symbolicArgs.push(""); menu.args.push(user); return this.user.execute(args, menu, metadata); @@ -389,20 +403,24 @@ export class Command extends BaseCommand { case "channel": const channel = await getChannelByID(id); - if (typeof channel !== "string") { + 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 `\`${id}\` is not a valid text channel!`; + return { + content: `\`${id}\` is not a valid text channel!` + }; } } else { return channel; } case "role": if (!menu.guild) { - return "You can't use role parameters in DM channels!"; + return { + content: "You can't use role parameters in DM channels!" + }; } const role = menu.guild.roles.cache.get(id); @@ -411,7 +429,9 @@ export class Command extends BaseCommand { menu.args.push(role); return this.id.execute(args, menu, metadata); } else { - return `\`${id}\` isn't a valid role in this server!`; + return { + content: `\`${id}\` isn't a valid role in this server!` + }; } case "emote": const emote = menu.client.emojis.cache.get(id); @@ -420,12 +440,14 @@ export class Command extends BaseCommand { menu.args.push(emote); return this.id.execute(args, menu, metadata); } else { - return `\`${id}\` isn't a valid emote!`; + return { + content: `\`${id}\` isn't a valid emote!` + }; } case "message": const message = await getMessageByID(menu.channel, id); - if (typeof message !== "string") { + if (message instanceof Message) { menu.args.push(message); return this.id.execute(args, menu, metadata); } else { @@ -434,7 +456,7 @@ export class Command extends BaseCommand { case "user": const user = await getUserByID(id); - if (typeof user !== "string") { + if (user instanceof User) { menu.args.push(user); return this.id.execute(args, menu, metadata); } else { @@ -443,7 +465,7 @@ export class Command extends BaseCommand { case "guild": const guild = getGuildByID(id); - if (typeof guild !== "string") { + if (guild instanceof Guild) { menu.args.push(guild); return this.id.execute(args, menu, metadata); } else { @@ -467,9 +489,11 @@ export class Command extends BaseCommand { return this.any.execute(args.join(" "), menu, metadata); } else { metadata.symbolicArgs.push(`"${param}"`); - return `No valid command sequence matching \`${metadata.header} ${metadata.symbolicArgs.join( - " " - )}\` found.`; + 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 @@ -658,7 +682,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; @@ -692,7 +716,9 @@ export class RestCommand extends BaseCommand { const errorMessage = error.stack ?? error; console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`); - return `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\``; + return { + content: `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\`` + }; } } @@ -717,34 +743,36 @@ 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): string | null { +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 "This command must be executed in a server."; + 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 "This command must be executed as a direct message."; + 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 "This command must be executed in either an NSFW channel or as a direct message."; + 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 `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 { + 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/libd.ts b/src/core/libd.ts index 58338a5..c9d5bcf 100644 --- a/src/core/libd.ts +++ b/src/core/libd.ts @@ -15,9 +15,7 @@ import { MessageAdditions, SplitOptions, APIMessage, - StringResolvable, - EmojiIdentifierResolvable, - MessageReaction + StringResolvable } from "discord.js"; import {unreactEventListeners, replyEventListeners} from "./eventListeners"; import {client} from "./interface"; @@ -33,6 +31,19 @@ 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 = "➡️"; @@ -45,35 +56,34 @@ const FIVE_FORWARDS_EMOJI = "⏩"; */ export async function paginate( send: SendFunction, - onTurnPage: (page: number, hasMultiplePages: boolean) => SingleMessageOptions, - totalPages: number, - listenTo: string | null = null, + senderID: string, + total: number, + callback: (page: number, hasMultiplePages: boolean) => SingleMessageOptions, duration = 60000 -): Promise { - const hasMultiplePages = totalPages > 1; - const message = await send(onTurnPage(0, hasMultiplePages)); +) { + const hasMultiplePages = total > 1; + const message = await send(callback(0, hasMultiplePages)); if (hasMultiplePages) { let page = 0; const turn = (amount: number) => { page += amount; - if (page >= totalPages) { - page %= totalPages; + 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) % totalPages; - if (flattened !== 0) page = totalPages - flattened; + const flattened = Math.abs(page) % total; + if (flattened !== 0) page = total - flattened; } - message.edit(onTurnPage(page, true)); + message.edit(callback(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. + if (senderID === reacterID) { switch (emote) { case FIVE_BACKWARDS_EMOJI: - if (totalPages > 5) turn(-5); + if (total > 5) turn(-5); break; case BACKWARDS_EMOJI: turn(-1); @@ -82,28 +92,28 @@ export async function paginate( turn(1); break; case FIVE_FORWARDS_EMOJI: - if (totalPages > 5) turn(5); + if (total > 5) turn(5); break; } } }; // Listen for reactions and call the handler. - let backwardsReactionFive = totalPages > 5 ? await message.react(FIVE_BACKWARDS_EMOJI) : null; + 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 = totalPages > 5 ? await message.react(FIVE_FORWARDS_EMOJI) : null; + let forwardsReactionFive = total > 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) { + if (user.id === senderID) { // 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; @@ -124,73 +134,100 @@ export async function paginate( } } -//export function generateMulti -// paginate after generateonetimeprompt +// 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; -// Returns null if timed out, otherwise, returns the value. -export function 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( 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)); + senderID: string, + condition: (reply: string) => boolean, + onSuccess: () => void, + onReject: () => string, + timeout = 60000 +) { + const referenceID = `${message.channel.id}-${message.id}`; - // Then setup the reaction listener in parallel. + 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("❌"); await message.awaitReactions( - (reaction: MessageReaction, user: User) => { - if (user.id === listenTo || listenTo === null) { - const emote = reaction.emoji.name; + (reaction, user) => { + if (user.id === senderID) { + const isCheckReacted = reaction.emoji.name === "✅"; - if (emote in stack) { - resolve(stack[emote]); + if (isCheckReacted || reaction.emoji.name === "❌") { + resolve(isCheckReacted); + 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} + {time: timeout} ); - if (!message.deleted) { + if (!isDeleted) { message.delete(); - resolve(null); + reject("Prompt timed out."); } }); } -// 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️⃣", "🔟"]; @@ -199,47 +236,40 @@ const multiNumbers = ["1️⃣", "2️⃣", "3️⃣", "4️⃣", "5️⃣", "6 export async function askMultipleChoice( message: Message, senderID: string, - choices: number, + callbackStack: (() => void)[], timeout = 90000 -): Promise { - if (choices > multiNumbers.length) - throw new Error( - `askMultipleChoice only accepts up to ${multiNumbers.length} options, ${choices} was provided.` +) { + 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})!\`` ); - const numbers: {[emote: string]: number} = {}; - for (let i = 0; i < choices; i++) numbers[multiNumbers[i]] = i; - return generateOneTimePrompt(message, numbers, senderID, timeout); -} + return; + } -// 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}`; + let isDeleted = false; - replyEventListeners.set(referenceID, (reply) => { - if (reply.author.id === listenTo) { - message.delete(); - replyEventListeners.delete(referenceID); - resolve(reply); + 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(); + } } - }); - if (timeout) { - client.setTimeout(() => { - if (!message.deleted) message.delete(); - replyEventListeners.delete(referenceID); - resolve(null); - }, timeout); - } - }); -} + return false; + }, + {time: timeout} + ); -/** - * 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); + if (!isDeleted) message.delete(); } // For "get x by y" methods: @@ -247,75 +277,79 @@ export function botHasPermission(guild: Guild | null, permission: number): boole // 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 | string { +export function getGuildByID(id: string): Guild | SingleMessageOptions { const guild = client.guilds.cache.get(id); if (guild) { if (guild.available) return guild; - else return `The guild \`${guild.name}\` (ID: \`${id}\`) is unavailable due to an outage.`; + else return {content: `The guild \`${guild.name}\` (ID: \`${id}\`) is unavailable due to an outage.`}; } else { - return `No guild found by the ID of \`${id}\`!`; + return { + content: `No guild found by the ID of \`${id}\`!` + }; } } -export function getGuildByName(name: string): Guild | string { +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 `The guild \`${guild.name}\` (ID: \`${guild.id}\`) is unavailable due to an outage.`; + else return {content: `The guild \`${guild.name}\` (ID: \`${guild.id}\`) is unavailable due to an outage.`}; } else { - return `No guild found by the name of \`${name}\`!`; + return { + content: `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 `No channel found by the ID of \`${id}\`!`; + 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 | string { +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 `No channel found by the name of \`${name}\`!`; + else return {content: `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 `\`${id}\` isn't a valid text-based channel!`; + 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 `\`${id}\` isn't a valid message of the channel ${channel}!`; + return {content: `\`${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 `No user found by the ID of \`${id}\`!`; + 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 | string { +export function getUserByName(name: string): User | SingleMessageOptions { let query = name.toLowerCase(); const tagMatch = /^(.+?)#(\d{4})$/.exec(name); let tag: string | null = null; @@ -332,19 +366,19 @@ export function getUserByName(name: string): User | string { }); if (user) return user; - else return `No user found by the name of \`${name}\`!`; + else return {content: `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 `No member found by the ID of \`${id}\`!`; + 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 { +export async function getMemberByName(guild: Guild, name: string): Promise { const member = ( await guild.members.fetch({ query: name, @@ -361,9 +395,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 (typeof linkMessage === "string") { + if (!(linkMessage instanceof Message)) { return message.channel.send("I don't have access to that channel!"); }