Compare commits

..

3 commits

Author SHA1 Message Date
0a265dcd5c
Removed unused run args and Command imports. 2021-04-11 11:11:21 +02:00
51d19d5787
Added Discord.JS Docs command. 2021-04-11 10:58:06 +02:00
WatDuhHekBro
c980a182f8 Updated library functions 2021-04-11 03:02:56 -05:00
42 changed files with 568 additions and 614 deletions

View file

@ -75,27 +75,24 @@ Because versions are assigned to batches of changes rather than single changes (
```ts ```ts
const pages = ["one", "two", "three"]; const pages = ["one", "two", "three"];
paginate(channel, author.id, pages.length, (page) => { paginate(send, page => {
return { return {content: pages[page]};
content: pages[page] }, pages.length, author.id);
};
});
``` ```
`prompt()` `confirm()`
```ts ```ts
const msg = await channel.send('Are you sure you want to delete this?'); const result = await confirm(await send("Are you sure you want to delete this?"), author.id); // boolean | null
prompt(msg, author.id, () => {
//...
});
``` ```
`callMemberByUsername()` `askMultipleChoice()`
```ts ```ts
callMemberByUsername(message, args.join(" "), (member) => { const result = await askMultipleChoice(await send("Which of the following numbers is your favorite?"), author.id, 4, 10000); // number (0 to 3) | null
channel.send(`Your nickname is ${member.nickname}.`); ```
});
`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 ## [src/lib](../src/lib.ts) - General utility functions

View file

@ -30,7 +30,7 @@ export default new NamedCommand({
run: "Please provide a question.", run: "Please provide a question.",
any: new Command({ any: new Command({
description: "Question to ask the 8-ball.", 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; const sender = message.author;
send(`${random(responses)} <@${sender.id}>`); send(`${random(responses)} <@${sender.id}>`);
} }

View file

@ -31,7 +31,7 @@ export default new NamedCommand({
run: ":cookie: Here's a cookie!", run: ":cookie: Here's a cookie!",
subcommands: { subcommands: {
all: new NamedCommand({ all: new NamedCommand({
async run({send, message, channel, guild, author, member, client, args}) { async run({send, author}) {
send(`${author} gave everybody a cookie!`); send(`${author} gave everybody a cookie!`);
} }
}) })
@ -39,7 +39,7 @@ export default new NamedCommand({
id: "user", id: "user",
user: new Command({ user: new Command({
description: "User to give cookie to.", 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 sender = author;
const mention: User = args[0]; const mention: User = args[0];

View file

@ -4,7 +4,6 @@ import {DailyCommand, PayCommand, GuildCommand, LeaderboardCommand} from "./modu
import {BuyCommand, ShopCommand} from "./modules/eco-shop"; import {BuyCommand, ShopCommand} from "./modules/eco-shop";
import {MondayCommand, AwardCommand} from "./modules/eco-extras"; import {MondayCommand, AwardCommand} from "./modules/eco-extras";
import {BetCommand} from "./modules/eco-bet"; import {BetCommand} from "./modules/eco-bet";
import {GuildMember} from "discord.js";
export default new NamedCommand({ export default new NamedCommand({
description: "Economy command for Monika.", description: "Economy command for Monika.",
@ -35,10 +34,10 @@ export default new NamedCommand({
}), }),
any: new RestCommand({ any: new RestCommand({
description: "See how much money someone else has by using their username.", 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)) { if (isAuthorized(guild, channel)) {
const member = await getMemberByName(guild!, combined); 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); else send(member);
} }
} }

View file

@ -1,11 +1,11 @@
import {Command, NamedCommand, RestCommand} from "../../core"; import {NamedCommand, RestCommand} from "../../core";
import figlet from "figlet"; import figlet from "figlet";
export default new NamedCommand({ export default new NamedCommand({
description: "Generates a figlet of your input.", description: "Generates a figlet of your input.",
run: "You have to provide input for me to create a figlet!", run: "You have to provide input for me to create a figlet!",
any: new RestCommand({ any: new RestCommand({
async run({send, message, channel, guild, author, member, client, args, combined}) { async run({send, combined}) {
return send( return send(
figlet.textSync(combined, { figlet.textSync(combined, {
horizontalLayout: "full" horizontalLayout: "full"

View file

@ -1,8 +1,8 @@
import {Command, NamedCommand} from "../../core"; import {NamedCommand} from "../../core";
export default new NamedCommand({ export default new NamedCommand({
description: "Insult TravBot! >:D", description: "Insult TravBot! >:D",
async run({send, message, channel, guild, author, member, client, args}) { async run({send, channel, author}) {
channel.startTyping(); channel.startTyping();
setTimeout(() => { setTimeout(() => {
send( send(

View file

@ -1,9 +1,9 @@
import {Command, NamedCommand, CHANNEL_TYPE} from "../../core"; import {NamedCommand, CHANNEL_TYPE} from "../../core";
export default new NamedCommand({ export default new NamedCommand({
description: "Chooses someone to love.", description: "Chooses someone to love.",
channelType: CHANNEL_TYPE.GUILD, channelType: CHANNEL_TYPE.GUILD,
async run({send, message, channel, guild, author, client, args}) { async run({send, guild}) {
const member = guild!.members.cache.random(); const member = guild!.members.cache.random();
send(`I love ${member.nickname ?? member.user.username}!`); send(`I love ${member.nickname ?? member.user.username}!`);
} }

View file

@ -1,7 +1,7 @@
import {Command, NamedCommand, askYesOrNo} from "../../../core"; import {Command, NamedCommand, confirm} from "../../../core";
import {pluralise} from "../../../lib"; import {pluralise} from "../../../lib";
import {Storage} from "../../../structures"; 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"; import {User} from "discord.js";
export const BetCommand = new NamedCommand({ export const BetCommand = new NamedCommand({
@ -79,14 +79,15 @@ export const BetCommand = new NamedCommand({
return 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. // Ask target whether or not they want to take the bet.
const takeBet = await askYesOrNo( const takeBet = await confirm(
await send( await send(
`<@${target.id}>, do you want to take this bet of ${pluralise(amount, "Mon", "s")}` `<@${target.id}>, do you want to take this bet of ${pluralise(amount, "Mon", "s")}`
), ),
target.id target.id
); );
if (takeBet) { if (!takeBet) return send(`<@${target.id}> has rejected your bet, <@${author.id}>`);
// [MEDIUM PRIORITY: bet persistence to prevent losses in case of shutdown.] // [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. // Remove amount money from both parts at the start to avoid duplication of money.
sender.money -= amount; sender.money -= amount;
@ -97,7 +98,7 @@ export const BetCommand = new NamedCommand({
Storage.save(); Storage.save();
// Notify both users. // Notify both users.
await send( send(
`<@${target.id}> has taken <@${author.id}>'s bet, the bet amount of ${pluralise( `<@${target.id}> has taken <@${author.id}>'s bet, the bet amount of ${pluralise(
amount, amount,
"Mon", "Mon",
@ -155,12 +156,12 @@ export const BetCommand = new NamedCommand({
`By the people's votes, <@${target.id}> couldn't be determined to have won or lost the bet that <@${author.id}> had sent them.` `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; sender.ecoBetInsurance -= amount;
receiver.ecoBetInsurance -= amount; receiver.ecoBetInsurance -= amount;
Storage.save(); Storage.save();
}); });
}, duration); }, duration);
} else return await send(`<@${target.id}> has rejected your bet, <@${author.id}>`);
} else return; } else return;
} }
}) })

View file

@ -1,5 +1,4 @@
import {GuildMember} from "discord.js"; import {Command, getMemberByName, NamedCommand, confirm, RestCommand} from "../../../core";
import {Command, getMemberByName, NamedCommand, prompt, RestCommand} from "../../../core";
import {pluralise} from "../../../lib"; import {pluralise} from "../../../lib";
import {Storage} from "../../../structures"; import {Storage} from "../../../structures";
import {isAuthorized, getMoneyEmbed, getSendEmbed, ECO_EMBED_COLOR} from "./eco-utils"; 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); const user = await client.users.fetch(id);
fields.push({ fields.push({
name: `#${i + 1}. ${user.username}#${user.discriminator}`, name: `#${i + 1}. ${user.tag}`,
value: pluralise(users[id].money, "Mon", "s") 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!"); 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); 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.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!"); else if (member.user.bot && process.argv[2] !== "dev") return send("You can't send Mons to a bot!");
const target = member.user; const target = member.user;
return prompt( const result = await confirm(
await send( await send(`Are you sure you want to send ${pluralise(amount, "Mon", "s")} to this person?`, {
`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: { embed: {
color: ECO_EMBED_COLOR, color: ECO_EMBED_COLOR,
author: { author: {
name: `${target.username}#${target.discriminator}`, name: target.tag,
icon_url: target.displayAvatarURL({ icon_url: target.displayAvatarURL({
format: "png", format: "png",
dynamic: true dynamic: true
}) })
} }
} }
} }),
), author.id
author.id, );
() => {
if (result) {
const receiver = Storage.getUser(target.id); const receiver = Storage.getUser(target.id);
sender.money -= amount; sender.money -= amount;
receiver.money += amount; receiver.money += amount;
Storage.save(); Storage.save();
send(getSendEmbed(author, target, amount)); send(getSendEmbed(author, target, amount));
} }
);
} }
return;
} }
}) })
}); });

View file

@ -34,12 +34,17 @@ export const ShopCommand = new NamedCommand({
const shopPages = split(ShopItems, 5); const shopPages = split(ShopItems, 5);
const pageAmount = shopPages.length; const pageAmount = shopPages.length;
paginate(send, author.id, pageAmount, (page, hasMultiplePages) => { paginate(
send,
(page, hasMultiplePages) => {
return getShopEmbed( return getShopEmbed(
shopPages[page], shopPages[page],
hasMultiplePages ? `Shop (Page ${page + 1} of ${pageAmount})` : "Shop" hasMultiplePages ? `Shop (Page ${page + 1} of ${pageAmount})` : "Shop"
); );
}); },
pageAmount,
author.id
);
} }
} }
}); });

View file

@ -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()}!`, description: `${sender.toString()} has sent ${pluralise(amount, "Mon", "s")} to ${receiver.toString()}!`,
fields: [ fields: [
{ {
name: `Sender: ${sender.username}#${sender.discriminator}`, name: `Sender: ${sender.tag}`,
value: pluralise(Storage.getUser(sender.id).money, "Mon", "s") 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") value: pluralise(Storage.getUser(receiver.id).money, "Mon", "s")
} }
], ],

View file

@ -36,12 +36,12 @@ const endpoints: {sfw: {[key: string]: string}} = {
export default new NamedCommand({ export default new NamedCommand({
description: "Provides you with a random image with the selected argument.", 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(", ")}]\`.`); send(`Please provide an image type. Available arguments:\n\`[${Object.keys(endpoints.sfw).join(", ")}]\`.`);
}, },
any: new Command({ any: new Command({
description: "Image type to send.", description: "Image type to send.",
async run({send, message, channel, guild, author, member, client, args}) { async run({send, args}) {
const arg = args[0]; const arg = args[0];
if (!(arg in endpoints.sfw)) return 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]}`); let url = new URL(`https://nekos.life/api/v2${endpoints.sfw[arg]}`);

View file

@ -1,4 +1,4 @@
import {Command, NamedCommand} from "../../core"; import {NamedCommand} from "../../core";
import {random} from "../../lib"; import {random} from "../../lib";
const responses = [ const responses = [
@ -61,7 +61,7 @@ const responses = [
export default new NamedCommand({ export default new NamedCommand({
description: "Sends random ok message.", description: "Sends random ok message.",
async run({send, message, channel, guild, author, member, client, args}) { async run({send}) {
send(`ok ${random(responses)}`); send(`ok ${random(responses)}`);
} }
}); });

View file

@ -1,4 +1,4 @@
import {Command, NamedCommand, RestCommand} from "../../core"; import {NamedCommand, RestCommand} from "../../core";
import {getContent} from "../../lib"; import {getContent} from "../../lib";
import {URL} from "url"; import {URL} from "url";
@ -6,7 +6,7 @@ export default new NamedCommand({
description: "OwO-ifies the input.", description: "OwO-ifies the input.",
run: "You need to specify some text to owoify.", run: "You need to specify some text to owoify.",
any: new RestCommand({ 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}`); 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}. const content = (await getContent(url.toString())) as any; // Apparently, the object in question is {owo: string}.
send(content.owo); send(content.owo);

View file

@ -1,8 +1,8 @@
import {Command, NamedCommand} from "../../core"; import {NamedCommand} from "../../core";
export default new NamedCommand({ export default new NamedCommand({
description: "Initiates a celebratory stream from the bot.", 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!"); send("This calls for a celebration!");
client.user!.setActivity({ client.user!.setActivity({
type: "STREAMING", type: "STREAMING",

View file

@ -1,5 +1,5 @@
import {MessageEmbed} from "discord.js"; import {MessageEmbed} from "discord.js";
import {Command, NamedCommand, RestCommand} from "../../core"; import {NamedCommand, RestCommand} from "../../core";
export default new NamedCommand({ export default new NamedCommand({
description: "Create a poll.", description: "Create a poll.",
@ -7,7 +7,7 @@ export default new NamedCommand({
run: "Please provide a question.", run: "Please provide a question.",
any: new RestCommand({ any: new RestCommand({
description: "Question for the poll.", 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() const embed = new MessageEmbed()
.setAuthor( .setAuthor(
`Poll created by ${message.author.username}`, `Poll created by ${message.author.username}`,

View file

@ -4,7 +4,7 @@ import {Random} from "../../lib";
export default new NamedCommand({ export default new NamedCommand({
description: "Ravioli ravioli...", description: "Ravioli ravioli...",
usage: "[number from 1 to 9]", usage: "[number from 1 to 9]",
async run({send, message, channel, guild, author, member, client, args}) { async run({send}) {
send({ send({
embed: { embed: {
title: "Ravioli ravioli...", title: "Ravioli ravioli...",
@ -18,7 +18,7 @@ export default new NamedCommand({
}); });
}, },
number: new Command({ number: new Command({
async run({send, message, channel, guild, author, member, client, args}) { async run({send, args}) {
const arg: number = args[0]; const arg: number = args[0];
if (arg >= 1 && arg <= 9) { if (arg >= 1 && arg <= 9) {

View file

@ -1,4 +1,4 @@
import {Command, NamedCommand, RestCommand} from "../../core"; import {NamedCommand, RestCommand} from "../../core";
const letters: {[letter: string]: string[]} = { const letters: {[letter: string]: string[]} = {
a: "aáàảãạâấầẩẫậăắằẳẵặ".split(""), a: "aáàảãạâấầẩẫậăắằẳẵặ".split(""),
@ -34,7 +34,7 @@ let phrase = "I have no currently set phrase!";
export default new NamedCommand({ export default new NamedCommand({
description: "Transforms your text into .", description: "Transforms your text into .",
usage: "thonk ([text])", usage: "thonk ([text])",
async run({send, message, channel, guild, author, member, client, args}) { async run({send, author}) {
const msg = await send(transform(phrase)); const msg = await send(transform(phrase));
msg.createReactionCollector( msg.createReactionCollector(
(reaction, user) => { (reaction, user) => {
@ -45,7 +45,7 @@ export default new NamedCommand({
); );
}, },
any: new RestCommand({ 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)); const msg = await send(transform(combined));
msg.createReactionCollector( msg.createReactionCollector(
(reaction, user) => { (reaction, user) => {

View file

@ -1,4 +1,4 @@
import {Command, NamedCommand, RestCommand} from "../../core"; import {NamedCommand, RestCommand} from "../../core";
import {MessageEmbed} from "discord.js"; import {MessageEmbed} from "discord.js";
import urban from "relevant-urban"; import urban from "relevant-urban";
@ -6,7 +6,7 @@ export default new NamedCommand({
description: "Gives you a definition of the inputted word.", description: "Gives you a definition of the inputted word.",
run: "Please input a word.", run: "Please input a word.",
any: new RestCommand({ 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" // [Bug Fix]: Use encodeURIComponent() when emojis are used: "TypeError [ERR_UNESCAPED_CHARACTERS]: Request path contains unescaped characters"
urban(encodeURIComponent(combined)) urban(encodeURIComponent(combined))
.then((res) => { .then((res) => {

View file

@ -1,4 +1,4 @@
import {Command, NamedCommand, RestCommand} from "../../core"; import {NamedCommand, RestCommand} from "../../core";
const vaporwave = (() => { const vaporwave = (() => {
const map = new Map<string, string>(); const map = new Map<string, string>();
@ -25,7 +25,7 @@ export default new NamedCommand({
description: "Transforms your text into .", description: "Transforms your text into .",
run: "You need to enter some text!", run: "You need to enter some text!",
any: new RestCommand({ any: new RestCommand({
async run({send, message, channel, guild, author, member, client, args, combined}) { async run({send, combined}) {
const text = getVaporwaveText(combined); const text = getVaporwaveText(combined);
if (text !== "") send(text); if (text !== "") send(text);
else send("Make sure to enter at least one valid character."); else send("Make sure to enter at least one valid character.");

View file

@ -1,4 +1,4 @@
import {Command, NamedCommand, RestCommand} from "../../core"; import {NamedCommand, RestCommand} from "../../core";
import {MessageEmbed} from "discord.js"; import {MessageEmbed} from "discord.js";
import {find} from "weather-js"; import {find} from "weather-js";
@ -6,7 +6,7 @@ export default new NamedCommand({
description: "Shows weather info of specified location.", description: "Shows weather info of specified location.",
run: "You need to provide a city.", run: "You need to provide a city.",
any: new RestCommand({ any: new RestCommand({
async run({send, message, channel, guild, author, member, client, args, combined}) { async run({send, combined}) {
find( find(
{ {
search: combined, search: combined,

View file

@ -1,4 +1,4 @@
import {User, GuildMember} from "discord.js"; import {User} from "discord.js";
import {Command, NamedCommand, getMemberByName, CHANNEL_TYPE, RestCommand} from "../../core"; import {Command, NamedCommand, getMemberByName, CHANNEL_TYPE, RestCommand} from "../../core";
// Quotes must be used here or the numbers will change // Quotes must be used here or the numbers will change
@ -43,7 +43,7 @@ const registry: {[id: string]: string} = {
export default new NamedCommand({ export default new NamedCommand({
description: "Tells you who you or the specified user is.", description: "Tells you who you or the specified user is.",
aliases: ["whoami"], aliases: ["whoami"],
async run({send, message, channel, guild, author, member, client, args}) { async run({send, author}) {
const id = author.id; const id = author.id;
if (id in registry) { if (id in registry) {
@ -54,7 +54,7 @@ export default new NamedCommand({
}, },
id: "user", id: "user",
user: new Command({ user: new Command({
async run({send, message, channel, guild, author, member, client, args}) { async run({send, args}) {
const user: User = args[0]; const user: User = args[0];
const id = user.id; const id = user.id;
@ -67,10 +67,10 @@ export default new NamedCommand({
}), }),
any: new RestCommand({ any: new RestCommand({
channelType: CHANNEL_TYPE.GUILD, 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); const member = await getMemberByName(guild!, combined);
if (member instanceof GuildMember) { if (typeof member !== "string") {
if (member.id in registry) { if (member.id in registry) {
send(`\`${member.nickname ?? member.user.username}\` - ${registry[member.id]}`); send(`\`${member.nickname ?? member.user.username}\` - ${registry[member.id]}`);
} else { } else {

View file

@ -20,7 +20,9 @@ export default new NamedCommand({
const commands = await getCommandList(); const commands = await getCommandList();
const categoryArray = commands.keyArray(); const categoryArray = commands.keyArray();
paginate(send, author.id, categoryArray.length, (page, hasMultiplePages) => { paginate(
send,
(page, hasMultiplePages) => {
const category = categoryArray[page]; const category = categoryArray[page];
const commandList = commands.get(category)!; const commandList = commands.get(category)!;
let output = `Legend: \`<type>\`, \`[list/of/stuff]\`, \`(optional)\`, \`(<optional type>)\`, \`([optional/list/...])\`\n`; let output = `Legend: \`<type>\`, \`[list/of/stuff]\`, \`(optional)\`, \`(<optional type>)\`, \`([optional/list/...])\`\n`;
@ -29,7 +31,10 @@ export default new NamedCommand({
.setTitle(hasMultiplePages ? `${category} (Page ${page + 1} of ${categoryArray.length})` : category) .setTitle(hasMultiplePages ? `${category} (Page ${page + 1} of ${categoryArray.length})` : category)
.setDescription(output) .setDescription(output)
.setColor(EMBED_COLOR); .setColor(EMBED_COLOR);
}); },
categoryArray.length,
author.id
);
}, },
any: new Command({ any: new Command({
async run({send, message, channel, guild, author, member, client, args}) { async run({send, message, channel, guild, author, member, client, args}) {

View file

@ -1,4 +1,4 @@
import {Command, NamedCommand, RestCommand} from "../../core"; import {NamedCommand, RestCommand} from "../../core";
import * as math from "mathjs"; import * as math from "mathjs";
import {MessageEmbed} from "discord.js"; import {MessageEmbed} from "discord.js";
@ -6,7 +6,7 @@ export default new NamedCommand({
description: "Calculates a specified math expression.", description: "Calculates a specified math expression.",
run: "Please provide a calculation.", run: "Please provide a calculation.",
any: new RestCommand({ any: new RestCommand({
async run({send, message, channel, guild, author, member, client, args, combined}) { async run({send, combined}) {
let resp; let resp;
try { try {
resp = math.evaluate(combined); resp = math.evaluate(combined);

View file

@ -1,4 +1,4 @@
import {Command, NamedCommand} from "../../core"; import {NamedCommand} from "../../core";
export default new NamedCommand({ export default new NamedCommand({
description: "Gives you the Github link.", description: "Gives you the Github link.",

View file

@ -1,11 +1,11 @@
import {Command, NamedCommand, RestCommand} from "../../core"; import {NamedCommand, RestCommand} from "../../core";
export default new NamedCommand({ export default new NamedCommand({
description: "Renames current voice channel.", description: "Renames current voice channel.",
usage: "<name>", usage: "<name>",
run: "Please provide a new voice channel name.", run: "Please provide a new voice channel name.",
any: new RestCommand({ 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; const voiceChannel = message.member?.voice.channel;
if (!voiceChannel) return send("You are not in a voice channel."); if (!voiceChannel) return send("You are not in a voice channel.");

View file

@ -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});
}
})
});

View file

@ -1,4 +1,4 @@
import {Command, NamedCommand, RestCommand} from "../../core"; import {NamedCommand, RestCommand} from "../../core";
import {processEmoteQueryFormatted} from "./modules/emote-utils"; import {processEmoteQueryFormatted} from "./modules/emote-utils";
export default new NamedCommand({ export default new NamedCommand({
@ -8,7 +8,7 @@ export default new NamedCommand({
any: new RestCommand({ any: new RestCommand({
description: "The emote(s) to send.", description: "The emote(s) to send.",
usage: "<emotes...>", usage: "<emotes...>",
async run({send, guild, channel, message, args}) { async run({send, args}) {
const output = processEmoteQueryFormatted(args); const output = processEmoteQueryFormatted(args);
if (output.length > 0) send(output); if (output.length > 0) send(output);
} }

View file

@ -8,20 +8,20 @@ import moment, {utc} from "moment";
export default new NamedCommand({ export default new NamedCommand({
description: "Command to provide all sorts of info about the current server, a user, etc.", 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)); send(await getUserInfo(author, member));
}, },
subcommands: { subcommands: {
avatar: new NamedCommand({ avatar: new NamedCommand({
description: "Shows your own, or another user's avatar.", description: "Shows your own, or another user's avatar.",
usage: "(<user>)", usage: "(<user>)",
async run({send, message, channel, guild, author, member, client, args}) { async run({send, author}) {
send(author.displayAvatarURL({dynamic: true, size: 2048})); send(author.displayAvatarURL({dynamic: true, size: 2048}));
}, },
id: "user", id: "user",
user: new Command({ user: new Command({
description: "Shows your own, or another user's avatar.", description: "Shows your own, or another user's avatar.",
async run({send, message, channel, guild, author, member, client, args}) { async run({send, args}) {
send( send(
args[0].displayAvatarURL({ args[0].displayAvatarURL({
dynamic: true, dynamic: true,
@ -33,10 +33,10 @@ export default new NamedCommand({
any: new RestCommand({ any: new RestCommand({
description: "Shows another user's avatar by searching their name", description: "Shows another user's avatar by searching their name",
channelType: CHANNEL_TYPE.GUILD, 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); const member = await getMemberByName(guild!, combined);
if (member instanceof GuildMember) { if (typeof member !== "string") {
send( send(
member.user.displayAvatarURL({ member.user.displayAvatarURL({
dynamic: true, dynamic: true,
@ -51,7 +51,7 @@ export default new NamedCommand({
}), }),
bot: new NamedCommand({ bot: new NamedCommand({
description: "Displays info about the bot.", 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 core = os.cpus()[0];
const embed = new MessageEmbed() const embed = new MessageEmbed()
.setColor(guild?.me?.displayHexColor || "BLUE") .setColor(guild?.me?.displayHexColor || "BLUE")
@ -94,23 +94,23 @@ export default new NamedCommand({
description: "Displays info about the current guild or another guild.", description: "Displays info about the current guild or another guild.",
usage: "(<guild name>/<guild ID>)", usage: "(<guild name>/<guild ID>)",
channelType: CHANNEL_TYPE.GUILD, channelType: CHANNEL_TYPE.GUILD,
async run({send, message, channel, guild, author, member, client, args}) { async run({send, guild}) {
send(await getGuildInfo(guild!, guild)); send(await getGuildInfo(guild!, guild));
}, },
id: "guild", id: "guild",
guild: new Command({ guild: new Command({
description: "Display info about a guild by its ID.", 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; const targetGuild = args[0] as Guild;
send(await getGuildInfo(targetGuild, guild)); send(await getGuildInfo(targetGuild, guild));
} }
}), }),
any: new RestCommand({ any: new RestCommand({
description: "Display info about a guild by finding its name.", 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); const targetGuild = getGuildByName(combined);
if (targetGuild instanceof Guild) { if (typeof targetGuild !== "string") {
send(await getGuildInfo(targetGuild, guild)); send(await getGuildInfo(targetGuild, guild));
} else { } else {
send(targetGuild); send(targetGuild);
@ -122,7 +122,7 @@ export default new NamedCommand({
id: "user", id: "user",
user: new Command({ user: new Command({
description: "Displays info about mentioned user.", 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; const user = args[0] as User;
// Transforms the User object into a GuildMember object of the current guild. // Transforms the User object into a GuildMember object of the current guild.
const member = guild?.members.resolve(args[0]); const member = guild?.members.resolve(args[0]);

View file

@ -1,8 +1,8 @@
import {Command, NamedCommand} from "../../core"; import {NamedCommand} from "../../core";
export default new NamedCommand({ export default new NamedCommand({
description: "Gives you the invite link.", description: "Gives you the invite link.",
async run({send, message, channel, guild, author, member, client, args}) { async run({send, client, args}) {
send( send(
`https://discordapp.com/api/oauth2/authorize?client_id=${client.user!.id}&permissions=${ `https://discordapp.com/api/oauth2/authorize?client_id=${client.user!.id}&permissions=${
args[0] || 8 args[0] || 8

View file

@ -1,5 +1,5 @@
import {GuildEmoji, MessageEmbed, User} from "discord.js"; 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 {split} from "../../lib";
import vm from "vm"; import vm from "vm";
@ -8,13 +8,13 @@ const REGEX_TIMEOUT_MS = 1000;
export default new NamedCommand({ export default new NamedCommand({
description: "Lists all emotes the bot has in it's registry,", description: "Lists all emotes the bot has in it's registry,",
usage: "<regex pattern> (-flags)", usage: "<regex pattern> (-flags)",
async run({send, message, channel, guild, author, member, client, args}) { async run({send, author, client}) {
displayEmoteList(client.emojis.cache.array(), send, author); displayEmoteList(client.emojis.cache.array(), send, author);
}, },
any: new RestCommand({ any: new RestCommand({
description: 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", "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 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])) { if (args.length === 1 && /^\d{17,}$/.test(args[0])) {
const guildID: string = args[0]; const guildID: string = args[0];
@ -90,7 +90,9 @@ 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) // Gather the first page (if it even exists, which it might not if there no valid emotes appear)
if (pages > 0) { if (pages > 0) {
paginate(send, author.id, pages, (page, hasMultiplePages) => { paginate(
send,
(page, hasMultiplePages) => {
embed.setTitle(hasMultiplePages ? `**Emotes** (Page ${page + 1} of ${pages})` : "**Emotes**"); embed.setTitle(hasMultiplePages ? `**Emotes** (Page ${page + 1} of ${pages})` : "**Emotes**");
let desc = ""; let desc = "";
@ -100,7 +102,10 @@ async function displayEmoteList(emotes: GuildEmoji[], send: SendFunction, author
embed.setDescription(desc); embed.setDescription(desc);
return embed; return embed;
}); },
pages,
author.id
);
} else { } else {
send("No valid emotes found by that query."); send("No valid emotes found by that query.");
} }

View file

@ -1,4 +1,4 @@
import {Command, NamedCommand, RestCommand} from "../../core"; import {NamedCommand, RestCommand} from "../../core";
import {Message, Channel, TextChannel} from "discord.js"; import {Message, Channel, TextChannel} from "discord.js";
import {processEmoteQueryArray} from "./modules/emote-utils"; import {processEmoteQueryArray} from "./modules/emote-utils";
@ -8,7 +8,7 @@ export default new NamedCommand({
usage: 'react <emotes...> (<distance / message ID / "Copy ID" / "Copy Message Link">)', usage: 'react <emotes...> (<distance / message ID / "Copy ID" / "Copy Message Link">)',
run: "You need to enter some emotes first.", run: "You need to enter some emotes first.",
any: new RestCommand({ 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 target: Message | undefined;
let distance = 1; let distance = 1;

View file

@ -1,4 +1,4 @@
import {Command, NamedCommand, RestCommand} from "../../core"; import {NamedCommand, RestCommand} from "../../core";
export default new NamedCommand({ export default new NamedCommand({
description: "Repeats your message.", description: "Repeats your message.",
@ -6,7 +6,7 @@ export default new NamedCommand({
run: "Please provide a message for me to say!", run: "Please provide a message for me to say!",
any: new RestCommand({ any: new RestCommand({
description: "Message to repeat.", 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}`); send(`*${author} says:*\n${combined}`);
} }
}) })

View file

@ -1,4 +1,4 @@
import {Command, NamedCommand, CHANNEL_TYPE} from "../../core"; import {NamedCommand, CHANNEL_TYPE} from "../../core";
import {pluralise} from "../../lib"; import {pluralise} from "../../lib";
import moment from "moment"; import moment from "moment";
import {Collection, TextChannel} from "discord.js"; import {Collection, TextChannel} from "discord.js";
@ -9,7 +9,7 @@ export default new NamedCommand({
description: 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.", "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, 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. // 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 startTime = Date.now();
const cooldown = 86400000; // 24 hours const cooldown = 86400000; // 24 hours
@ -185,7 +185,7 @@ export default new NamedCommand({
forcereset: new NamedCommand({ forcereset: new NamedCommand({
description: "Forces the cooldown timer to reset.", description: "Forces the cooldown timer to reset.",
permission: PERMISSIONS.BOT_SUPPORT, permission: PERMISSIONS.BOT_SUPPORT,
async run({send, message, channel, guild, author, member, client, args}) { async run({send, guild}) {
lastUsedTimestamps.set(guild!.id, 0); lastUsedTimestamps.set(guild!.id, 0);
send("Reset the cooldown on `scanemotes`."); send("Reset the cooldown on `scanemotes`.");
} }

View file

@ -5,7 +5,7 @@ export default new NamedCommand({
description: "Shortens a given URL.", description: "Shortens a given URL.",
run: "Please provide a URL.", run: "Please provide a URL.",
any: new Command({ 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) { https.get("https://is.gd/create.php?format=simple&url=" + encodeURIComponent(args[0]), function (res) {
var body = ""; var body = "";
res.on("data", function (chunk) { res.on("data", function (chunk) {

View file

@ -1,9 +1,9 @@
import {Command, NamedCommand, RestCommand} from "../../core"; import {NamedCommand, RestCommand} from "../../core";
import {streamList} from "../../modules/streamNotifications"; import {streamList} from "../../modules/streamNotifications";
export default new NamedCommand({ export default new NamedCommand({
description: "Sets the description of your stream. You can embed links by writing `[some name](some link)`", 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; const userID = author.id;
if (streamList.has(userID)) { if (streamList.has(userID)) {
@ -22,7 +22,7 @@ export default new NamedCommand({
} }
}, },
any: new RestCommand({ any: new RestCommand({
async run({send, message, channel, guild, author, member, client, args, combined}) { async run({send, author, member, combined}) {
const userID = author.id; const userID = author.id;
if (streamList.has(userID)) { if (streamList.has(userID)) {

View file

@ -1,15 +1,6 @@
import { import {Command, NamedCommand, askForReply, confirm, askMultipleChoice, getMemberByName, RestCommand} from "../../core";
Command,
NamedCommand,
ask,
askYesOrNo,
askMultipleChoice,
prompt,
getMemberByName,
RestCommand
} from "../../core";
import {Storage} from "../../structures"; import {Storage} from "../../structures";
import {User, GuildMember} from "discord.js"; import {User} from "discord.js";
import moment from "moment"; import moment from "moment";
const DATE_FORMAT = "D MMMM YYYY"; const DATE_FORMAT = "D MMMM YYYY";
@ -178,34 +169,31 @@ function getTimeEmbed(user: User) {
export default new NamedCommand({ export default new NamedCommand({
description: "Show others what time it is for you.", description: "Show others what time it is for you.",
aliases: ["tz"], aliases: ["tz"],
async run({send, channel, author}) { async run({send, author}) {
send(getTimeEmbed(author)); send(getTimeEmbed(author));
}, },
subcommands: { subcommands: {
// Welcome to callback hell. We hope you enjoy your stay here! // Welcome to callback hell. We hope you enjoy your stay here!
setup: new NamedCommand({ setup: new NamedCommand({
description: "Registers your timezone information for the bot.", description: "Registers your timezone information for the bot.",
async run({send, author, channel}) { async run({send, author}) {
const profile = Storage.getUser(author.id); const profile = Storage.getUser(author.id);
profile.timezone = null; profile.timezone = null;
profile.daylightSavingsRegion = null; profile.daylightSavingsRegion = null;
let hour: number;
ask( // Parse and validate reply
const reply = await askForReply(
await 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!)*" "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, author.id,
(reply) => { 30000
hour = parseInt(reply); );
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");
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. // 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. // 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. // For example, the old logic was just (hour - hourUTC). When I setup my timezone (UTC-6) at 18:00, it was UTC 00:00.
@ -265,10 +253,8 @@ export default new NamedCommand({
// I calculate the list beforehand and check for duplicates to reduce unnecessary asking. // I calculate the list beforehand and check for duplicates to reduce unnecessary asking.
if (duplicates.includes(hour)) { if (duplicates.includes(hour)) {
const isSameDay = await askYesOrNo( const isSameDay = await confirm(
await send( await send(`Is the current day of the month the ${moment().utc().format("Do")} for you?`),
`Is the current day of the month the ${moment().utc().format("Do")} for you?`
),
author.id author.id
); );
@ -297,7 +283,7 @@ export default new NamedCommand({
} }
// I should note that error handling should be added sometime because await throws an exception on Promise.reject. // I should note that error handling should be added sometime because await throws an exception on Promise.reject.
const hasDST = await askYesOrNo( const hasDST = await confirm(
await send("Does your timezone change based on daylight savings?"), await send("Does your timezone change based on daylight savings?"),
author.id author.id
); );
@ -322,39 +308,45 @@ export default new NamedCommand({
finalize(); finalize();
}; };
askMultipleChoice(await send(DST_NOTE_SETUP), author.id, [ const index = await askMultipleChoice(await send(DST_NOTE_SETUP), author.id, 3);
() => finalizeDST("na"),
() => finalizeDST("eu"), switch (index) {
() => finalizeDST("sh") case 0:
]); finalizeDST("na");
break;
case 1:
finalizeDST("eu");
break;
case 2:
finalizeDST("sh");
break;
}
} else { } else {
finalize(); finalize();
} }
},
() => "you need to enter in a valid integer between 0 to 23" return;
);
} }
}), }),
delete: new NamedCommand({ delete: new NamedCommand({
description: "Delete your timezone information.", description: "Delete your timezone information.",
async run({send, channel, author}) { async run({send, author}) {
prompt( const result = await confirm(
await send( await send("Are you sure you want to delete your timezone information?"),
"Are you sure you want to delete your timezone information?\n*(This message will automatically be deleted after 10 seconds.)*" author.id
), );
author.id,
() => { if (result) {
const profile = Storage.getUser(author.id); const profile = Storage.getUser(author.id);
profile.timezone = null; profile.timezone = null;
profile.daylightSavingsRegion = null; profile.daylightSavingsRegion = null;
Storage.save(); Storage.save();
} }
);
} }
}), }),
utc: new NamedCommand({ utc: new NamedCommand({
description: "Displays UTC time.", description: "Displays UTC time.",
async run({send, channel}) { async run({send}) {
const time = moment().utc(); const time = moment().utc();
send({ send({
@ -386,15 +378,15 @@ export default new NamedCommand({
id: "user", id: "user",
user: new Command({ user: new Command({
description: "See what time it is for someone else.", description: "See what time it is for someone else.",
async run({send, channel, args}) { async run({send, args}) {
send(getTimeEmbed(args[0])); send(getTimeEmbed(args[0]));
} }
}), }),
any: new RestCommand({ any: new RestCommand({
description: "See what time it is for someone else (by their username).", 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); 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); else send(member);
} }
}) })

View file

@ -1,11 +1,11 @@
import {Command, NamedCommand, RestCommand} from "../../core"; import {NamedCommand, RestCommand} from "../../core";
import moment from "moment"; import moment from "moment";
import {Storage} from "../../structures"; import {Storage} from "../../structures";
import {MessageEmbed} from "discord.js"; import {MessageEmbed} from "discord.js";
export default new NamedCommand({ export default new NamedCommand({
description: "Keep and edit your personal todo list.", 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 user = Storage.getUser(author.id);
const embed = new MessageEmbed().setTitle(`Todo list for ${author.tag}`).setColor("BLUE"); const embed = new MessageEmbed().setTitle(`Todo list for ${author.tag}`).setColor("BLUE");
@ -23,7 +23,7 @@ export default new NamedCommand({
add: new NamedCommand({ add: new NamedCommand({
run: "You need to specify a note to add.", run: "You need to specify a note to add.",
any: new RestCommand({ 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); const user = Storage.getUser(author.id);
user.todoList[Date.now().toString()] = combined; user.todoList[Date.now().toString()] = combined;
console.debug(user.todoList); console.debug(user.todoList);
@ -35,7 +35,7 @@ export default new NamedCommand({
remove: new NamedCommand({ remove: new NamedCommand({
run: "You need to specify a note to remove.", run: "You need to specify a note to remove.",
any: new RestCommand({ 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); const user = Storage.getUser(author.id);
let isFound = false; let isFound = false;
@ -55,7 +55,7 @@ export default new NamedCommand({
}) })
}), }),
clear: 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); const user = Storage.getUser(author.id);
user.todoList = {}; user.todoList = {};
Storage.save(); Storage.save();

View file

@ -8,7 +8,7 @@ export default new NamedCommand({
any: new Command({ any: new Command({
run: "You need to enter some text to translate.", run: "You need to enter some text to translate.",
any: new RestCommand({ any: new RestCommand({
async run({send, message, channel, guild, author, member, client, args}) { async run({send, args}) {
const lang = args[0]; const lang = args[0];
const input = args.slice(1).join(" "); const input = args.slice(1).join(" ");
translate(input, { translate(input, {

View file

@ -11,7 +11,7 @@ import {
GuildChannel, GuildChannel,
Channel Channel
} from "discord.js"; } 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 {hasPermission, getPermissionLevel, getPermissionName} from "./permissions";
import {getPrefix} from "./interface"; import {getPrefix} from "./interface";
import {parseVars, requireAllCasesHandledFor} from "../lib"; 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. // 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. // 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. // 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. // 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. // 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( public async execute(args: string[], menu: CommandMenu, metadata: ExecuteCommandMetadata): Promise<string | null> {
args: string[],
menu: CommandMenu,
metadata: ExecuteCommandMetadata
): Promise<SingleMessageOptions | null> {
// Update inherited properties if the current command specifies a property. // 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. // 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.permission !== -1) metadata.permission = this.permission;
@ -292,9 +288,7 @@ export class Command extends BaseCommand {
const errorMessage = error.stack ?? error; const errorMessage = error.stack ?? error;
console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`); console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`);
return { return `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\``;
content: `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 id = patterns.channel.exec(param)![1];
const channel = await getChannelByID(id); const channel = await getChannelByID(id);
if (channel instanceof Channel) { if (typeof channel !== "string") {
if (channel instanceof TextChannel || channel instanceof DMChannel) { if (channel instanceof TextChannel || channel instanceof DMChannel) {
metadata.symbolicArgs.push("<channel>"); metadata.symbolicArgs.push("<channel>");
menu.args.push(channel); menu.args.push(channel);
return this.channel.execute(args, menu, metadata); return this.channel.execute(args, menu, metadata);
} else { } else {
return { return `\`${id}\` is not a valid text channel!`;
content: `\`${id}\` is not a valid text channel!`
};
} }
} else { } else {
return channel; return channel;
@ -330,9 +322,7 @@ export class Command extends BaseCommand {
const id = patterns.role.exec(param)![1]; const id = patterns.role.exec(param)![1];
if (!menu.guild) { if (!menu.guild) {
return { return "You can't use role parameters in DM channels!";
content: "You can't use role parameters in DM channels!"
};
} }
const role = menu.guild.roles.cache.get(id); const role = menu.guild.roles.cache.get(id);
@ -342,9 +332,7 @@ export class Command extends BaseCommand {
menu.args.push(role); menu.args.push(role);
return this.role.execute(args, menu, metadata); return this.role.execute(args, menu, metadata);
} else { } else {
return { return `\`${id}\` is not a valid role in this server!`;
content: `\`${id}\` is not a valid role in this server!`
};
} }
} else if (this.emote && patterns.emote.test(param)) { } else if (this.emote && patterns.emote.test(param)) {
const id = patterns.emote.exec(param)![1]; const id = patterns.emote.exec(param)![1];
@ -355,9 +343,7 @@ export class Command extends BaseCommand {
menu.args.push(emote); menu.args.push(emote);
return this.emote.execute(args, menu, metadata); return this.emote.execute(args, menu, metadata);
} else { } else {
return { return `\`${id}\` isn't a valid emote!`;
content: `\`${id}\` isn't a valid emote!`
};
} }
} else if (this.message && (isMessageLink || isMessagePair)) { } else if (this.message && (isMessageLink || isMessagePair)) {
let channelID = ""; let channelID = "";
@ -375,7 +361,7 @@ export class Command extends BaseCommand {
const message = await getMessageByID(channelID, messageID); const message = await getMessageByID(channelID, messageID);
if (message instanceof Message) { if (typeof message !== "string") {
metadata.symbolicArgs.push("<message>"); metadata.symbolicArgs.push("<message>");
menu.args.push(message); menu.args.push(message);
return this.message.execute(args, menu, metadata); return this.message.execute(args, menu, metadata);
@ -386,7 +372,7 @@ export class Command extends BaseCommand {
const id = patterns.user.exec(param)![1]; const id = patterns.user.exec(param)![1];
const user = await getUserByID(id); const user = await getUserByID(id);
if (user instanceof User) { if (typeof user !== "string") {
metadata.symbolicArgs.push("<user>"); metadata.symbolicArgs.push("<user>");
menu.args.push(user); menu.args.push(user);
return this.user.execute(args, menu, metadata); return this.user.execute(args, menu, metadata);
@ -403,24 +389,20 @@ export class Command extends BaseCommand {
case "channel": case "channel":
const channel = await getChannelByID(id); const channel = await getChannelByID(id);
if (channel instanceof Channel) { if (typeof channel !== "string") {
if (channel instanceof TextChannel || channel instanceof DMChannel) { if (channel instanceof TextChannel || channel instanceof DMChannel) {
metadata.symbolicArgs.push("<channel>"); metadata.symbolicArgs.push("<channel>");
menu.args.push(channel); menu.args.push(channel);
return this.id.execute(args, menu, metadata); return this.id.execute(args, menu, metadata);
} else { } else {
return { return `\`${id}\` is not a valid text channel!`;
content: `\`${id}\` is not a valid text channel!`
};
} }
} else { } else {
return channel; return channel;
} }
case "role": case "role":
if (!menu.guild) { if (!menu.guild) {
return { return "You can't use role parameters in DM channels!";
content: "You can't use role parameters in DM channels!"
};
} }
const role = menu.guild.roles.cache.get(id); const role = menu.guild.roles.cache.get(id);
@ -429,9 +411,7 @@ export class Command extends BaseCommand {
menu.args.push(role); menu.args.push(role);
return this.id.execute(args, menu, metadata); return this.id.execute(args, menu, metadata);
} else { } else {
return { return `\`${id}\` isn't a valid role in this server!`;
content: `\`${id}\` isn't a valid role in this server!`
};
} }
case "emote": case "emote":
const emote = menu.client.emojis.cache.get(id); const emote = menu.client.emojis.cache.get(id);
@ -440,14 +420,12 @@ export class Command extends BaseCommand {
menu.args.push(emote); menu.args.push(emote);
return this.id.execute(args, menu, metadata); return this.id.execute(args, menu, metadata);
} else { } else {
return { return `\`${id}\` isn't a valid emote!`;
content: `\`${id}\` isn't a valid emote!`
};
} }
case "message": case "message":
const message = await getMessageByID(menu.channel, id); const message = await getMessageByID(menu.channel, id);
if (message instanceof Message) { if (typeof message !== "string") {
menu.args.push(message); menu.args.push(message);
return this.id.execute(args, menu, metadata); return this.id.execute(args, menu, metadata);
} else { } else {
@ -456,7 +434,7 @@ export class Command extends BaseCommand {
case "user": case "user":
const user = await getUserByID(id); const user = await getUserByID(id);
if (user instanceof User) { if (typeof user !== "string") {
menu.args.push(user); menu.args.push(user);
return this.id.execute(args, menu, metadata); return this.id.execute(args, menu, metadata);
} else { } else {
@ -465,7 +443,7 @@ export class Command extends BaseCommand {
case "guild": case "guild":
const guild = getGuildByID(id); const guild = getGuildByID(id);
if (guild instanceof Guild) { if (typeof guild !== "string") {
menu.args.push(guild); menu.args.push(guild);
return this.id.execute(args, menu, metadata); return this.id.execute(args, menu, metadata);
} else { } else {
@ -489,11 +467,9 @@ export class Command extends BaseCommand {
return this.any.execute(args.join(" "), menu, metadata); return this.any.execute(args.join(" "), menu, metadata);
} else { } else {
metadata.symbolicArgs.push(`"${param}"`); metadata.symbolicArgs.push(`"${param}"`);
return { return `No valid command sequence matching \`${metadata.header} ${metadata.symbolicArgs.join(
content: `No valid command sequence matching \`${metadata.header} ${metadata.symbolicArgs.join(
" " " "
)}\` found.` )}\` found.`;
};
} }
// Note: Do NOT add a return statement here. In case one of the other sections is missing // 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, combined: string,
menu: CommandMenu, menu: CommandMenu,
metadata: ExecuteCommandMetadata metadata: ExecuteCommandMetadata
): Promise<SingleMessageOptions | null> { ): Promise<string | null> {
// Update inherited properties if the current command specifies a property. // 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. // 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.permission !== -1) metadata.permission = this.permission;
@ -716,9 +692,7 @@ export class RestCommand extends BaseCommand {
const errorMessage = error.stack ?? error; const errorMessage = error.stack ?? error;
console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`); console.error(`Command Error: ${metadata.header} (${metadata.args.join(", ")})\n${errorMessage}`);
return { return `There was an error while trying to execute that command!\`\`\`${errorMessage}\`\`\``;
content: `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. // 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. // 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? // 1. Does this command specify a required channel type? If so, does the channel type match?
if ( if (
metadata.channelType === CHANNEL_TYPE.GUILD && metadata.channelType === CHANNEL_TYPE.GUILD &&
(!(menu.channel instanceof GuildChannel) || menu.guild === null || menu.member === null) (!(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 ( } else if (
metadata.channelType === CHANNEL_TYPE.DM && metadata.channelType === CHANNEL_TYPE.DM &&
(menu.channel.type !== "dm" || menu.guild !== null || menu.member !== null) (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.) // 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) { 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? // 3. Does the user have permission to execute the command?
if (!hasPermission(menu.author, menu.member, metadata.permission)) { if (!hasPermission(menu.author, menu.member, metadata.permission)) {
const userPermLevel = getPermissionLevel(menu.author, menu.member); const userPermLevel = getPermissionLevel(menu.author, menu.member);
return { return `You don't have access to this command! Your permission level is \`${getPermissionName(
content: `You don't have access to this command! Your permission level is \`${getPermissionName(
userPermLevel userPermLevel
)}\` (${userPermLevel}), but this command requires a permission level of \`${getPermissionName( )}\` (${userPermLevel}), but this command requires a permission level of \`${getPermissionName(
metadata.permission metadata.permission
)}\` (${metadata.permission}).` )}\` (${metadata.permission}).`;
};
} }
return null; return null;

View file

@ -15,7 +15,9 @@ import {
MessageAdditions, MessageAdditions,
SplitOptions, SplitOptions,
APIMessage, APIMessage,
StringResolvable StringResolvable,
EmojiIdentifierResolvable,
MessageReaction
} from "discord.js"; } from "discord.js";
import {unreactEventListeners, replyEventListeners} from "./eventListeners"; import {unreactEventListeners, replyEventListeners} from "./eventListeners";
import {client} from "./interface"; import {client} from "./interface";
@ -31,19 +33,6 @@ export type SendFunction = ((
((content: StringResolvable, options: MessageOptions & {split: true | SplitOptions}) => Promise<Message[]>) & ((content: StringResolvable, options: MessageOptions & {split: true | SplitOptions}) => Promise<Message[]>) &
((content: StringResolvable, options: MessageOptions) => Promise<Message | Message[]>); ((content: StringResolvable, options: MessageOptions) => Promise<Message | Message[]>);
/**
* 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 FIVE_BACKWARDS_EMOJI = "⏪";
const BACKWARDS_EMOJI = "⬅️"; const BACKWARDS_EMOJI = "⬅️";
const FORWARDS_EMOJI = "➡️"; const FORWARDS_EMOJI = "➡️";
@ -56,34 +45,35 @@ const FIVE_FORWARDS_EMOJI = "⏩";
*/ */
export async function paginate( export async function paginate(
send: SendFunction, send: SendFunction,
senderID: string, onTurnPage: (page: number, hasMultiplePages: boolean) => SingleMessageOptions,
total: number, totalPages: number,
callback: (page: number, hasMultiplePages: boolean) => SingleMessageOptions, listenTo: string | null = null,
duration = 60000 duration = 60000
) { ): Promise<void> {
const hasMultiplePages = total > 1; const hasMultiplePages = totalPages > 1;
const message = await send(callback(0, hasMultiplePages)); const message = await send(onTurnPage(0, hasMultiplePages));
if (hasMultiplePages) { if (hasMultiplePages) {
let page = 0; let page = 0;
const turn = (amount: number) => { const turn = (amount: number) => {
page += amount; page += amount;
if (page >= total) { if (page >= totalPages) {
page %= total; page %= totalPages;
} else if (page < 0) { } 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. // 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; const flattened = Math.abs(page) % totalPages;
if (flattened !== 0) page = total - flattened; if (flattened !== 0) page = totalPages - flattened;
} }
message.edit(callback(page, true)); message.edit(onTurnPage(page, true));
}; };
const handle = (emote: string, reacterID: string) => { 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) { switch (emote) {
case FIVE_BACKWARDS_EMOJI: case FIVE_BACKWARDS_EMOJI:
if (total > 5) turn(-5); if (totalPages > 5) turn(-5);
break; break;
case BACKWARDS_EMOJI: case BACKWARDS_EMOJI:
turn(-1); turn(-1);
@ -92,28 +82,28 @@ export async function paginate(
turn(1); turn(1);
break; break;
case FIVE_FORWARDS_EMOJI: case FIVE_FORWARDS_EMOJI:
if (total > 5) turn(5); if (totalPages > 5) turn(5);
break; break;
} }
} }
}; };
// Listen for reactions and call the handler. // 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 backwardsReaction = await message.react(BACKWARDS_EMOJI);
let forwardsReaction = await message.react(FORWARDS_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); unreactEventListeners.set(message.id, handle);
const collector = message.createReactionCollector( const collector = message.createReactionCollector(
(reaction, user) => { (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. // 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. // 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); const canDeleteEmotes = botHasPermission(message.guild, Permissions.FLAGS.MANAGE_MESSAGES);
handle(reaction.emoji.name, user.id); handle(reaction.emoji.name, user.id);
if (canDeleteEmotes) reaction.users.remove(user); if (canDeleteEmotes) reaction.users.remove(user);
collector.resetTimer();
} }
return false; return false;
@ -134,22 +124,28 @@ export async function paginate(
} }
} }
// Waits for the sender to either confirm an action or let it pass (and delete the message). //export function generateMulti
// This should probably be renamed to "confirm" now that I think of it, "prompt" is better used elsewhere. // paginate after generateonetimeprompt
// 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;
message.react("✅"); // Returns null if timed out, otherwise, returns the value.
export function generateOneTimePrompt<T>(
message: Message,
stack: {[emote: string]: T},
listenTo: string | null = null,
duration = 60000
): Promise<T | null> {
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( await message.awaitReactions(
(reaction, user) => { (reaction: MessageReaction, user: User) => {
if (user.id === senderID) { if (user.id === listenTo || listenTo === null) {
if (reaction.emoji.name === "✅") { const emote = reaction.emoji.name;
onConfirm();
isDeleted = true; if (emote in stack) {
resolve(stack[emote]);
message.delete(); message.delete();
} }
} }
@ -163,69 +159,36 @@ export async function prompt(message: Message, senderID: string, onConfirm: () =
{time: duration} {time: duration}
); );
if (!isDeleted) message.delete(); if (!message.deleted) {
}
// 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,
senderID: string,
condition: (reply: string) => boolean,
onSuccess: () => void,
onReject: () => string,
timeout = 60000
) {
const referenceID = `${message.channel.id}-${message.id}`;
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<boolean> {
return new Promise(async (resolve, reject) => {
let isDeleted = false;
await message.react("✅");
message.react("❌");
await message.awaitReactions(
(reaction, user) => {
if (user.id === senderID) {
const isCheckReacted = reaction.emoji.name === "✅";
if (isCheckReacted || reaction.emoji.name === "❌") {
resolve(isCheckReacted);
isDeleted = true;
message.delete(); 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<void> {
for (const emote of emotes) {
try {
await message.react(emote);
} catch {
return;
}
} }
} }
return false; export function confirm(message: Message, senderID: string, timeout = 30000): Promise<boolean | null> {
return generateOneTimePrompt(
message,
{
"✅": true,
"❌": false
}, },
{time: timeout} senderID,
timeout
); );
if (!isDeleted) {
message.delete();
reject("Prompt timed out.");
}
});
} }
// This MUST be split into an array. These emojis are made up of several characters each, adding up to 29 in length. // This MUST be split into an array. These emojis are made up of several characters each, adding up to 29 in length.
@ -236,40 +199,47 @@ const multiNumbers = ["1⃣", "2⃣", "3⃣", "4⃣", "5⃣", "6
export async function askMultipleChoice( export async function askMultipleChoice(
message: Message, message: Message,
senderID: string, senderID: string,
callbackStack: (() => void)[], choices: number,
timeout = 90000 timeout = 90000
) { ): Promise<number | null> {
if (callbackStack.length > multiNumbers.length) { if (choices > multiNumbers.length)
message.channel.send( throw new Error(
`\`ERROR: The amount of callbacks in "askMultipleChoice" must not exceed the total amount of allowed options (${multiNumbers.length})!\`` `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<Message | null> {
return new Promise((resolve) => {
const referenceID = `${message.channel.id}-${message.id}`;
for (let i = 0; i < callbackStack.length; i++) { replyEventListeners.set(referenceID, (reply) => {
await message.react(multiNumbers[i]); if (reply.author.id === listenTo) {
}
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(); message.delete();
replyEventListeners.delete(referenceID);
resolve(reply);
} }
});
if (timeout) {
client.setTimeout(() => {
if (!message.deleted) message.delete();
replyEventListeners.delete(referenceID);
resolve(null);
}, timeout);
}
});
} }
return false; /**
}, * Tests if a bot has a certain permission in a specified guild.
{time: timeout} */
); export function botHasPermission(guild: Guild | null, permission: number): boolean {
return !!guild?.me?.hasPermission(permission);
if (!isDeleted) message.delete();
} }
// For "get x by y" methods: // 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. // 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). // 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); const guild = client.guilds.cache.get(id);
if (guild) { if (guild) {
if (guild.available) return 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 { } else {
return { return `No guild found by the ID of \`${id}\`!`;
content: `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 query = name.toLowerCase();
const guild = client.guilds.cache.find((guild) => guild.name.toLowerCase().includes(query)); const guild = client.guilds.cache.find((guild) => guild.name.toLowerCase().includes(query));
if (guild) { if (guild) {
if (guild.available) return 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 { } else {
return { return `No guild found by the name of \`${name}\`!`;
content: `No guild found by the name of \`${name}\`!`
};
} }
} }
export async function getChannelByID(id: string): Promise<Channel | SingleMessageOptions> { export async function getChannelByID(id: string): Promise<Channel | string> {
try { try {
return await client.channels.fetch(id); return await client.channels.fetch(id);
} catch { } 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. // 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 query = name.toLowerCase();
const channel = client.channels.cache.find( const channel = client.channels.cache.find(
(channel) => channel instanceof GuildChannel && channel.name.toLowerCase().includes(query) (channel) => channel instanceof GuildChannel && channel.name.toLowerCase().includes(query)
) as GuildChannel | undefined; ) as GuildChannel | undefined;
if (channel) return channel; 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( export async function getMessageByID(
channel: TextChannel | DMChannel | NewsChannel | string, channel: TextChannel | DMChannel | NewsChannel | string,
id: string id: string
): Promise<Message | SingleMessageOptions> { ): Promise<Message | string> {
if (typeof channel === "string") { if (typeof channel === "string") {
const targetChannel = await getChannelByID(channel); const targetChannel = await getChannelByID(channel);
if (targetChannel instanceof TextChannel || targetChannel instanceof DMChannel) channel = targetChannel; 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; else return targetChannel;
} }
try { try {
return await channel.messages.fetch(id); return await channel.messages.fetch(id);
} catch { } 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<User | SingleMessageOptions> { export async function getUserByID(id: string): Promise<User | string> {
try { try {
return await client.users.fetch(id); return await client.users.fetch(id);
} catch { } 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. // 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(); let query = name.toLowerCase();
const tagMatch = /^(.+?)#(\d{4})$/.exec(name); const tagMatch = /^(.+?)#(\d{4})$/.exec(name);
let tag: string | null = null; let tag: string | null = null;
@ -366,19 +332,19 @@ export function getUserByName(name: string): User | SingleMessageOptions {
}); });
if (user) return user; 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<GuildMember | SingleMessageOptions> { export async function getMemberByID(guild: Guild, id: string): Promise<GuildMember | string> {
try { try {
return await guild.members.fetch(id); return await guild.members.fetch(id);
} catch { } 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. // First checks if a member can be found by that nickname, then check if a member can be found by that username.
export async function getMemberByName(guild: Guild, name: string): Promise<GuildMember | SingleMessageOptions> { export async function getMemberByName(guild: Guild, name: string): Promise<GuildMember | string> {
const member = ( const member = (
await guild.members.fetch({ await guild.members.fetch({
query: name, query: name,
@ -395,9 +361,9 @@ export async function getMemberByName(guild: Guild, name: string): Promise<Guild
if (user instanceof User) { if (user instanceof User) {
const member = guild.members.resolve(user); const member = guild.members.resolve(user);
if (member) return member; if (member) return member;
else return {content: `The user \`${user.tag}\` isn't in this guild!`}; else return `The user \`${user.tag}\` isn't in this guild!`;
} else { } else {
return {content: `No member found by the name of \`${name}\`!`}; return `No member found by the name of \`${name}\`!`;
} }
} }
} }

View file

@ -1,5 +1,5 @@
import {client} from "../index"; import {client} from "../index";
import {Message, MessageEmbed} from "discord.js"; import {MessageEmbed} from "discord.js";
import {getPrefix} from "../structures"; import {getPrefix} from "../structures";
import {getMessageByID} from "../core"; import {getMessageByID} from "../core";
@ -13,7 +13,7 @@ client.on("message", async (message) => {
const linkMessage = await getMessageByID(channelID, messageID); const linkMessage = await getMessageByID(channelID, messageID);
// If it's an invalid link (or the bot doesn't have access to it). // 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!"); return message.channel.send("I don't have access to that channel!");
} }