Fully separated utility functions from command menu

This commit is contained in:
WatDuhHekBro 2021-03-30 05:25:07 -05:00
parent 10c1cd9cff
commit 51fa9457b4
37 changed files with 677 additions and 698 deletions

View File

@ -1,5 +1,5 @@
import Command from "../core/command"; import Command, {handler} from "../core/command";
import {CommonLibrary, botHasPermission, clean} from "../core/lib"; import {botHasPermission, clean} from "../core/libd";
import {Config, Storage} from "../core/structures"; import {Config, Storage} from "../core/structures";
import {PermissionNames, getPermissionLevel} from "../core/permissions"; import {PermissionNames, getPermissionLevel} from "../core/permissions";
import {Permissions} from "discord.js"; import {Permissions} from "discord.js";
@ -23,11 +23,11 @@ const statuses = ["online", "idle", "dnd", "invisible"];
export default new Command({ export default new Command({
description: description:
"An all-in-one command to do admin stuff. You need to be either an admin of the server or one of the bot's mechanics to use this command.", "An all-in-one command to do admin stuff. You need to be either an admin of the server or one of the bot's mechanics to use this command.",
async run($: CommonLibrary): Promise<any> { async run($) {
if (!$.member) if (!$.member) {
return $.channel.send( $.channel.send("Couldn't find a member object for you! Did you make sure you used this in a server?");
"Couldn't find a member object for you! Did you make sure you used this in a server?" return;
); }
const permLevel = getPermissionLevel($.member); const permLevel = getPermissionLevel($.member);
$.channel.send( $.channel.send(
`${$.author.toString()}, your permission level is \`${PermissionNames[permLevel]}\` (${permLevel}).` `${$.author.toString()}, your permission level is \`${PermissionNames[permLevel]}\` (${permLevel}).`
@ -42,7 +42,7 @@ export default new Command({
prefix: new Command({ prefix: new Command({
description: "Set a custom prefix for your guild. Removes your custom prefix if none is provided.", description: "Set a custom prefix for your guild. Removes your custom prefix if none is provided.",
usage: "(<prefix>)", usage: "(<prefix>)",
async run($: CommonLibrary): Promise<any> { async run($) {
Storage.getGuild($.guild?.id || "N/A").prefix = null; Storage.getGuild($.guild?.id || "N/A").prefix = null;
Storage.save(); Storage.save();
$.channel.send( $.channel.send(
@ -50,7 +50,7 @@ export default new Command({
); );
}, },
any: new Command({ any: new Command({
async run($: CommonLibrary): Promise<any> { async run($) {
Storage.getGuild($.guild?.id || "N/A").prefix = $.args[0]; Storage.getGuild($.guild?.id || "N/A").prefix = $.args[0];
Storage.save(); Storage.save();
$.channel.send(`The custom prefix for this guild is now \`${$.args[0]}\`.`); $.channel.send(`The custom prefix for this guild is now \`${$.args[0]}\`.`);
@ -62,12 +62,12 @@ export default new Command({
diag: new Command({ diag: new Command({
description: 'Requests a debug log with the "info" verbosity level.', description: 'Requests a debug log with the "info" verbosity level.',
permission: Command.PERMISSIONS.BOT_SUPPORT, permission: Command.PERMISSIONS.BOT_SUPPORT,
async run($: CommonLibrary): Promise<any> { async run($) {
$.channel.send(getLogBuffer("info")); $.channel.send(getLogBuffer("info"));
}, },
any: new Command({ any: new Command({
description: `Select a verbosity to listen to. Available levels: \`[${Object.keys(logs).join(", ")}]\``, description: `Select a verbosity to listen to. Available levels: \`[${Object.keys(logs).join(", ")}]\``,
async run($: CommonLibrary): Promise<any> { async run($) {
const type = $.args[0]; const type = $.args[0];
if (type in logs) $.channel.send(getLogBuffer(type)); if (type in logs) $.channel.send(getLogBuffer(type));
@ -83,14 +83,16 @@ export default new Command({
status: new Command({ status: new Command({
description: "Changes the bot's status.", description: "Changes the bot's status.",
permission: Command.PERMISSIONS.BOT_SUPPORT, permission: Command.PERMISSIONS.BOT_SUPPORT,
async run($: CommonLibrary): Promise<any> { async run($) {
$.channel.send("Setting status to `online`..."); $.channel.send("Setting status to `online`...");
}, },
any: new Command({ any: new Command({
description: `Select a status to set to. Available statuses: \`[${statuses.join(", ")}]\`.`, description: `Select a status to set to. Available statuses: \`[${statuses.join(", ")}]\`.`,
async run($: CommonLibrary): Promise<any> { async run($) {
if (!statuses.includes($.args[0])) return $.channel.send("That status doesn't exist!"); if (!statuses.includes($.args[0])) {
else { $.channel.send("That status doesn't exist!");
return;
} else {
$.client.user?.setStatus($.args[0]); $.client.user?.setStatus($.args[0]);
$.channel.send(`Setting status to \`${$.args[0]}\`...`); $.channel.send(`Setting status to \`${$.args[0]}\`...`);
} }
@ -100,7 +102,7 @@ export default new Command({
purge: new Command({ purge: new Command({
description: "Purges bot messages.", description: "Purges bot messages.",
permission: Command.PERMISSIONS.BOT_SUPPORT, permission: Command.PERMISSIONS.BOT_SUPPORT,
async run($: CommonLibrary): Promise<any> { async run($) {
if ($.message.channel instanceof discord.DMChannel) { if ($.message.channel instanceof discord.DMChannel) {
return; return;
} }
@ -124,7 +126,7 @@ export default new Command({
run: "A number was not provided.", run: "A number was not provided.",
number: new Command({ number: new Command({
description: "Amount of messages to delete.", description: "Amount of messages to delete.",
async run($: CommonLibrary): Promise<any> { async run($) {
if ($.channel.type === "dm") { if ($.channel.type === "dm") {
await $.channel.send("Can't clear messages in the DMs!"); await $.channel.send("Can't clear messages in the DMs!");
return; return;
@ -142,7 +144,7 @@ export default new Command({
usage: "<code>", usage: "<code>",
permission: Command.PERMISSIONS.BOT_OWNER, permission: Command.PERMISSIONS.BOT_OWNER,
// You have to bring everything into scope to use them. AFAIK, there isn't a more maintainable way to do this, but at least TS will let you know if anything gets removed. // You have to bring everything into scope to use them. AFAIK, there isn't a more maintainable way to do this, but at least TS will let you know if anything gets removed.
async run({args, author, channel, client, guild, member, message}): Promise<any> { async run({args, author, channel, client, guild, member, message}) {
try { try {
const code = args.join(" "); const code = args.join(" ");
let evaled = eval(code); let evaled = eval(code);
@ -157,18 +159,18 @@ export default new Command({
nick: new Command({ nick: new Command({
description: "Change the bot's nickname.", description: "Change the bot's nickname.",
permission: Command.PERMISSIONS.BOT_SUPPORT, permission: Command.PERMISSIONS.BOT_SUPPORT,
async run($: CommonLibrary): Promise<any> { async run($) {
const nickName = $.args.join(" "); const nickName = $.args.join(" ");
await $.guild?.me?.setNickname(nickName); await $.guild?.me?.setNickname(nickName);
if (botHasPermission($.guild, Permissions.FLAGS.MANAGE_MESSAGES)) if (botHasPermission($.guild, Permissions.FLAGS.MANAGE_MESSAGES))
$.message.delete({timeout: 5000}).catch($.handler.bind($)); $.message.delete({timeout: 5000}).catch(handler.bind($));
$.channel.send(`Nickname set to \`${nickName}\``).then((m) => m.delete({timeout: 5000})); $.channel.send(`Nickname set to \`${nickName}\``).then((m) => m.delete({timeout: 5000}));
} }
}), }),
guilds: new Command({ guilds: new Command({
description: "Shows a list of all guilds the bot is a member of.", description: "Shows a list of all guilds the bot is a member of.",
permission: Command.PERMISSIONS.BOT_SUPPORT, permission: Command.PERMISSIONS.BOT_SUPPORT,
async run($: CommonLibrary): Promise<any> { async run($) {
const guildList = $.client.guilds.cache.array().map((e) => e.name); const guildList = $.client.guilds.cache.array().map((e) => e.name);
$.channel.send(guildList); $.channel.send(guildList);
} }
@ -177,7 +179,7 @@ export default new Command({
description: "Set the activity of the bot.", description: "Set the activity of the bot.",
permission: Command.PERMISSIONS.BOT_SUPPORT, permission: Command.PERMISSIONS.BOT_SUPPORT,
usage: "<type> <string>", usage: "<type> <string>",
async run($: CommonLibrary): Promise<any> { async run($) {
$.client.user?.setActivity(".help", { $.client.user?.setActivity(".help", {
type: "LISTENING" type: "LISTENING"
}); });
@ -185,7 +187,7 @@ export default new Command({
}, },
any: new Command({ any: new Command({
description: `Select an activity type to set. Available levels: \`[${activities.join(", ")}]\``, description: `Select an activity type to set. Available levels: \`[${activities.join(", ")}]\``,
async run($: CommonLibrary): Promise<any> { async run($) {
const type = $.args[0]; const type = $.args[0];
if (activities.includes(type)) { if (activities.includes(type)) {

View File

@ -1,5 +1,5 @@
import Command from "../../core/command"; import Command from "../../core/command";
import {CommonLibrary} from "../../core/lib"; import {random} from "../../core/lib";
const responses = [ const responses = [
"Most likely,", "Most likely,",
@ -31,9 +31,9 @@ export default new Command({
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($: CommonLibrary): Promise<any> { async run($) {
const sender = $.message.author; const sender = $.message.author;
$.channel.send($(responses).random() + ` <@${sender.id}>`); $.channel.send(`${random(responses)} <@${sender.id}>`);
} }
}) })
}); });

View File

@ -1,18 +1,17 @@
import Command from "../../core/command"; import Command from "../../core/command";
import {CommonLibrary} from "../../core/lib";
export default new Command({ export default new Command({
description: "Gives specified user a cookie.", description: "Gives specified user a cookie.",
usage: "['all'/@user]", usage: "['all'/@user]",
run: ":cookie: Here's a cookie!", run: ":cookie: Here's a cookie!",
any: new Command({ any: new Command({
async run($: CommonLibrary): Promise<any> { async run($) {
if ($.args[0] == "all") return $.channel.send(`${$.author} gave everybody a cookie!`); if ($.args[0] == "all") $.channel.send(`${$.author} gave everybody a cookie!`);
} }
}), }),
user: new Command({ user: new Command({
description: "User to give cookie to.", description: "User to give cookie to.",
async run($: CommonLibrary): Promise<any> { async run($) {
const sender = $.author; const sender = $.author;
const mention = $.message.mentions.users.first(); const mention = $.message.mentions.users.first();
@ -41,7 +40,10 @@ export default new Command({
`bakes <@${mention.id}> fresh cookies, it smells amazing.` `bakes <@${mention.id}> fresh cookies, it smells amazing.`
]; ];
if (mention.id == sender.id) return $.channel.send("You can't give yourself cookies!"); if (mention.id == sender.id) {
$.channel.send("You can't give yourself cookies!");
return;
}
$.channel.send(`:cookie: <@${sender.id}> ` + cookies[Math.floor(Math.random() * cookies.length)]); $.channel.send(`:cookie: <@${sender.id}> ` + cookies[Math.floor(Math.random() * cookies.length)]);
} }

View File

@ -3,6 +3,7 @@ import {isAuthorized, getMoneyEmbed} from "./subcommands/eco-utils";
import {DailyCommand, PayCommand, GuildCommand, LeaderboardCommand} from "./subcommands/eco-core"; import {DailyCommand, PayCommand, GuildCommand, LeaderboardCommand} from "./subcommands/eco-core";
import {BuyCommand, ShopCommand} from "./subcommands/eco-shop"; import {BuyCommand, ShopCommand} from "./subcommands/eco-shop";
import {MondayCommand} from "./subcommands/eco-extras"; import {MondayCommand} from "./subcommands/eco-extras";
import {callMemberByUsername} from "../../core/libd";
export default new Command({ export default new Command({
description: "Economy command for Monika.", description: "Economy command for Monika.",
@ -26,7 +27,7 @@ export default new Command({
}), }),
any: new Command({ any: new Command({
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({guild, channel, args, callMemberByUsername, message}) { async run({guild, channel, args, message}) {
if (isAuthorized(guild, channel)) if (isAuthorized(guild, channel))
callMemberByUsername(message, args.join(" "), (member) => { callMemberByUsername(message, args.join(" "), (member) => {
channel.send(getMoneyEmbed(member.user)); channel.send(getMoneyEmbed(member.user));

View File

@ -2,13 +2,13 @@
import {URL} from "url"; import {URL} from "url";
import FileManager from "../../core/storage"; import FileManager from "../../core/storage";
import Command from "../../core/command"; import Command from "../../core/command";
import {CommonLibrary, getContent} from "../../core/lib"; import {getContent} from "../../core/libd";
const endpoints = FileManager.read("endpoints"); const endpoints = FileManager.read("endpoints");
export default new Command({ export default new Command({
description: "Provides you with a random image with the selected argument.", description: "Provides you with a random image with the selected argument.",
async run($: CommonLibrary): Promise<any> { async run($) {
console.log(endpoints.sfw); console.log(endpoints.sfw);
$.channel.send( $.channel.send(
`Please provide an image type. Available arguments:\n\`[${Object.keys(endpoints.sfw).join(", ")}]\`.` `Please provide an image type. Available arguments:\n\`[${Object.keys(endpoints.sfw).join(", ")}]\`.`
@ -16,7 +16,7 @@ export default new Command({
}, },
any: new Command({ any: new Command({
description: "Image type to send.", description: "Image type to send.",
async run($: CommonLibrary): Promise<any> { async run($) {
if (!($.args[0] in endpoints.sfw)) return $.channel.send("Couldn't find that endpoint!"); if (!($.args[0] in endpoints.sfw)) return $.channel.send("Couldn't find that endpoint!");
let baseURL = "https://nekos.life/api/v2"; let baseURL = "https://nekos.life/api/v2";

View File

@ -1,9 +1,8 @@
import Command from "../../core/command"; import Command from "../../core/command";
import {CommonLibrary} from "../../core/lib";
export default new Command({ export default new Command({
description: "Sends random ok message.", description: "Sends random ok message.",
async run($: CommonLibrary): Promise<any> { async run($) {
const responses = [ const responses = [
"boomer", "boomer",
"zoomer", "zoomer",

View File

@ -1,10 +1,10 @@
/// @ts-nocheck /// @ts-nocheck
import Command from "../../core/command"; import Command from "../../core/command";
import {CommonLibrary, getContent} from "../../core/lib"; import {getContent} from "../../core/libd";
export default new Command({ export default new Command({
description: "OwO-ifies the input.", description: "OwO-ifies the input.",
async run($: CommonLibrary): Promise<any> { async run($) {
let url = new URL(`https://nekos.life/api/v2/owoify?text=${$.args.join(" ")}`); let url = new URL(`https://nekos.life/api/v2/owoify?text=${$.args.join(" ")}`);
const content = await getContent(url.toString()); const content = await getContent(url.toString());
$.channel.send(content.owo); $.channel.send(content.owo);

View File

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

View File

@ -1,5 +1,6 @@
import Command from "../../../core/command"; import Command from "../../../core/command";
import $ from "../../../core/lib"; import {prompt} from "../../../core/libd";
import {pluralise} from "../../../core/lib";
import {Storage} from "../../../core/structures"; import {Storage} from "../../../core/structures";
import {isAuthorized, getMoneyEmbed, getSendEmbed, ECO_EMBED_COLOR} from "./eco-utils"; import {isAuthorized, getMoneyEmbed, getSendEmbed, ECO_EMBED_COLOR} from "./eco-utils";
@ -90,7 +91,7 @@ export const LeaderboardCommand = new Command({
fields.push({ fields.push({
name: `#${i + 1}. ${user.username}#${user.discriminator}`, name: `#${i + 1}. ${user.username}#${user.discriminator}`,
value: $(users[id].money).pluralise("Mon", "s") value: pluralise(users[id].money, "Mon", "s")
}); });
} }
@ -141,7 +142,7 @@ export const PayCommand = new Command({
run: "You must use the format `eco pay <user> <amount>`!" run: "You must use the format `eco pay <user> <amount>`!"
}), }),
any: new Command({ any: new Command({
async run({args, author, channel, guild, prompt}) { async run({args, author, channel, guild}) {
if (isAuthorized(guild, channel)) { if (isAuthorized(guild, channel)) {
const last = args.pop(); const last = args.pop();
@ -177,7 +178,8 @@ export const PayCommand = new Command({
return prompt( return prompt(
await channel.send( await channel.send(
`Are you sure you want to send ${$(amount).pluralise( `Are you sure you want to send ${pluralise(
amount,
"Mon", "Mon",
"s" "s"
)} to this person?\n*(This message will automatically be deleted after 10 seconds.)*`, )} to this person?\n*(This message will automatically be deleted after 10 seconds.)*`,

View File

@ -1,5 +1,5 @@
import {Message} from "discord.js"; import {Message} from "discord.js";
import $ from "../../../core/lib"; import {random} from "../../../core/lib";
export interface ShopItem { export interface ShopItem {
cost: number; cost: number;
@ -43,7 +43,7 @@ export const ShopItems: ShopItem[] = [
description: "Buys what is technically a laser bridge.", description: "Buys what is technically a laser bridge.",
usage: "laser bridge", usage: "laser bridge",
run(message) { run(message) {
message.channel.send($(lines).random(), { message.channel.send(random(lines), {
files: [ files: [
{ {
attachment: attachment:

View File

@ -1,5 +1,6 @@
import Command from "../../../core/command"; import Command from "../../../core/command";
import $ from "../../../core/lib"; import {pluralise, split} from "../../../core/lib";
import {paginate} from "../../../core/libd";
import {Storage, getPrefix} from "../../../core/structures"; import {Storage, getPrefix} from "../../../core/structures";
import {isAuthorized, ECO_EMBED_COLOR} from "./eco-utils"; import {isAuthorized, ECO_EMBED_COLOR} from "./eco-utils";
import {ShopItems, ShopItem} from "./eco-shop-items"; import {ShopItems, ShopItem} from "./eco-shop-items";
@ -15,7 +16,7 @@ export const ShopCommand = new Command({
for (const item of selection) for (const item of selection)
fields.push({ fields.push({
name: `**${item.title}** (${getPrefix(guild)}eco buy ${item.usage})`, name: `**${item.title}** (${getPrefix(guild)}eco buy ${item.usage})`,
value: `${item.description} Costs ${$(item.cost).pluralise("Mon", "s")}.`, value: `${item.description} Costs ${pluralise(item.cost, "Mon", "s")}.`,
inline: false inline: false
}); });
@ -34,11 +35,11 @@ export const ShopCommand = new Command({
// In case there's just one page, omit unnecessary details. // In case there's just one page, omit unnecessary details.
if (ShopItems.length <= 5) channel.send(getShopEmbed(ShopItems)); if (ShopItems.length <= 5) channel.send(getShopEmbed(ShopItems));
else { else {
const shopPages = $(ShopItems).split(5); const shopPages = split(ShopItems, 5);
const pageAmount = shopPages.length; const pageAmount = shopPages.length;
const msg = await channel.send(getShopEmbed(shopPages[0], `Shop (Page 1 of ${pageAmount})`)); const msg = await channel.send(getShopEmbed(shopPages[0], `Shop (Page 1 of ${pageAmount})`));
$.paginate(msg, author.id, pageAmount, (page) => { paginate(msg, author.id, pageAmount, (page) => {
msg.edit(getShopEmbed(shopPages[page], `Shop (Page ${page + 1} of ${pageAmount})`)); msg.edit(getShopEmbed(shopPages[page], `Shop (Page ${page + 1} of ${pageAmount})`));
}); });
} }

View File

@ -1,4 +1,4 @@
import $ from "../../../core/lib"; import {pluralise} from "../../../core/lib";
import {Storage} from "../../../core/structures"; import {Storage} from "../../../core/structures";
import {User, Guild, TextChannel, DMChannel, NewsChannel} from "discord.js"; import {User, Guild, TextChannel, DMChannel, NewsChannel} from "discord.js";
@ -20,7 +20,7 @@ export function getMoneyEmbed(user: User): object {
fields: [ fields: [
{ {
name: "Balance", name: "Balance",
value: $(profile.money).pluralise("Mon", "s") value: pluralise(profile.money, "Mon", "s")
} }
] ]
} }
@ -39,15 +39,15 @@ export function getSendEmbed(sender: User, receiver: User, amount: number): obje
}) })
}, },
title: "Transaction", title: "Transaction",
description: `${sender.toString()} has sent ${$(amount).pluralise("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.username}#${sender.discriminator}`,
value: $(Storage.getUser(sender.id).money).pluralise("Mon", "s") value: pluralise(Storage.getUser(sender.id).money, "Mon", "s")
}, },
{ {
name: `Receiver: ${receiver.username}#${receiver.discriminator}`, name: `Receiver: ${receiver.username}#${receiver.discriminator}`,
value: $(Storage.getUser(receiver.id).money).pluralise("Mon", "s") value: pluralise(Storage.getUser(receiver.id).money, "Mon", "s")
} }
], ],
footer: { footer: {

View File

@ -1,5 +1,5 @@
import Command from "../core/command"; import Command from "../core/command";
import {CommonLibrary} from "../core/lib"; import {toTitleCase} from "../core/lib";
import {loadableCommands, categories} from "../core/command"; import {loadableCommands, categories} from "../core/command";
import {PermissionNames} from "../core/permissions"; import {PermissionNames} from "../core/permissions";
@ -7,12 +7,12 @@ export default new Command({
description: "Lists all commands. If a command is specified, their arguments are listed as well.", description: "Lists all commands. If a command is specified, their arguments are listed as well.",
usage: "([command, [subcommand/type], ...])", usage: "([command, [subcommand/type], ...])",
aliases: ["h"], aliases: ["h"],
async run($: CommonLibrary): Promise<any> { async run($) {
const commands = await loadableCommands; const commands = await loadableCommands;
let output = `Legend: \`<type>\`, \`[list/of/stuff]\`, \`(optional)\`, \`(<optional type>)\`, \`([optional/list/...])\``; let output = `Legend: \`<type>\`, \`[list/of/stuff]\`, \`(optional)\`, \`(<optional type>)\`, \`([optional/list/...])\``;
for (const [category, headers] of categories) { for (const [category, headers] of categories) {
output += `\n\n===[ ${$(category).toTitleCase()} ]===`; output += `\n\n===[ ${toTitleCase(category)} ]===`;
for (const header of headers) { for (const header of headers) {
if (header !== "test") { if (header !== "test") {
@ -31,12 +31,15 @@ export default new Command({
$.channel.send(output, {split: true}); $.channel.send(output, {split: true});
}, },
any: new Command({ any: new Command({
async run($: CommonLibrary): Promise<any> { async run($) {
const commands = await loadableCommands; const commands = await loadableCommands;
let header = $.args.shift() as string; let header = $.args.shift() as string;
let command = commands.get(header); let command = commands.get(header);
if (!command || header === "test") return $.channel.send(`No command found by the name \`${header}\`!`); if (!command || header === "test") {
$.channel.send(`No command found by the name \`${header}\`!`);
return;
}
if (command.originalCommandName) header = command.originalCommandName; if (command.originalCommandName) header = command.originalCommandName;
else console.warn(`originalCommandName isn't defined for ${header}?!`); else console.warn(`originalCommandName isn't defined for ${header}?!`);
@ -53,7 +56,7 @@ export default new Command({
console.warn( console.warn(
`Command "${header}" is somehow in multiple categories. This means that the command loading stage probably failed in properly adding categories.` `Command "${header}" is somehow in multiple categories. This means that the command loading stage probably failed in properly adding categories.`
); );
else selectedCategory = $(category).toTitleCase(); else selectedCategory = toTitleCase(category);
} }
} }
@ -86,7 +89,10 @@ export default new Command({
} }
} }
if (invalid) return $.channel.send(`No command found by the name \`${header}\`!`); if (invalid) {
$.channel.send(`No command found by the name \`${header}\`!`);
return;
}
let append = ""; let append = "";

View File

@ -2,7 +2,7 @@ import {MessageEmbed, version as djsversion} from "discord.js";
import ms from "ms"; import ms from "ms";
import os from "os"; import os from "os";
import Command from "../core/command"; import Command from "../core/command";
import {CommonLibrary, formatBytes, trimArray} from "../core/lib"; import {formatBytes, trimArray} from "../core/libd";
import {verificationLevels, filterLevels, regions, flags} from "../defs/info"; import {verificationLevels, filterLevels, regions, flags} from "../defs/info";
import moment from "moment"; import moment from "moment";
import utc from "moment"; import utc from "moment";
@ -17,12 +17,12 @@ export default new Command({
avatar: new Command({ avatar: new Command({
description: "Shows your own, or another user's avatar.", description: "Shows your own, or another user's avatar.",
usage: "(<user>)", usage: "(<user>)",
async run($: CommonLibrary): Promise<any> { async run($) {
$.channel.send($.author.displayAvatarURL({dynamic: true, size: 2048})); $.channel.send($.author.displayAvatarURL({dynamic: true, size: 2048}));
}, },
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($: CommonLibrary): Promise<any> { async run($) {
$.channel.send( $.channel.send(
$.args[0].displayAvatarURL({ $.args[0].displayAvatarURL({
dynamic: true, dynamic: true,
@ -34,7 +34,7 @@ export default new Command({
}), }),
bot: new Command({ bot: new Command({
description: "Displays info about the bot.", description: "Displays info about the bot.",
async run($: CommonLibrary): Promise<any> { async run($) {
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")
@ -76,7 +76,7 @@ export default new Command({
guild: new Command({ guild: new Command({
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>)",
async run($: CommonLibrary): Promise<any> { async run($) {
if ($.guild) { if ($.guild) {
$.channel.send(await getGuildInfo($.guild, $.guild)); $.channel.send(await getGuildInfo($.guild, $.guild));
} else { } else {
@ -85,7 +85,7 @@ export default new Command({
}, },
any: new Command({ any: new Command({
description: "Display info about a guild by finding its name or ID.", description: "Display info about a guild by finding its name or ID.",
async run($: CommonLibrary): Promise<any> { async run($) {
// If a guild ID is provided (avoid the "number" subcommand because of inaccuracies), search for that guild // If a guild ID is provided (avoid the "number" subcommand because of inaccuracies), search for that guild
if ($.args.length === 1 && /^\d{17,19}$/.test($.args[0])) { if ($.args.length === 1 && /^\d{17,19}$/.test($.args[0])) {
const id = $.args[0]; const id = $.args[0];
@ -112,14 +112,16 @@ export default new Command({
}, },
user: new Command({ user: new Command({
description: "Displays info about mentioned user.", description: "Displays info about mentioned user.",
async run($: CommonLibrary): Promise<any> { async run($) {
// 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 = await $.guild?.members.fetch($.args[0]); const member = await $.guild?.members.fetch($.args[0]);
if (!member) if (!member) {
return $.channel.send( $.channel.send(
"No member object was found by that user! Are you sure you used this command in a server?" "No member object was found by that user! Are you sure you used this command in a server?"
); );
return;
}
const roles = member.roles.cache const roles = member.roles.cache
.sort((a: {position: number}, b: {position: number}) => b.position - a.position) .sort((a: {position: number}, b: {position: number}) => b.position - a.position)

View File

@ -1,5 +1,5 @@
import Command from "../core/command"; import Command, {handler} from "../core/command";
import {CommonLibrary} from "../core/lib"; import {pluralise} from "../core/lib";
import moment from "moment"; import moment from "moment";
import {Collection, TextChannel} from "discord.js"; import {Collection, TextChannel} from "discord.js";
@ -8,8 +8,11 @@ const lastUsedTimestamps: {[id: string]: number} = {};
export default new Command({ export default new Command({
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.",
async run($: CommonLibrary): Promise<any> { async run($) {
if (!$.guild) return $.channel.send(`You must use this command on a server!`); if (!$.guild) {
$.channel.send(`You must use this command on a server!`);
return;
}
// 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();
@ -19,11 +22,12 @@ export default new Command({
const howLong = moment(startTime).to(lastUsedTimestamp + cooldown); const howLong = moment(startTime).to(lastUsedTimestamp + cooldown);
// If it's been less than an hour since the command was last used, prevent it from executing. // If it's been less than an hour since the command was last used, prevent it from executing.
if (difference < cooldown) if (difference < cooldown) {
return $.channel.send( $.channel.send(
`This command requires a day to cooldown. You'll be able to activate this command ${howLong}.` `This command requires a day to cooldown. You'll be able to activate this command ${howLong}.`
); );
else lastUsedTimestamps[$.guild.id] = startTime; return;
} else lastUsedTimestamps[$.guild.id] = startTime;
const stats: { const stats: {
[id: string]: { [id: string]: {
@ -155,7 +159,8 @@ export default new Command({
const finishTime = Date.now(); const finishTime = Date.now();
clearInterval(interval); clearInterval(interval);
statusMessage.edit( statusMessage.edit(
`Finished operation in ${moment.duration(finishTime - startTime).humanize()} with ${$(warnings).pluralise( `Finished operation in ${moment.duration(finishTime - startTime).humanize()} with ${pluralise(
warnings,
"inconsistenc", "inconsistenc",
"ies", "ies",
"y" "y"
@ -181,6 +186,6 @@ export default new Command({
); );
} }
$.channel.send(lines, {split: true}).catch($.handler.bind($)); $.channel.send(lines, {split: true}).catch(handler.bind($));
} }
}); });

View File

@ -1,5 +1,4 @@
import Command from "../core/command"; import Command from "../core/command";
import {CommonLibrary} from "../core/lib";
export default new Command({ export default new Command({
description: description:
@ -8,7 +7,7 @@ export default new Command({
usage: "", usage: "",
permission: null, permission: null,
aliases: [], aliases: [],
async run($: CommonLibrary): Promise<any> { async run($) {
// code // code
}, },
subcommands: { subcommands: {
@ -19,7 +18,7 @@ export default new Command({
usage: "", usage: "",
permission: null, permission: null,
aliases: [], aliases: [],
async run($: CommonLibrary): Promise<any> { async run($) {
// code // code
} }
}) })
@ -30,7 +29,7 @@ export default new Command({
endpoint: false, endpoint: false,
usage: "", usage: "",
permission: null, permission: null,
async run($: CommonLibrary): Promise<any> { async run($) {
// code // code
} }
}), }),
@ -40,7 +39,7 @@ export default new Command({
endpoint: false, endpoint: false,
usage: "", usage: "",
permission: null, permission: null,
async run($: CommonLibrary): Promise<any> { async run($) {
// code // code
} }
}), }),
@ -50,7 +49,7 @@ export default new Command({
endpoint: false, endpoint: false,
usage: "", usage: "",
permission: null, permission: null,
async run($: CommonLibrary): Promise<any> { async run($) {
// code // code
} }
}) })

View File

@ -1,18 +1,25 @@
import Command from "../../core/command"; import Command from "../../core/command";
import {CommonLibrary} from "../../core/lib";
export default new Command({ export default new Command({
description: "Renames current voice channel.", description: "Renames current voice channel.",
usage: "<name>", usage: "<name>",
async run($: CommonLibrary): Promise<any> { async run($) {
const voiceChannel = $.message.member?.voice.channel; const voiceChannel = $.message.member?.voice.channel;
if (!voiceChannel) return $.channel.send("You are not in a voice channel."); if (!voiceChannel) {
$.channel.send("You are not in a voice channel.");
return;
}
if (!voiceChannel.guild.me?.hasPermission("MANAGE_CHANNELS")) if (!voiceChannel.guild.me?.hasPermission("MANAGE_CHANNELS")) {
return $.channel.send("I am lacking the required permissions to perform this action."); $.channel.send("I am lacking the required permissions to perform this action.");
return;
}
if ($.args.length === 0) return $.channel.send("Please provide a new voice channel name."); if ($.args.length === 0) {
$.channel.send("Please provide a new voice channel name.");
return;
}
const prevName = voiceChannel.name; const prevName = voiceChannel.name;
const newName = $.args.join(" "); const newName = $.args.join(" ");

View File

@ -1,6 +1,6 @@
import Command from "../../core/command"; import Command from "../../core/command";
import {queryClosestEmoteByName} from "./subcommands/emote-utils"; import {queryClosestEmoteByName} from "./subcommands/emote-utils";
import {botHasPermission} from "../../core/lib"; import {botHasPermission} from "../../core/libd";
import {Permissions} from "discord.js"; import {Permissions} from "discord.js";
export default new Command({ export default new Command({

View File

@ -1,26 +1,35 @@
import {GuildEmoji} from "discord.js"; import {GuildEmoji} from "discord.js";
import {MessageEmbed} from "discord.js"; import {MessageEmbed} from "discord.js";
import Command from "../../core/command"; import Command from "../../core/command";
import {CommonLibrary} from "../../core/lib"; import {split} from "../../core/lib";
import {paginate} from "../../core/libd";
import vm from "vm"; import vm from "vm";
import {TextChannel} from "discord.js";
import {DMChannel} from "discord.js";
import {NewsChannel} from "discord.js";
import {User} from "discord.js";
const REGEX_TIMEOUT_MS = 1000; const REGEX_TIMEOUT_MS = 1000;
export default new Command({ export default new Command({
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($: CommonLibrary): Promise<any> { async run($) {
displayEmoteList($, $.client.emojis.cache.array()); displayEmoteList($.client.emojis.cache.array(), $.channel, $.author);
}, },
any: new Command({ any: new Command({
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($: CommonLibrary): Promise<any> { async run($) {
// 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,19}$/.test($.args[0])) { if ($.args.length === 1 && /^\d{17,19}$/.test($.args[0])) {
const guildID: string = $.args[0]; const guildID: string = $.args[0];
displayEmoteList($, $.client.emojis.cache.filter((emote) => emote.guild.id === guildID).array()); displayEmoteList(
$.client.emojis.cache.filter((emote) => emote.guild.id === guildID).array(),
$.channel,
$.author
);
} else { } else {
// Otherwise, search via a regex pattern // Otherwise, search via a regex pattern
let flags: string | undefined = undefined; let flags: string | undefined = undefined;
@ -55,7 +64,7 @@ export default new Command({
script.runInContext(context, {timeout: REGEX_TIMEOUT_MS}); script.runInContext(context, {timeout: REGEX_TIMEOUT_MS});
emotes = sandbox.emotes; emotes = sandbox.emotes;
emoteCollection = emoteCollection.filter((emote) => emotes.has(emote.id)); // Only allow emotes that haven't been deleted. emoteCollection = emoteCollection.filter((emote) => emotes.has(emote.id)); // Only allow emotes that haven't been deleted.
displayEmoteList($, emoteCollection); displayEmoteList(emoteCollection, $.channel, $.author);
} catch (error) { } catch (error) {
if (error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") { if (error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") {
$.channel.send( $.channel.send(
@ -73,7 +82,7 @@ export default new Command({
}) })
}); });
async function displayEmoteList($: CommonLibrary, emotes: GuildEmoji[]) { async function displayEmoteList(emotes: GuildEmoji[], channel: TextChannel | DMChannel | NewsChannel, author: User) {
emotes.sort((a, b) => { emotes.sort((a, b) => {
const first = a.name.toLowerCase(); const first = a.name.toLowerCase();
const second = b.name.toLowerCase(); const second = b.name.toLowerCase();
@ -82,7 +91,7 @@ async function displayEmoteList($: CommonLibrary, emotes: GuildEmoji[]) {
else if (first < second) return -1; else if (first < second) return -1;
else return 0; else return 0;
}); });
const sections = $(emotes).split(20); const sections = split(emotes, 20);
const pages = sections.length; const pages = sections.length;
const embed = new MessageEmbed().setTitle("**Emotes**").setColor("AQUA"); const embed = new MessageEmbed().setTitle("**Emotes**").setColor("AQUA");
let desc = ""; let desc = "";
@ -97,9 +106,9 @@ async function displayEmoteList($: CommonLibrary, emotes: GuildEmoji[]) {
if (pages > 1) { if (pages > 1) {
embed.setTitle(`**Emotes** (Page 1 of ${pages})`); embed.setTitle(`**Emotes** (Page 1 of ${pages})`);
const msg = await $.channel.send({embed}); const msg = await channel.send({embed});
$.paginate(msg, $.author.id, pages, (page) => { paginate(msg, author.id, pages, (page) => {
let desc = ""; let desc = "";
for (const emote of sections[page]) { for (const emote of sections[page]) {
desc += `${emote} ${emote.name} (**${emote.guild.name}**)\n`; desc += `${emote} ${emote.name} (**${emote.guild.name}**)\n`;
@ -109,9 +118,9 @@ async function displayEmoteList($: CommonLibrary, emotes: GuildEmoji[]) {
msg.edit(embed); msg.edit(embed);
}); });
} else { } else {
await $.channel.send({embed}); channel.send({embed});
} }
} else { } else {
$.channel.send("No valid emotes found by that query."); channel.send("No valid emotes found by that query.");
} }
} }

View File

@ -1,5 +1,4 @@
import Command from "../../core/command"; import Command from "../../core/command";
import {CommonLibrary} from "../../core/lib";
import {Message, Channel, TextChannel} from "discord.js"; import {Message, Channel, TextChannel} from "discord.js";
import {queryClosestEmoteByName} from "./subcommands/emote-utils"; import {queryClosestEmoteByName} from "./subcommands/emote-utils";
@ -7,7 +6,7 @@ export default new Command({
description: description:
"Reacts to the a previous message in your place. You have to react with the same emote before the bot removes that reaction.", "Reacts to the a previous message in your place. You have to react with the same emote before the bot removes that reaction.",
usage: 'react <emotes...> (<distance / message ID / "Copy ID" / "Copy Message Link">)', usage: 'react <emotes...> (<distance / message ID / "Copy ID" / "Copy Message Link">)',
async run($: CommonLibrary): Promise<any> { async run($) {
let target: Message | undefined; let target: Message | undefined;
let distance = 1; let distance = 1;
@ -106,5 +105,7 @@ export default new Command({
reaction.users.remove($.client.user!); reaction.users.remove($.client.user!);
}, 5000); }, 5000);
} }
return;
} }
}); });

View File

@ -1,5 +1,4 @@
import Command from "../../core/command"; import Command from "../../core/command";
import {CommonLibrary} from "../../core/lib";
export default new Command({ export default new Command({
description: "Repeats your message.", description: "Repeats your message.",
@ -7,7 +6,7 @@ export default new Command({
run: "Please provide a message for me to say!", run: "Please provide a message for me to say!",
any: new Command({ any: new Command({
description: "Message to repeat.", description: "Message to repeat.",
async run($: CommonLibrary): Promise<any> { async run($) {
$.channel.send(`*${$.author} says:*\n${$.args.join(" ")}`); $.channel.send(`*${$.author} says:*\n${$.args.join(" ")}`);
} }
}) })

View File

@ -1,12 +1,11 @@
import Command from "../../core/command"; import Command from "../../core/command";
import {CommonLibrary} from "../../core/lib";
import * as https from "https"; import * as https from "https";
export default new Command({ export default new Command({
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($: CommonLibrary): Promise<any> { async run($) {
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,4 +1,5 @@
import Command from "../../core/command"; import Command from "../../core/command";
import {ask, askYesOrNo, askMultipleChoice, prompt, callMemberByUsername} from "../../core/libd";
import {Storage} from "../../core/structures"; import {Storage} from "../../core/structures";
import {User} from "discord.js"; import {User} from "discord.js";
import moment from "moment"; import moment from "moment";
@ -176,7 +177,7 @@ export default new Command({
// Welcome to callback hell. We hope you enjoy your stay here! // Welcome to callback hell. We hope you enjoy your stay here!
setup: new Command({ setup: new Command({
description: "Registers your timezone information for the bot.", description: "Registers your timezone information for the bot.",
async run({author, channel, ask, askYesOrNo, askMultipleChoice}) { async run({author, channel}) {
const profile = Storage.getUser(author.id); const profile = Storage.getUser(author.id);
profile.timezone = null; profile.timezone = null;
profile.daylightSavingsRegion = null; profile.daylightSavingsRegion = null;
@ -328,7 +329,7 @@ export default new Command({
}), }),
delete: new Command({ delete: new Command({
description: "Delete your timezone information.", description: "Delete your timezone information.",
async run({channel, author, prompt}) { async run({channel, author}) {
prompt( prompt(
await channel.send( await channel.send(
"Are you sure you want to delete your timezone information?\n*(This message will automatically be deleted after 10 seconds.)*" "Are you sure you want to delete your timezone information?\n*(This message will automatically be deleted after 10 seconds.)*"
@ -382,7 +383,7 @@ export default new Command({
}), }),
any: new Command({ any: new Command({
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({channel, args, message, callMemberByUsername}) { async run({channel, args, message}) {
callMemberByUsername(message, args.join(" "), (member) => { callMemberByUsername(message, args.join(" "), (member) => {
channel.send(getTimeEmbed(member.user)); channel.send(getTimeEmbed(member.user));
}); });

View File

@ -1,16 +1,27 @@
import {isType, parseVars, CommonLibrary} from "./lib"; import {parseVars} from "./libd";
import {Collection} from "discord.js"; import {Collection} from "discord.js";
import {Client, Message, TextChannel, DMChannel, NewsChannel, Guild, User, GuildMember} from "discord.js";
import {PERMISSIONS} from "./permissions"; import {PERMISSIONS} from "./permissions";
import {getPrefix} from "../core/structures"; import {getPrefix} from "../core/structures";
import glob from "glob"; import glob from "glob";
interface CommandMenu {
args: any[];
client: Client;
message: Message;
channel: TextChannel | DMChannel | NewsChannel;
guild: Guild | null;
author: User;
member: GuildMember | null;
}
interface CommandOptions { interface CommandOptions {
description?: string; description?: string;
endpoint?: boolean; endpoint?: boolean;
usage?: string; usage?: string;
permission?: PERMISSIONS | null; permission?: PERMISSIONS | null;
aliases?: string[]; aliases?: string[];
run?: (($: CommonLibrary) => Promise<any>) | string; run?: (($: CommandMenu) => Promise<any>) | string;
subcommands?: {[key: string]: Command}; subcommands?: {[key: string]: Command};
user?: Command; user?: Command;
number?: Command; number?: Command;
@ -32,7 +43,7 @@ export default class Command {
public readonly permission: PERMISSIONS | null; public readonly permission: PERMISSIONS | null;
public readonly aliases: string[]; // This is to keep the array intact for parent Command instances to use. It'll also be used when loading top-level aliases. public readonly aliases: string[]; // This is to keep the array intact for parent Command instances to use. It'll also be used when loading top-level aliases.
public originalCommandName: string | null; // If the command is an alias, what's the original name? public originalCommandName: string | null; // If the command is an alias, what's the original name?
public run: (($: CommonLibrary) => Promise<any>) | string; public run: (($: CommandMenu) => Promise<any>) | string;
public readonly subcommands: Collection<string, Command>; // This is the final data structure you'll actually use to work with the commands the aliases point to. public readonly subcommands: Collection<string, Command>; // This is the final data structure you'll actually use to work with the commands the aliases point to.
public user: Command | null; public user: Command | null;
public number: Command | null; public number: Command | null;
@ -96,11 +107,11 @@ export default class Command {
); );
} }
public execute($: CommonLibrary) { public execute($: CommandMenu) {
if (isType(this.run, String)) { if (typeof this.run === "string") {
$.channel.send( $.channel.send(
parseVars( parseVars(
this.run as string, this.run,
{ {
author: $.author.toString(), author: $.author.toString(),
prefix: getPrefix($.guild) prefix: getPrefix($.guild)
@ -108,7 +119,7 @@ export default class Command {
"???" "???"
) )
); );
} else (this.run as Function)($).catch($.handler.bind($)); } else this.run($).catch(handler.bind($));
} }
public resolve(param: string): TYPES { public resolve(param: string): TYPES {
@ -224,3 +235,20 @@ function globP(path: string) {
}); });
}); });
} }
// If you use promises, use this function to display the error in chat.
// Case #1: await $.channel.send(""); --> Automatically caught by Command.execute().
// Case #2: $.channel.send("").catch(handler.bind($)); --> Manually caught by the user.
// TODO: Find a way to catch unhandled rejections automatically, forgoing the need for this.
export function handler(this: CommandMenu, error: Error) {
if (this)
this.channel.send(
`There was an error while trying to execute that command!\`\`\`${error.stack ?? error}\`\`\``
);
else
console.warn(
"No context was attached to $.handler! Make sure to use .catch($.handler.bind($)) or .catch(error => $.handler(error)) instead!"
);
console.error(error);
}

View File

@ -1,46 +1,46 @@
import {strict as assert} from "assert"; import {strict as assert} from "assert";
import {NumberWrapper, StringWrapper, ArrayWrapper} from "./wrappers"; import {pluralise, pluraliseSigned, replaceAll, toTitleCase, split} from "./lib";
// I can't figure out a way to run the test suite while running the bot. // I can't figure out a way to run the test suite while running the bot.
describe("Wrappers", () => { describe("Wrappers", () => {
describe("NumberWrapper", () => { describe("NumberWrapper", () => {
describe("#pluralise()", () => { describe("#pluralise()", () => {
it('should return "5 credits"', () => { it('should return "5 credits"', () => {
assert.strictEqual(new NumberWrapper(5).pluralise("credit", "s"), "5 credits"); assert.strictEqual(pluralise(5, "credit", "s"), "5 credits");
}); });
it('should return "1 credit"', () => { it('should return "1 credit"', () => {
assert.strictEqual(new NumberWrapper(1).pluralise("credit", "s"), "1 credit"); assert.strictEqual(pluralise(1, "credit", "s"), "1 credit");
}); });
it('should return "-1 credits"', () => { it('should return "-1 credits"', () => {
assert.strictEqual(new NumberWrapper(-1).pluralise("credit", "s"), "-1 credits"); assert.strictEqual(pluralise(-1, "credit", "s"), "-1 credits");
}); });
it("should be able to work with a plural suffix", () => { it("should be able to work with a plural suffix", () => {
assert.strictEqual(new NumberWrapper(2).pluralise("part", "ies", "y"), "2 parties"); assert.strictEqual(pluralise(2, "part", "ies", "y"), "2 parties");
}); });
it("should be able to work with a singular suffix", () => { it("should be able to work with a singular suffix", () => {
assert.strictEqual(new NumberWrapper(1).pluralise("part", "ies", "y"), "1 party"); assert.strictEqual(pluralise(1, "part", "ies", "y"), "1 party");
}); });
it("should be able to exclude the number", () => { it("should be able to exclude the number", () => {
assert.strictEqual(new NumberWrapper(1).pluralise("credit", "s", "", true), "credit"); assert.strictEqual(pluralise(1, "credit", "s", "", true), "credit");
}); });
}); });
describe("#pluraliseSigned()", () => { describe("#pluraliseSigned()", () => {
it('should return "-1 credits"', () => { it('should return "-1 credits"', () => {
assert.strictEqual(new NumberWrapper(-1).pluraliseSigned("credit", "s"), "-1 credits"); assert.strictEqual(pluraliseSigned(-1, "credit", "s"), "-1 credits");
}); });
it('should return "+0 credits"', () => { it('should return "+0 credits"', () => {
assert.strictEqual(new NumberWrapper(0).pluraliseSigned("credit", "s"), "+0 credits"); assert.strictEqual(pluraliseSigned(0, "credit", "s"), "+0 credits");
}); });
it('should return "+1 credit"', () => { it('should return "+1 credit"', () => {
assert.strictEqual(new NumberWrapper(1).pluraliseSigned("credit", "s"), "+1 credit"); assert.strictEqual(pluraliseSigned(1, "credit", "s"), "+1 credit");
}); });
}); });
}); });
@ -48,14 +48,14 @@ describe("Wrappers", () => {
describe("StringWrapper", () => { describe("StringWrapper", () => {
describe("#replaceAll()", () => { describe("#replaceAll()", () => {
it('should convert "test" to "zesz"', () => { it('should convert "test" to "zesz"', () => {
assert.strictEqual(new StringWrapper("test").replaceAll("t", "z"), "zesz"); assert.strictEqual(replaceAll("test", "t", "z"), "zesz");
}); });
}); });
describe("#toTitleCase()", () => { describe("#toTitleCase()", () => {
it("should capitalize the first letter of each word", () => { it("should capitalize the first letter of each word", () => {
assert.strictEqual( assert.strictEqual(
new StringWrapper("yeetus deletus find salvation from jesus").toTitleCase(), toTitleCase("yeetus deletus find salvation from jesus"),
"Yeetus Deletus Find Salvation From Jesus" "Yeetus Deletus Find Salvation From Jesus"
); );
}); });
@ -65,7 +65,7 @@ describe("Wrappers", () => {
describe("ArrayWrapper", () => { describe("ArrayWrapper", () => {
describe("#split()", () => { describe("#split()", () => {
it("should split [1,2,3,4,5,6,7,8,9,10] into [[1,2,3],[4,5,6],[7,8,9],[10]]", () => { it("should split [1,2,3,4,5,6,7,8,9,10] into [[1,2,3],[4,5,6],[7,8,9],[10]]", () => {
assert.deepStrictEqual(new ArrayWrapper([1, 2, 3, 4, 5, 6, 7, 8, 9, 10]).split(3), [ assert.deepStrictEqual(split([1, 2, 3, 4, 5, 6, 7, 8, 9, 10], 3), [
[1, 2, 3], [1, 2, 3],
[4, 5, 6], [4, 5, 6],
[7, 8, 9], [7, 8, 9],

View File

@ -1,483 +1,61 @@
import {GenericWrapper, NumberWrapper, StringWrapper, ArrayWrapper} from "./wrappers"; // Library for pure functions
import {Client, Message, TextChannel, DMChannel, NewsChannel, Guild, User, GuildMember, Permissions} from "discord.js";
import {get} from "https";
import FileManager from "./storage";
import {eventListeners} from "../events/messageReactionRemove";
import {client} from "../index";
import {EmoteRegistryDump, EmoteRegistryDumpEntry} from "./structures";
/** A type that describes what the library module does. */
export interface CommonLibrary {
// Wrapper Object //
/** Wraps the value you enter with an object that provides extra functionality and provides common utility functions. */
(value: number): NumberWrapper;
(value: string): StringWrapper;
<T>(value: T[]): ArrayWrapper<T>;
<T>(value: T): GenericWrapper<T>;
// Common Library Functions //
/** <Promise>.catch($.handler.bind($)) or <Promise>.catch(error => $.handler(error)) */
handler: (error: Error) => void;
paginate: (
message: Message,
senderID: string,
total: number,
callback: (page: number) => void,
duration?: number
) => void;
prompt: (message: Message, senderID: string, onConfirm: () => void, duration?: number) => void;
getMemberByUsername: (guild: Guild, username: string) => Promise<GuildMember | undefined>;
callMemberByUsername: (
message: Message,
username: string,
onSuccess: (member: GuildMember) => void
) => Promise<void>;
ask: (
message: Message,
senderID: string,
condition: (reply: string) => boolean,
onSuccess: () => void,
onReject: () => string,
timeout?: number
) => void;
askYesOrNo: (message: Message, senderID: string, timeout?: number) => Promise<boolean>;
askMultipleChoice: (message: Message, senderID: string, callbackStack: (() => void)[], timeout?: number) => void;
// Dynamic Properties //
args: any[];
client: Client;
message: Message;
channel: TextChannel | DMChannel | NewsChannel;
guild: Guild | null;
author: User;
member: GuildMember | null;
}
export default function $(value: number): NumberWrapper;
export default function $(value: string): StringWrapper;
export default function $<T>(value: T[]): ArrayWrapper<T>;
export default function $<T>(value: T): GenericWrapper<T>;
export default function $(value: any) {
if (isType(value, Number)) return new NumberWrapper(value);
else if (isType(value, String)) return new StringWrapper(value);
else if (isType(value, Array)) return new ArrayWrapper(value);
else return new GenericWrapper(value);
}
// If you use promises, use this function to display the error in chat.
// Case #1: await $.channel.send(""); --> Automatically caught by Command.execute().
// Case #2: $.channel.send("").catch($.handler.bind($)); --> Manually caught by the user.
$.handler = function (this: CommonLibrary, error: Error) {
if (this)
this.channel.send(
`There was an error while trying to execute that command!\`\`\`${error.stack ?? error}\`\`\``
);
else
console.warn(
"No context was attached to $.handler! Make sure to use .catch($.handler.bind($)) or .catch(error => $.handler(error)) instead!"
);
console.error(error);
};
export function botHasPermission(guild: Guild | null, permission: number): boolean {
return !!guild?.me?.hasPermission(permission);
}
export function updateGlobalEmoteRegistry(): void {
const data: EmoteRegistryDump = {version: 1, list: []};
for (const guild of client.guilds.cache.values()) {
for (const emote of guild.emojis.cache.values()) {
data.list.push({
ref: emote.name,
id: emote.id,
name: emote.name,
requires_colons: emote.requiresColons || false,
animated: emote.animated,
url: emote.url,
guild_id: emote.guild.name,
guild_name: emote.guild.name
});
}
}
FileManager.write("emote-registry", data, true);
}
// Maybe promisify this section to reduce the potential for creating callback hell? Especially if multiple questions in a row are being asked.
// Pagination function that allows for customization via a callback.
// Define your own pages outside the function because this only manages the actual turning of pages.
$.paginate = async (
message: Message,
senderID: string,
total: number,
callback: (page: number) => void,
duration = 60000
) => {
let page = 0;
const turn = (amount: number) => {
page += amount;
if (page < 0) page += total;
else if (page >= total) page -= total;
callback(page);
};
const BACKWARDS_EMOJI = "⬅️";
const FORWARDS_EMOJI = "➡️";
const handle = (emote: string, reacterID: string) => {
switch (emote) {
case BACKWARDS_EMOJI:
turn(-1);
break;
case FORWARDS_EMOJI:
turn(1);
break;
}
};
// Listen for reactions and call the handler.
let backwardsReaction = await message.react(BACKWARDS_EMOJI);
let forwardsReaction = await message.react(FORWARDS_EMOJI);
eventListeners.set(message.id, handle);
await message.awaitReactions(
(reaction, user) => {
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);
}
return false;
},
{time: duration}
);
// When time's up, remove the bot's own reactions.
eventListeners.delete(message.id);
backwardsReaction.users.remove(message.author);
forwardsReaction.users.remove(message.author);
};
// 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?
$.prompt = async (message: Message, senderID: string, onConfirm: () => void, duration = 10000) => {
let isDeleted = false;
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();
};
// A list of "channel-message" and callback pairs. Also, I imagine that the callback will be much more maintainable when discord.js v13 comes out with a dedicated message.referencedMessage property.
// Also, I'm defining it here instead of the message event because the load order screws up if you export it from there. Yeah... I'm starting to notice just how much technical debt has been built up. The command handler needs to be modularized and refactored sooner rather than later. Define all constants in one area then grab from there.
export const replyEventListeners = new Map<string, (message: Message) => void>();
// 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.
$.ask = async (
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);
};
$.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();
}
}
return false;
},
{time: 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.
const multiNumbers = ["1⃣", "2⃣", "3⃣", "4⃣", "5⃣", "6⃣", "7⃣", "8⃣", "9⃣", "🔟"];
// This will bring up an option to let the user choose between one option out of many.
// This definitely needs a single callback alternative, because using the numerical version isn't actually that uncommon of a pattern.
$.askMultipleChoice = async (message: Message, senderID: string, callbackStack: (() => void)[], timeout = 90000) => {
if (callbackStack.length > multiNumbers.length) {
message.channel.send(
`\`ERROR: The amount of callbacks in "askMultipleChoice" must not exceed the total amount of allowed options (${multiNumbers.length})!\``
);
return;
}
let isDeleted = false;
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();
}
}
return false;
},
{time: timeout}
);
if (!isDeleted) message.delete();
};
$.getMemberByUsername = async (guild: Guild, username: string) => {
return (
await guild.members.fetch({
query: username,
limit: 1
})
).first();
};
/** Convenience function to handle false cases automatically. */
$.callMemberByUsername = async (message: Message, username: string, onSuccess: (member: GuildMember) => void) => {
const guild = message.guild;
const send = message.channel.send;
if (guild) {
const member = await $.getMemberByUsername(guild, username);
if (member) onSuccess(member);
else send(`Couldn't find a user by the name of \`${username}\`!`);
} else send("You must execute this command in a server!");
};
/** /**
* Splits a command by spaces while accounting for quotes which capture string arguments. * Pluralises a word and chooses a suffix attached to the root provided.
* - `\"` = `"` * - pluralise("credit", "s") = credit/credits
* - `\\` = `\` * - pluralise("part", "ies", "y") = party/parties
* - pluralise("sheep") = sheep
*/ */
export function parseArgs(line: string): string[] { export function pluralise(value: number, word: string, plural = "", singular = "", excludeNumber = false): string {
let result = []; let result = excludeNumber ? "" : `${value} `;
let selection = "";
let inString = false;
let isEscaped = false;
for (let c of line) { if (value === 1) result += word + singular;
if (isEscaped) { else result += word + plural;
if (['"', "\\"].includes(c)) selection += c;
else selection += "\\" + c;
isEscaped = false;
} else if (c === "\\") isEscaped = true;
else if (c === '"') inString = !inString;
else if (c === " " && !inString) {
result.push(selection);
selection = "";
} else selection += c;
}
if (selection.length > 0) result.push(selection);
return result; return result;
} }
/** /**
* Allows you to store a template string with variable markers and parse it later. * Pluralises a word for changes.
* - Use `%name%` for variables * - (-1).pluraliseSigned() = '-1 credits'
* - `%%` = `%` * - (0).pluraliseSigned() = '+0 credits'
* - If the invalid token is null/undefined, nothing is changed. * - (1).pluraliseSigned() = '+1 credit'
*/ */
export function parseVars(line: string, definitions: {[key: string]: string}, invalid: string | null = ""): string { export function pluraliseSigned(
let result = ""; value: number,
let inVariable = false; word: string,
let token = ""; plural = "",
singular = "",
for (const c of line) { excludeNumber = false
if (c === "%") { ): string {
if (inVariable) { const sign = value >= 0 ? "+" : "";
if (token === "") result += "%"; return `${sign}${pluralise(value, word, plural, singular, excludeNumber)}`;
else {
if (token in definitions) result += definitions[token];
else if (invalid === null) result += `%${token}%`;
else result += invalid;
token = "";
}
}
inVariable = !inVariable;
} else if (inVariable) token += c;
else result += c;
}
return result;
} }
export function isType(value: any, type: any): boolean { export function replaceAll(text: string, before: string, after: string): string {
if (value === undefined && type === undefined) return true; while (text.indexOf(before) !== -1) text = text.replace(before, after);
else if (value === null && type === null) return true; return text;
else return value !== undefined && value !== null && value.constructor === type; }
export function toTitleCase(text: string): string {
return text.replace(/([^\W_]+[^\s-]*) */g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase());
}
/** Returns a random element from this array. */
export function random<T>(array: T[]): T {
return array[Math.floor(Math.random() * array.length)];
} }
/** /**
* Checks a value to see if it matches the fallback's type, otherwise returns the fallback. * Splits up this array into a specified length.
* For the purposes of the templates system, this function will only check array types, objects should be checked under their own type (as you'd do anyway with something like a User object). * `$([1,2,3,4,5,6,7,8,9,10]).split(3)` = `[[1,2,3],[4,5,6],[7,8,9],[10]]`
* If at any point the value doesn't match the data structure provided, the fallback is returned.
* Warning: Type checking is based on the fallback's type. Be sure that the "type" parameter is accurate to this!
*/ */
export function select<T>(value: any, fallback: T, type: Function, isArray = false): T { export function split<T>(array: T[], lengthOfEachSection: number): T[][] {
if (isArray && isType(value, Array)) { const amountOfSections = Math.ceil(array.length / lengthOfEachSection);
for (let item of value) if (!isType(item, type)) return fallback; const sections = new Array<T[]>(amountOfSections);
return value;
} else { for (let index = 0; index < amountOfSections; index++)
if (isType(value, type)) return value; sections[index] = array.slice(index * lengthOfEachSection, (index + 1) * lengthOfEachSection);
else return fallback;
} return sections;
} }
export function clean(text: any) {
if (typeof text === "string")
return text.replace(/`/g, "`" + String.fromCharCode(8203)).replace(/@/g, "@" + String.fromCharCode(8203));
else return text;
}
export function trimArray(arr: any, maxLen = 10) {
if (arr.length > maxLen) {
const len = arr.length - maxLen;
arr = arr.slice(0, maxLen);
arr.push(`${len} more...`);
}
return arr;
}
export function formatBytes(bytes: any) {
if (bytes === 0) return "0 Bytes";
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${parseFloat((bytes / Math.pow(1024, i)).toFixed(2))} ${sizes[i]}`;
}
export function getContent(url: any) {
return new Promise((resolve, reject) => {
get(url, (res: {resume?: any; setEncoding?: any; on?: any; statusCode?: any}) => {
const {statusCode} = res;
if (statusCode !== 200) {
res.resume();
reject(`Request failed. Status code: ${statusCode}`);
}
res.setEncoding("utf8");
let rawData = "";
res.on("data", (chunk: string) => {
rawData += chunk;
});
res.on("end", () => {
try {
const parsedData = JSON.parse(rawData);
resolve(parsedData);
} catch (e) {
reject(`Error: ${e.message}`);
}
});
}).on("error", (err: {message: any}) => {
reject(`Error: ${err.message}`);
});
});
}
export interface GenericJSON {
[key: string]: any;
}
export abstract class GenericStructure {
private __meta__ = "generic";
constructor(tag?: string) {
this.__meta__ = tag || this.__meta__;
}
public save(asynchronous = true) {
const tag = this.__meta__;
/// @ts-ignore
delete this.__meta__;
FileManager.write(tag, this, asynchronous);
this.__meta__ = tag;
}
}
// A 50% chance would be "Math.random() < 0.5" because Math.random() can be [0, 1), so to make two equal ranges, you'd need [0, 0.5)U[0.5, 1).
// Similar logic would follow for any other percentage. Math.random() < 1 is always true (100% chance) and Math.random() < 0 is always false (0% chance).
export const Random = {
num: (min: number, max: number) => Math.random() * (max - min) + min,
int: (min: number, max: number) => Math.floor(Random.num(min, max)),
chance: (decimal: number) => Math.random() < decimal,
sign: (number = 1) => number * (Random.chance(0.5) ? -1 : 1),
deviation: (base: number, deviation: number) => Random.num(base - deviation, base + deviation)
};

418
src/core/libd.ts Normal file
View File

@ -0,0 +1,418 @@
// Library for Discord-specific functions
import {Message, Guild, GuildMember, Permissions} from "discord.js";
import {get} from "https";
import FileManager from "./storage";
import {eventListeners} from "../events/messageReactionRemove";
import {client} from "../index";
import {EmoteRegistryDump} from "./structures";
export function botHasPermission(guild: Guild | null, permission: number): boolean {
return !!guild?.me?.hasPermission(permission);
}
export function updateGlobalEmoteRegistry(): void {
const data: EmoteRegistryDump = {version: 1, list: []};
for (const guild of client.guilds.cache.values()) {
for (const emote of guild.emojis.cache.values()) {
data.list.push({
ref: emote.name,
id: emote.id,
name: emote.name,
requires_colons: emote.requiresColons || false,
animated: emote.animated,
url: emote.url,
guild_id: emote.guild.name,
guild_name: emote.guild.name
});
}
}
FileManager.write("emote-registry", data, true);
}
// Maybe promisify this section to reduce the potential for creating callback hell? Especially if multiple questions in a row are being asked.
// Pagination function that allows for customization via a callback.
// Define your own pages outside the function because this only manages the actual turning of pages.
export async function paginate(
message: Message,
senderID: string,
total: number,
callback: (page: number) => void,
duration = 60000
) {
let page = 0;
const turn = (amount: number) => {
page += amount;
if (page < 0) page += total;
else if (page >= total) page -= total;
callback(page);
};
const BACKWARDS_EMOJI = "⬅️";
const FORWARDS_EMOJI = "➡️";
const handle = (emote: string, reacterID: string) => {
switch (emote) {
case BACKWARDS_EMOJI:
turn(-1);
break;
case FORWARDS_EMOJI:
turn(1);
break;
}
};
// Listen for reactions and call the handler.
let backwardsReaction = await message.react(BACKWARDS_EMOJI);
let forwardsReaction = await message.react(FORWARDS_EMOJI);
eventListeners.set(message.id, handle);
await message.awaitReactions(
(reaction, user) => {
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);
}
return false;
},
{time: duration}
);
// When time's up, remove the bot's own reactions.
eventListeners.delete(message.id);
backwardsReaction.users.remove(message.author);
forwardsReaction.users.remove(message.author);
}
// 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?
export async function prompt(message: Message, senderID: string, onConfirm: () => void, duration = 10000) {
let isDeleted = false;
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();
}
// A list of "channel-message" and callback pairs. Also, I imagine that the callback will be much more maintainable when discord.js v13 comes out with a dedicated message.referencedMessage property.
// Also, I'm defining it here instead of the message event because the load order screws up if you export it from there. Yeah... I'm starting to notice just how much technical debt has been built up. The command handler needs to be modularized and refactored sooner rather than later. Define all constants in one area then grab from there.
export const replyEventListeners = new Map<string, (message: Message) => void>();
// 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();
}
}
return false;
},
{time: 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.
const multiNumbers = ["1⃣", "2⃣", "3⃣", "4⃣", "5⃣", "6⃣", "7⃣", "8⃣", "9⃣", "🔟"];
// This will bring up an option to let the user choose between one option out of many.
// This definitely needs a single callback alternative, because using the numerical version isn't actually that uncommon of a pattern.
export async function askMultipleChoice(
message: Message,
senderID: string,
callbackStack: (() => void)[],
timeout = 90000
) {
if (callbackStack.length > multiNumbers.length) {
message.channel.send(
`\`ERROR: The amount of callbacks in "askMultipleChoice" must not exceed the total amount of allowed options (${multiNumbers.length})!\``
);
return;
}
let isDeleted = false;
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();
}
}
return false;
},
{time: timeout}
);
if (!isDeleted) message.delete();
}
export async function getMemberByUsername(guild: Guild, username: string) {
return (
await guild.members.fetch({
query: username,
limit: 1
})
).first();
}
/** Convenience function to handle false cases automatically. */
export async function callMemberByUsername(
message: Message,
username: string,
onSuccess: (member: GuildMember) => void
) {
const guild = message.guild;
const send = message.channel.send;
if (guild) {
const member = await getMemberByUsername(guild, username);
if (member) onSuccess(member);
else send(`Couldn't find a user by the name of \`${username}\`!`);
} else send("You must execute this command in a server!");
}
/**
* Splits a command by spaces while accounting for quotes which capture string arguments.
* - `\"` = `"`
* - `\\` = `\`
*/
export function parseArgs(line: string): string[] {
let result = [];
let selection = "";
let inString = false;
let isEscaped = false;
for (let c of line) {
if (isEscaped) {
if (['"', "\\"].includes(c)) selection += c;
else selection += "\\" + c;
isEscaped = false;
} else if (c === "\\") isEscaped = true;
else if (c === '"') inString = !inString;
else if (c === " " && !inString) {
result.push(selection);
selection = "";
} else selection += c;
}
if (selection.length > 0) result.push(selection);
return result;
}
/**
* Allows you to store a template string with variable markers and parse it later.
* - Use `%name%` for variables
* - `%%` = `%`
* - If the invalid token is null/undefined, nothing is changed.
*/
export function parseVars(line: string, definitions: {[key: string]: string}, invalid: string | null = ""): string {
let result = "";
let inVariable = false;
let token = "";
for (const c of line) {
if (c === "%") {
if (inVariable) {
if (token === "") result += "%";
else {
if (token in definitions) result += definitions[token];
else if (invalid === null) result += `%${token}%`;
else result += invalid;
token = "";
}
}
inVariable = !inVariable;
} else if (inVariable) token += c;
else result += c;
}
return result;
}
export function isType(value: any, type: any): boolean {
if (value === undefined && type === undefined) return true;
else if (value === null && type === null) return true;
else return value !== undefined && value !== null && value.constructor === type;
}
/**
* Checks a value to see if it matches the fallback's type, otherwise returns the fallback.
* For the purposes of the templates system, this function will only check array types, objects should be checked under their own type (as you'd do anyway with something like a User object).
* If at any point the value doesn't match the data structure provided, the fallback is returned.
* Warning: Type checking is based on the fallback's type. Be sure that the "type" parameter is accurate to this!
*/
export function select<T>(value: any, fallback: T, type: Function, isArray = false): T {
if (isArray && isType(value, Array)) {
for (let item of value) if (!isType(item, type)) return fallback;
return value;
} else {
if (isType(value, type)) return value;
else return fallback;
}
}
export function clean(text: any) {
if (typeof text === "string")
return text.replace(/`/g, "`" + String.fromCharCode(8203)).replace(/@/g, "@" + String.fromCharCode(8203));
else return text;
}
export function trimArray(arr: any, maxLen = 10) {
if (arr.length > maxLen) {
const len = arr.length - maxLen;
arr = arr.slice(0, maxLen);
arr.push(`${len} more...`);
}
return arr;
}
export function formatBytes(bytes: any) {
if (bytes === 0) return "0 Bytes";
const sizes = ["Bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB"];
const i = Math.floor(Math.log(bytes) / Math.log(1024));
return `${parseFloat((bytes / Math.pow(1024, i)).toFixed(2))} ${sizes[i]}`;
}
export function getContent(url: any) {
return new Promise((resolve, reject) => {
get(url, (res: {resume?: any; setEncoding?: any; on?: any; statusCode?: any}) => {
const {statusCode} = res;
if (statusCode !== 200) {
res.resume();
reject(`Request failed. Status code: ${statusCode}`);
}
res.setEncoding("utf8");
let rawData = "";
res.on("data", (chunk: string) => {
rawData += chunk;
});
res.on("end", () => {
try {
const parsedData = JSON.parse(rawData);
resolve(parsedData);
} catch (e) {
reject(`Error: ${e.message}`);
}
});
}).on("error", (err: {message: any}) => {
reject(`Error: ${err.message}`);
});
});
}
export interface GenericJSON {
[key: string]: any;
}
export abstract class GenericStructure {
private __meta__ = "generic";
constructor(tag?: string) {
this.__meta__ = tag || this.__meta__;
}
public save(asynchronous = true) {
const tag = this.__meta__;
/// @ts-ignore
delete this.__meta__;
FileManager.write(tag, this, asynchronous);
this.__meta__ = tag;
}
}
// A 50% chance would be "Math.random() < 0.5" because Math.random() can be [0, 1), so to make two equal ranges, you'd need [0, 0.5)U[0.5, 1).
// Similar logic would follow for any other percentage. Math.random() < 1 is always true (100% chance) and Math.random() < 0 is always false (0% chance).
export const Random = {
num: (min: number, max: number) => Math.random() * (max - min) + min,
int: (min: number, max: number) => Math.floor(Random.num(min, max)),
chance: (decimal: number) => Math.random() < decimal,
sign: (number = 1) => number * (Random.chance(0.5) ? -1 : 1),
deviation: (base: number, deviation: number) => Random.num(base - deviation, base + deviation)
};

View File

@ -1,5 +1,5 @@
import FileManager from "./storage"; import FileManager from "./storage";
import {select, GenericJSON, GenericStructure} from "./lib"; import {select, GenericJSON, GenericStructure} from "./libd";
import {watch} from "fs"; import {watch} from "fs";
import {Guild as DiscordGuild, Snowflake} from "discord.js"; import {Guild as DiscordGuild, Snowflake} from "discord.js";

View File

@ -1,73 +0,0 @@
export class GenericWrapper<T> {
protected readonly value: T;
public constructor(value: T) {
this.value = value;
}
}
export class NumberWrapper extends GenericWrapper<number> {
/**
* Pluralises a word and chooses a suffix attached to the root provided.
* - pluralise("credit", "s") = credit/credits
* - pluralise("part", "ies", "y") = party/parties
* - pluralise("sheep") = sheep
*/
public pluralise(word: string, plural = "", singular = "", excludeNumber = false): string {
let result = excludeNumber ? "" : `${this.value} `;
if (this.value === 1) result += word + singular;
else result += word + plural;
return result;
}
/**
* Pluralises a word for changes.
* - (-1).pluraliseSigned() = '-1 credits'
* - (0).pluraliseSigned() = '+0 credits'
* - (1).pluraliseSigned() = '+1 credit'
*/
public pluraliseSigned(word: string, plural = "", singular = "", excludeNumber = false): string {
const sign = this.value >= 0 ? "+" : "";
return `${sign}${this.pluralise(word, plural, singular, excludeNumber)}`;
}
}
export class StringWrapper extends GenericWrapper<string> {
public replaceAll(before: string, after: string): string {
let result = this.value;
while (result.indexOf(before) !== -1) result = result.replace(before, after);
return result;
}
public toTitleCase(): string {
return this.value.replace(
/([^\W_]+[^\s-]*) */g,
(txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()
);
}
}
export class ArrayWrapper<T> extends GenericWrapper<T[]> {
/** Returns a random element from this array. */
public random(): T {
return this.value[Math.floor(Math.random() * this.value.length)];
}
/**
* Splits up this array into a specified length.
* `$([1,2,3,4,5,6,7,8,9,10]).split(3)` = `[[1,2,3],[4,5,6],[7,8,9],[10]]`
*/
public split(lengthOfEachSection: number): T[][] {
const amountOfSections = Math.ceil(this.value.length / lengthOfEachSection);
const sections: T[][] = new Array(amountOfSections);
for (let index = 0; index < amountOfSections; index++)
sections[index] = this.value.slice(index * lengthOfEachSection, (index + 1) * lengthOfEachSection);
return sections;
}
}

View File

@ -1,5 +1,5 @@
import Event from "../core/event"; import Event from "../core/event";
import {updateGlobalEmoteRegistry} from "../core/lib"; import {updateGlobalEmoteRegistry} from "../core/libd";
export default new Event<"emojiCreate">({ export default new Event<"emojiCreate">({
on(emote) { on(emote) {

View File

@ -1,5 +1,5 @@
import Event from "../core/event"; import Event from "../core/event";
import {updateGlobalEmoteRegistry} from "../core/lib"; import {updateGlobalEmoteRegistry} from "../core/libd";
export default new Event<"emojiDelete">({ export default new Event<"emojiDelete">({
on() { on() {

View File

@ -1,5 +1,5 @@
import Event from "../core/event"; import Event from "../core/event";
import {updateGlobalEmoteRegistry} from "../core/lib"; import {updateGlobalEmoteRegistry} from "../core/libd";
export default new Event<"emojiUpdate">({ export default new Event<"emojiUpdate">({
on() { on() {

View File

@ -1,5 +1,5 @@
import Event from "../core/event"; import Event from "../core/event";
import {updateGlobalEmoteRegistry} from "../core/lib"; import {updateGlobalEmoteRegistry} from "../core/libd";
export default new Event<"guildCreate">({ export default new Event<"guildCreate">({
on() { on() {

View File

@ -1,5 +1,5 @@
import Event from "../core/event"; import Event from "../core/event";
import {updateGlobalEmoteRegistry} from "../core/lib"; import {updateGlobalEmoteRegistry} from "../core/libd";
export default new Event<"guildDelete">({ export default new Event<"guildDelete">({
on() { on() {

View File

@ -3,7 +3,7 @@ import Command, {loadableCommands} from "../core/command";
import {hasPermission, getPermissionLevel, PermissionNames} from "../core/permissions"; import {hasPermission, getPermissionLevel, PermissionNames} from "../core/permissions";
import {Permissions} from "discord.js"; import {Permissions} from "discord.js";
import {getPrefix} from "../core/structures"; import {getPrefix} from "../core/structures";
import $, {replyEventListeners} from "../core/lib"; import {replyEventListeners} from "../core/libd";
import quote from "../modules/message_embed"; import quote from "../modules/message_embed";
export default new Event<"message">({ export default new Event<"message">({
@ -132,20 +132,14 @@ export default new Event<"message">({
// The purpose of using $.bind($) is to clone the function so as to not modify the original $. // The purpose of using $.bind($) is to clone the function so as to not modify the original $.
// The cloned function doesn't copy the properties, so Object.assign() is used. // The cloned function doesn't copy the properties, so Object.assign() is used.
// Object.assign() modifies the first element and returns that, the second element applies its properties and the third element applies its own overriding the second one. // Object.assign() modifies the first element and returns that, the second element applies its properties and the third element applies its own overriding the second one.
command.execute( command.execute({
Object.assign( args: params,
$.bind($), author: message.author,
{ channel: message.channel,
args: params, client: message.client,
author: message.author, guild: message.guild,
channel: message.channel, member: message.member,
client: message.client, message: message
guild: message.guild, });
member: message.member,
message: message
},
$
)
);
} }
}); });

View File

@ -1,6 +1,6 @@
import Event from "../core/event"; import Event from "../core/event";
import {Permissions} from "discord.js"; import {Permissions} from "discord.js";
import {botHasPermission} from "../core/lib"; import {botHasPermission} from "../core/libd";
// A list of message ID and callback pairs. You get the emote name and ID of the user reacting. // A list of message ID and callback pairs. You get the emote name and ID of the user reacting.
export const eventListeners: Map<string, (emote: string, id: string) => void> = new Map(); export const eventListeners: Map<string, (emote: string, id: string) => void> = new Map();

View File

@ -1,7 +1,7 @@
import Event from "../core/event"; import Event from "../core/event";
import {client} from "../index"; import {client} from "../index";
import {Config} from "../core/structures"; import {Config} from "../core/structures";
import {updateGlobalEmoteRegistry} from "../core/lib"; import {updateGlobalEmoteRegistry} from "../core/libd";
export default new Event<"ready">({ export default new Event<"ready">({
once() { once() {