Compare commits

..

23 commits

Author SHA1 Message Date
WatDuhHekBro
86ccb74ac2 Highly biased code review 2021-03-30 18:14:15 -05:00
WatDuhHekBro
02c18f57c7 Reworked paginate function 2021-03-30 07:16:31 -05:00
WatDuhHekBro
475ecb3d5d Reworked permission handling 2021-03-30 05:54:52 -05:00
WatDuhHekBro
51fa9457b4 Fully separated utility functions from command menu 2021-03-30 05:25:07 -05:00
WatDuhHekBro
10c1cd9cff Separated custom logger from command menu 2021-03-30 04:02:01 -05:00
WatDuhHekBro
00addd468c Merge branch 'typescript' of https://github.com/keanuplayz/TravBot-v3 into experimental-core 2021-03-30 03:23:11 -05:00
WatDuhHekBro
1954b2d999 Fixed small error 2021-03-28 13:58:10 -05:00
WatDuhHekBro
3b1b8ec914 Added a timeout to regex search 2021-03-28 12:49:45 -05:00
WatDuhHekBro
b3e1b5e140 Removed problematic feature 2021-03-28 11:01:07 -05:00
WatDuhHekBro
90c41c8df4 Added query to lsemotes and searching other guilds 2021-03-28 09:34:31 -05:00
705e093999
Added message quoting.
See https://is.gd/lpGqxj
2021-03-20 13:27:57 +01:00
38e03a85bb
Too lazy to add interceptor; get content instead.
This is for my running joke of reacting with 🚱
to CheeseBot's "Remember to drink water!" message.
2021-03-16 19:07:53 +01:00
0e1d8f3907
Merge branch 'typescript' of github.com:keanuplayz/TravBot-v3 into typescript 2021-03-13 15:09:27 +01:00
3f4ee9315f
Added channel lock for eco. 2021-03-13 15:09:18 +01:00
cec38cf4bd Stop deleting the emote invocation. 2021-03-07 22:07:59 +01:00
Dmytro Meleshko
22bd5302c5
don't install the module os from npm, it's a built-in one (#24) 2021-02-21 11:24:37 +01:00
2ff732c927
Merge pull request #23 from dmitmel/typescript 2021-02-16 13:15:23 +01:00
Dmytro Meleshko
e22250b3f1 introduce the terrible hack for reducing memory usage 2021-01-29 21:29:46 +02:00
Dmytro Meleshko
303e81cc37 reduce the amount of cache lookups 2021-01-29 21:12:53 +02:00
Dmytro Meleshko
0cba164f3d get rid of @ts-ignore or something 2021-01-29 19:15:32 +02:00
Dmytro Meleshko
593efb3602 raise the ES target level and enable sourcemaps in tsconfig.json 2021-01-29 19:07:39 +02:00
Dmytro Meleshko
417bfc8a18 get rid of the remaining calls to BaseManager#resolve to make all cache access explicit 2021-01-29 18:21:06 +02:00
a21ed7a97f
Fixed info using heapTotal for both values. 2021-01-29 14:34:46 +01:00
49 changed files with 9305 additions and 1782 deletions

View file

@ -1,31 +0,0 @@
{
"sfw": {
"tickle": "/img/tickle",
"slap": "/img/slap",
"poke": "/img/poke",
"pat": "/img/pat",
"neko": "/img/neko",
"meow": "/img/meow",
"lizard": "/img/lizard",
"kiss": "/img/kiss",
"hug": "/img/hug",
"foxGirl": "/img/fox_girl",
"feed": "/img/feed",
"cuddle": "/img/cuddle",
"why": "/why",
"catText": "/cat",
"fact": "/fact",
"nekoGif": "/img/ngif",
"kemonomimi": "/img/kemonomimi",
"holo": "/img/holo",
"smug": "/img/smug",
"baka": "/img/baka",
"woof": "/img/woof",
"spoiler": "/spoiler",
"wallpaper": "/img/wallpaper",
"goose": "/img/goose",
"gecg": "/img/gecg",
"avatar": "/img/avatar",
"waifu": "/img/waifu"
}
}

8503
package-lock.json generated

File diff suppressed because it is too large Load diff

View file

@ -10,8 +10,7 @@
"glob": "^7.1.6", "glob": "^7.1.6",
"inquirer": "^7.3.3", "inquirer": "^7.3.3",
"moment": "^2.29.1", "moment": "^2.29.1",
"ms": "^2.1.3", "ms": "^2.1.3"
"os": "^0.1.1"
}, },
"devDependencies": { "devDependencies": {
"@types/glob": "^7.1.3", "@types/glob": "^7.1.3",

View file

@ -1,9 +1,10 @@
import Command from "../core/command"; import Command, {handler} from "../core/command";
import {CommonLibrary, logs, 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 {getPermissionLevel, getPermissionName} from "../core/permissions";
import {Permissions} from "discord.js"; import {Permissions} from "discord.js";
import * as discord from "discord.js"; import * as discord from "discord.js";
import {logs} from "../globals";
function getLogBuffer(type: string) { function getLogBuffer(type: string) {
return { return {
@ -22,26 +23,26 @@ 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 \`${getPermissionName(permLevel)}\` (${permLevel}).`
); );
}, },
subcommands: { subcommands: {
set: new Command({ set: new Command({
description: "Set different per-guild settings for the bot.", description: "Set different per-guild settings for the bot.",
run: "You have to specify the option you want to set.", run: "You have to specify the option you want to set.",
permission: Command.PERMISSIONS.ADMIN, permission: PERMISSIONS.ADMIN,
subcommands: { subcommands: {
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(
@ -49,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]}\`.`);
@ -60,13 +61,13 @@ 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: 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));
@ -81,15 +82,17 @@ 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: 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]}\`...`);
} }
@ -97,24 +100,28 @@ export default new Command({
}) })
}), }),
purge: new Command({ purge: new Command({
description: "Purges bot messages.", description: "Purges the bot's own messages.",
permission: Command.PERMISSIONS.BOT_SUPPORT, permission: PERMISSIONS.BOT_SUPPORT,
async run($: CommonLibrary): Promise<any> { async run($) {
if ($.message.channel instanceof discord.DMChannel) { // It's probably better to go through the bot's own messages instead of calling bulkDelete which requires MANAGE_MESSAGES.
return; if (botHasPermission($.guild, Permissions.FLAGS.MANAGE_MESSAGES) && $.channel.type !== "dm") {
}
$.message.delete(); $.message.delete();
const msgs = await $.channel.messages.fetch({ const msgs = await $.channel.messages.fetch({
limit: 100 limit: 100
}); });
const travMessages = msgs.filter((m) => m.author.id === $.client.user?.id); const travMessages = msgs.filter((m) => m.author.id === $.client.user?.id);
await $.message.channel.send(`Found ${travMessages.size} messages to delete.`).then((m) => await $.channel.send(`Found ${travMessages.size} messages to delete.`).then((m) =>
m.delete({ m.delete({
timeout: 5000 timeout: 5000
}) })
); );
await $.message.channel.bulkDelete(travMessages); await $.channel.bulkDelete(travMessages);
} else {
$.channel.send(
"This command must be executed in a guild where I have the `MANAGE_MESSAGES` permission."
);
}
} }
}), }),
clear: new Command({ clear: new Command({
@ -123,24 +130,25 @@ 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") {
await $.channel.send("Can't clear messages in the DMs!");
return;
}
$.message.delete(); $.message.delete();
const fetched = await $.channel.messages.fetch({ const fetched = await $.channel.messages.fetch({
limit: $.args[0] limit: $.args[0]
}); });
$.channel await $.channel.bulkDelete(fetched);
/// @ts-ignore
.bulkDelete(fetched)
.catch((error: any) => $.channel.send(`Error: ${error}`));
} }
}) })
}), }),
eval: new Command({ eval: new Command({
description: "Evaluate code.", description: "Evaluate code.",
usage: "<code>", usage: "<code>",
permission: Command.PERMISSIONS.BOT_OWNER, permission: PERMISSIONS.BOT_OWNER,
// You have to bring everything into scope to use them. AFAIK, there isn't a more maintainable way to do this, but at least TS will let you know if anything gets removed. // 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);
@ -154,29 +162,28 @@ 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: PERMISSIONS.BOT_SUPPORT,
async run($: CommonLibrary): Promise<any> { async run($) {
const nickName = $.args.join(" "); const nickName = $.args.join(" ");
const trav = $.guild?.members.cache.find((member) => member.id === $.client.user?.id); await $.guild?.me?.setNickname(nickName);
await trav?.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: 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, {split: true});
} }
}), }),
activity: new Command({ activity: new Command({
description: "Set the activity of the bot.", description: "Set the activity of the bot.",
permission: Command.PERMISSIONS.BOT_SUPPORT, permission: 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"
}); });
@ -184,7 +191,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,49 +1,58 @@
import {User} from "discord.js";
import Command from "../../core/command"; import Command from "../../core/command";
import {CommonLibrary} from "../../core/lib"; import {random} from "../../core/lib";
import {parseVars} from "../../core/libd";
const cookies = [
`has given %target% a chocolate chip cookie!`,
`has given %target% a soft homemade oatmeal cookie!`,
`has given %target% a plain, dry, old cookie. It was the last one in the bag. Gross.`,
`gives %target% a sugar cookie. What, no frosting and sprinkles? 0/10 would not touch.`,
`gives %target% a chocolate chip cookie. Oh wait, those are raisins. Bleck!`,
`gives %target% an enormous cookie. Poking it gives you more cookies. Weird.`,
`gives %target% a fortune cookie. It reads "Why aren't you working on any projects?"`,
`gives %target% a fortune cookie. It reads "Give that special someone a compliment"`,
`gives %target% a fortune cookie. It reads "Take a risk!"`,
`gives %target% a fortune cookie. It reads "Go outside."`,
`gives %target% a fortune cookie. It reads "Don't forget to eat your veggies!"`,
`gives %target% a fortune cookie. It reads "Do you even lift?"`,
`gives %target% a fortune cookie. It reads "m808 pls"`,
`gives %target% a fortune cookie. It reads "If you move your hips, you'll get all the ladies."`,
`gives %target% a fortune cookie. It reads "I love you."`,
`gives %target% a Golden Cookie. You can't eat it because it is made of gold. Dammit.`,
`gives %target% an Oreo cookie with a glass of milk!`,
`gives %target% a rainbow cookie made with love :heart:`,
`gives %target% an old cookie that was left out in the rain, it's moldy.`,
`bakes %target% fresh cookies, it smells amazing.`
];
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({ subcommands: {
async run($: CommonLibrary): Promise<any> { all: new Command({
if ($.args[0] == "all") return $.channel.send(`${$.author} gave everybody a cookie!`); async run($) {
$.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: User = $.args[0];
if (!mention) return; if (mention.id == sender.id) {
$.channel.send("You can't give yourself cookies!");
return;
}
const cookies = [ $.channel.send(
`has given <@${mention.id}> a chocolate chip cookie!`, `:cookie: <@${sender.id}> ${parseVars(random(cookies), {
`has given <@${mention.id}> a soft homemade oatmeal cookie!`, target: mention.toString()
`has given <@${mention.id}> a plain, dry, old cookie. It was the last one in the bag. Gross.`, })}`
`gives <@${mention.id}> a sugar cookie. What, no frosting and sprinkles? 0/10 would not touch.`, );
`gives <@${mention.id}> a chocolate chip cookie. Oh wait, those are raisins. Bleck!`,
`gives <@${mention.id}> an enormous cookie. Poking it gives you more cookies. Weird.`,
`gives <@${mention.id}> a fortune cookie. It reads "Why aren't you working on any projects?"`,
`gives <@${mention.id}> a fortune cookie. It reads "Give that special someone a compliment"`,
`gives <@${mention.id}> a fortune cookie. It reads "Take a risk!"`,
`gives <@${mention.id}> a fortune cookie. It reads "Go outside."`,
`gives <@${mention.id}> a fortune cookie. It reads "Don't forget to eat your veggies!"`,
`gives <@${mention.id}> a fortune cookie. It reads "Do you even lift?"`,
`gives <@${mention.id}> a fortune cookie. It reads "m808 pls"`,
`gives <@${mention.id}> a fortune cookie. It reads "If you move your hips, you'll get all the ladies."`,
`gives <@${mention.id}> a fortune cookie. It reads "I love you."`,
`gives <@${mention.id}> a Golden Cookie. You can't eat it because it is made of gold. Dammit.`,
`gives <@${mention.id}> an Oreo cookie with a glass of milk!`,
`gives <@${mention.id}> a rainbow cookie made with love :heart:`,
`gives <@${mention.id}> an old cookie that was left out in the rain, it's moldy.`,
`bakes <@${mention.id}> fresh cookies, it smells amazing.`
];
if (mention.id == sender.id) return $.channel.send("You can't give yourself cookies!");
$.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

@ -1,26 +1,57 @@
/// @ts-nocheck
import {URL} from "url"; import {URL} from "url";
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: {sfw: {[key: string]: string}} = {
sfw: {
tickle: "/img/tickle",
slap: "/img/slap",
poke: "/img/poke",
pat: "/img/pat",
neko: "/img/neko",
meow: "/img/meow",
lizard: "/img/lizard",
kiss: "/img/kiss",
hug: "/img/hug",
foxGirl: "/img/fox_girl",
feed: "/img/feed",
cuddle: "/img/cuddle",
why: "/why",
catText: "/cat",
fact: "/fact",
nekoGif: "/img/ngif",
kemonomimi: "/img/kemonomimi",
holo: "/img/holo",
smug: "/img/smug",
baka: "/img/baka",
woof: "/img/woof",
spoiler: "/spoiler",
wallpaper: "/img/wallpaper",
goose: "/img/goose",
gecg: "/img/gecg",
avatar: "/img/avatar",
waifu: "/img/waifu"
}
};
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);
$.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(", ")}]\`.`
); );
}, },
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!"); const arg = $.args[0];
let baseURL = "https://nekos.life/api/v2"; if (!(arg in endpoints.sfw)) {
let url = new URL(`${baseURL}${endpoints.sfw[$.args[0]]}`); $.channel.send("Couldn't find that endpoint!");
return;
}
let url = new URL(`https://nekos.life/api/v2${endpoints.sfw[arg]}`);
const content = await getContent(url.toString()); const content = await getContent(url.toString());
$.channel.send(content.url); $.channel.send(content.url);
} }

View file

@ -1,9 +1,6 @@
import Command from "../../core/command"; import Command from "../../core/command";
import {CommonLibrary} from "../../core/lib"; import {random} from "../../core/lib";
export default new Command({
description: "Sends random ok message.",
async run($: CommonLibrary): Promise<any> {
const responses = [ const responses = [
"boomer", "boomer",
"zoomer", "zoomer",
@ -62,6 +59,9 @@ export default new Command({
"large man" "large man"
]; ];
$.channel.send("ok " + responses[Math.floor(Math.random() * responses.length)]); export default new Command({
description: "Sends random ok message.",
async run($) {
$.channel.send(`ok ${random(responses)}`);
} }
}); });

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";
@ -9,13 +10,13 @@ export const ShopCommand = new Command({
description: "Displays the list of items you can buy in the shop.", description: "Displays the list of items you can buy in the shop.",
async run({guild, channel, author}) { async run({guild, channel, author}) {
if (isAuthorized(guild, channel)) { if (isAuthorized(guild, channel)) {
function getShopEmbed(selection: ShopItem[], title = "Shop") { function getShopEmbed(selection: ShopItem[], title: string) {
const fields: EmbedField[] = []; const fields: EmbedField[] = [];
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
}); });
@ -31,19 +32,17 @@ export const ShopCommand = new Command({
}; };
} }
// In case there's just one page, omit unnecessary details. const shopPages = split(ShopItems, 5);
if (ShopItems.length <= 5) channel.send(getShopEmbed(ShopItems));
else {
const shopPages = $(ShopItems).split(5);
const pageAmount = shopPages.length; const pageAmount = shopPages.length;
const msg = await channel.send(getShopEmbed(shopPages[0], `Shop (Page 1 of ${pageAmount})`));
$.paginate(msg, author.id, pageAmount, (page) => { paginate(channel, author.id, pageAmount, (page, hasMultiplePages) => {
msg.edit(getShopEmbed(shopPages[page], `Shop (Page ${page + 1} of ${pageAmount})`)); return getShopEmbed(
shopPages[page],
hasMultiplePages ? `Shop (Page ${page + 1} of ${pageAmount})` : "Shop"
);
}); });
} }
} }
}
}); });
export const BuyCommand = new Command({ export const BuyCommand = new Command({

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: {
@ -62,9 +62,9 @@ export function getSendEmbed(sender: User, receiver: User, amount: number): obje
} }
export function isAuthorized(guild: Guild | null, channel: TextChannel | DMChannel | NewsChannel): boolean { export function isAuthorized(guild: Guild | null, channel: TextChannel | DMChannel | NewsChannel): boolean {
if (guild?.id === "637512823676600330" || process.argv[2] === "dev") return true; if ((guild?.id === "637512823676600330" && channel?.id === "669464416420364288") || IS_DEV_MODE) return true;
else { else {
channel.send("Sorry, this command can only be used in Monika's emote server."); channel.send("Sorry, this command can only be used in Monika's emote server. (#mon-stocks)");
return false; return false;
} }
} }

View file

@ -1,25 +1,27 @@
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 {getPermissionName} from "../core/permissions";
export default new Command({ 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") {
const command = commands.get(header); const command = commands.get(header);
if (!command) if (!command)
return $.warn(`Command "${header}" of category "${category}" unexpectedly doesn't exist!`); return console.warn(
`Command "${header}" of category "${category}" unexpectedly doesn't exist!`
);
output += `\n- \`${header}\`: ${command.description}`; output += `\n- \`${header}\`: ${command.description}`;
} }
@ -29,17 +31,20 @@ 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 $.warn(`originalCommandName isn't defined for ${header}?!`); else console.warn(`originalCommandName isn't defined for ${header}?!`);
let permLevel = command.permission ?? Command.PERMISSIONS.NONE; let permLevel = command.permission ?? 0;
let usage = command.usage; let usage = command.usage;
let invalid = false; let invalid = false;
@ -48,10 +53,10 @@ export default new Command({
for (const [category, headers] of categories) { for (const [category, headers] of categories) {
if (headers.includes(header)) { if (headers.includes(header)) {
if (selectedCategory !== "Unknown") if (selectedCategory !== "Unknown")
$.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);
} }
} }
@ -84,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 = "";
@ -127,7 +135,9 @@ export default new Command({
} }
$.channel.send( $.channel.send(
`Command: \`${header}\`\nAliases: ${aliases}\nCategory: \`${selectedCategory}\`\nPermission Required: \`${PermissionNames[permLevel]}\` (${permLevel})\nDescription: ${command.description}\n${append}`, `Command: \`${header}\`\nAliases: ${aliases}\nCategory: \`${selectedCategory}\`\nPermission Required: \`${getPermissionName(
permLevel
)}\` (${permLevel})\nDescription: ${command.description}\n${append}`,
{split: true} {split: true}
); );
} }

View file

@ -2,10 +2,11 @@ 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, getMemberByUsername} from "../core/libd";
import {verificationLevels, filterLevels, regions, flags} from "../defs/info"; import {verificationLevels, filterLevels, regions} from "../defs/info";
import moment from "moment"; import moment from "moment";
import utc from "moment"; import utc from "moment";
import {Guild} from "discord.js";
export default new Command({ export default new Command({
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.",
@ -14,12 +15,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,
@ -27,20 +28,33 @@ export default new Command({
}) })
); );
} }
}),
any: new Command({
description: "Shows another user's avatar by searching their name",
async run($) {
if ($.guild) {
const name = $.args.join(" ");
const member = await getMemberByUsername($.guild, name);
if (member) {
$.channel.send(
member.user.displayAvatarURL({
dynamic: true,
size: 2048
})
);
} else {
$.channel.send(`No user found by the name \`${name}\`!`);
}
}
}
}) })
}), }),
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()
.setThumbnail(
/// @ts-ignore
$.client.user?.displayAvatarURL({
dynamic: true,
size: 2048
})
)
.setColor($.guild?.me?.displayHexColor || "BLUE") .setColor($.guild?.me?.displayHexColor || "BLUE")
.addField("General", [ .addField("General", [
`** Client:** ${$.client.user?.tag} (${$.client.user?.id})`, `** Client:** ${$.client.user?.tag} (${$.client.user?.id})`,
@ -66,96 +80,72 @@ export default new Command({
`\u3000 • Speed: ${core.speed}MHz`, `\u3000 • Speed: ${core.speed}MHz`,
`** Memory:**`, `** Memory:**`,
`\u3000 • Total: ${formatBytes(process.memoryUsage().heapTotal)}`, `\u3000 • Total: ${formatBytes(process.memoryUsage().heapTotal)}`,
`\u3000 • Used: ${formatBytes(process.memoryUsage().heapTotal)}` `\u3000 • Used: ${formatBytes(process.memoryUsage().heapUsed)}`
]) ])
.setTimestamp(); .setTimestamp();
const avatarURL = $.client.user?.displayAvatarURL({
dynamic: true,
size: 2048
});
if (avatarURL) embed.setThumbnail(avatarURL);
$.channel.send(embed); $.channel.send(embed);
} }
}), }),
guild: new Command({ guild: new Command({
description: "Displays info about the current guild.", description: "Displays info about the current guild or another guild.",
async run($: CommonLibrary): Promise<any> { usage: "(<guild name>/<guild ID>)",
async run($) {
if ($.guild) { if ($.guild) {
const roles = $.guild.roles.cache $.channel.send(await getGuildInfo($.guild, $.guild));
.sort((a, b) => b.position - a.position)
.map((role) => role.toString());
const members = $.guild.members.cache;
const channels = $.guild.channels.cache;
const emojis = $.guild.emojis.cache;
const iconURL = $.guild.iconURL({dynamic: true});
const embed = new MessageEmbed()
.setDescription(`**Guild information for __${$.guild.name}__**`)
.setColor("BLUE");
if (iconURL)
embed
.setThumbnail(iconURL)
.addField("General", [
`** Name:** ${$.guild.name}`,
`** ID:** ${$.guild.id}`,
`** Owner:** ${$.guild.owner?.user.tag} (${$.guild.ownerID})`,
`** Region:** ${regions[$.guild.region]}`,
`** Boost Tier:** ${$.guild.premiumTier ? `Tier ${$.guild.premiumTier}` : "None"}`,
`** Explicit Filter:** ${filterLevels[$.guild.explicitContentFilter]}`,
`** Verification Level:** ${verificationLevels[$.guild.verificationLevel]}`,
`** Time Created:** ${moment($.guild.createdTimestamp).format("LT")} ${moment(
$.guild.createdTimestamp
).format("LL")} ${moment($.guild.createdTimestamp).fromNow()})`,
"\u200b"
])
.addField("Statistics", [
`** Role Count:** ${roles.length}`,
`** Emoji Count:** ${emojis.size}`,
`** Regular Emoji Count:** ${emojis.filter((emoji) => !emoji.animated).size}`,
`** Animated Emoji Count:** ${emojis.filter((emoji) => emoji.animated).size}`,
`** Member Count:** ${$.guild.memberCount}`,
`** Humans:** ${members.filter((member) => !member.user.bot).size}`,
`** Bots:** ${members.filter((member) => member.user.bot).size}`,
`** Text Channels:** ${channels.filter((channel) => channel.type === "text").size}`,
`** Voice Channels:** ${channels.filter((channel) => channel.type === "voice").size}`,
`** Boost Count:** ${$.guild.premiumSubscriptionCount || "0"}`,
`\u200b`
])
.addField("Presence", [
`** Online:** ${members.filter((member) => member.presence.status === "online").size}`,
`** Idle:** ${members.filter((member) => member.presence.status === "idle").size}`,
`** Do Not Disturb:** ${
members.filter((member) => member.presence.status === "dnd").size
}`,
`** Offline:** ${
members.filter((member) => member.presence.status === "offline").size
}`,
"\u200b"
])
.addField(
`Roles [${roles.length - 1}]`,
roles.length < 10 ? roles.join(", ") : roles.length > 10 ? trimArray(roles) : "None"
)
.setTimestamp();
$.channel.send(embed);
} else { } else {
$.channel.send("Please execute this command in a guild."); $.channel.send("Please execute this command in a guild.");
} }
},
any: new Command({
description: "Display info about a guild by finding its name or ID.",
async run($) {
// 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])) {
const id = $.args[0];
const guild = $.client.guilds.cache.get(id);
if (guild) {
$.channel.send(await getGuildInfo(guild, $.guild));
} else {
$.channel.send(`None of the servers I'm in matches the guild ID \`${id}\`!`);
} }
} else {
const query: string = $.args.join(" ").toLowerCase();
const guild = $.client.guilds.cache.find((guild) => guild.name.toLowerCase().includes(query));
if (guild) {
$.channel.send(await getGuildInfo(guild, $.guild));
} else {
$.channel.send(`None of the servers I'm in matches the query \`${query}\`!`);
}
}
}
})
}) })
}, },
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 = $.guild?.members.resolve($.args[0]) ?? (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)
.map((role: {toString: () => any}) => role.toString()) .map((role: {toString: () => any}) => role.toString())
.slice(0, -1); .slice(0, -1);
// @ts-ignore - Discord.js' typings seem to be outdated here. According to their v12 docs, it's User.fetchFlags() instead of User.flags. const userFlags = (await member.user.fetchFlags()).toArray();
const userFlags = ((await member.user.fetchFlags()) as UserFlags).toArray();
const embed = new MessageEmbed() const embed = new MessageEmbed()
.setThumbnail(member.user.displayAvatarURL({dynamic: true, size: 512})) .setThumbnail(member.user.displayAvatarURL({dynamic: true, size: 512}))
@ -188,3 +178,64 @@ export default new Command({
} }
}) })
}); });
async function getGuildInfo(guild: Guild, currentGuild: Guild | null) {
const members = await guild.members.fetch({
withPresences: true,
force: true
});
const roles = guild.roles.cache.sort((a, b) => b.position - a.position).map((role) => role.toString());
const channels = guild.channels.cache;
const emojis = guild.emojis.cache;
const iconURL = guild.iconURL({dynamic: true});
const embed = new MessageEmbed().setDescription(`**Guild information for __${guild.name}__**`).setColor("BLUE");
const displayRoles = !!(currentGuild && guild.id === currentGuild.id);
if (iconURL) {
embed
.setThumbnail(iconURL)
.addField("General", [
`** Name:** ${guild.name}`,
`** ID:** ${guild.id}`,
`** Owner:** ${guild.owner?.user.tag} (${guild.ownerID})`,
`** Region:** ${regions[guild.region]}`,
`** Boost Tier:** ${guild.premiumTier ? `Tier ${guild.premiumTier}` : "None"}`,
`** Explicit Filter:** ${filterLevels[guild.explicitContentFilter]}`,
`** Verification Level:** ${verificationLevels[guild.verificationLevel]}`,
`** Time Created:** ${moment(guild.createdTimestamp).format("LT")} ${moment(
guild.createdTimestamp
).format("LL")} ${moment(guild.createdTimestamp).fromNow()}`,
"\u200b"
])
.addField("Statistics", [
`** Role Count:** ${roles.length}`,
`** Emoji Count:** ${emojis.size}`,
`** Regular Emoji Count:** ${emojis.filter((emoji) => !emoji.animated).size}`,
`** Animated Emoji Count:** ${emojis.filter((emoji) => emoji.animated).size}`,
`** Member Count:** ${guild.memberCount}`,
`** Humans:** ${members.filter((member) => !member.user.bot).size}`,
`** Bots:** ${members.filter((member) => member.user.bot).size}`,
`** Text Channels:** ${channels.filter((channel) => channel.type === "text").size}`,
`** Voice Channels:** ${channels.filter((channel) => channel.type === "voice").size}`,
`** Boost Count:** ${guild.premiumSubscriptionCount || "0"}`,
`\u200b`
])
.addField("Presence", [
`** Online:** ${members.filter((member) => member.presence.status === "online").size}`,
`** Idle:** ${members.filter((member) => member.presence.status === "idle").size}`,
`** Do Not Disturb:** ${members.filter((member) => member.presence.status === "dnd").size}`,
`** Offline:** ${members.filter((member) => member.presence.status === "offline").size}`,
displayRoles ? "\u200b" : ""
])
.setTimestamp();
// Only add the roles if the guild the bot is sending the message to is the same one that's being requested.
if (displayRoles) {
embed.addField(
`Roles [${roles.length - 1}]`,
roles.length < 10 ? roles.join(", ") : roles.length > 10 ? trimArray(roles) : "None"
);
}
}
return embed;
}

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]: {
@ -131,7 +135,7 @@ export default new Command({
continueReactionLoop = false; continueReactionLoop = false;
if (reaction.count !== userReactions + botReactions) { if (reaction.count !== userReactions + botReactions) {
$.warn( console.warn(
`[Channel: ${channel.id}, Message: ${msg.id}] A reaction count of ${reaction.count} was expected but was given ${userReactions} user reactions and ${botReactions} bot reactions.` `[Channel: ${channel.id}, Message: ${msg.id}] A reaction count of ${reaction.count} was expected but was given ${userReactions} user reactions and ${botReactions} bot reactions.`
); );
warnings++; warnings++;
@ -155,13 +159,14 @@ 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"
)}.` )}.`
); );
$.log(`Finished operation in ${finishTime - startTime} ms.`); console.log(`Finished operation in ${finishTime - startTime} ms.`);
$.channel.stopTyping(); $.channel.stopTyping();
// Display stats on emote usage. // Display stats on emote usage.
@ -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,14 +1,13 @@
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:
'This is a template/testing command providing common functionality. Remove what you don\'t need, and rename/delete this file to generate a fresh command file here. This command should be automatically excluded from the help command. The "usage" parameter (string) overrides the default usage for the help command. The "endpoint" parameter (boolean) prevents further arguments from being passed. Also, as long as you keep the run function async, it\'ll return a promise allowing the program to automatically catch any synchronous errors. However, you\'ll have to do manual error handling if you go the then and catch route.', 'This is a template/testing command providing common functionality. Remove what you don\'t need, and rename/delete this file to generate a fresh command file here. This command should be automatically excluded from the help command. The "usage" parameter (string) overrides the default usage for the help command. The "endpoint" parameter (boolean) prevents further arguments from being passed. Also, as long as you keep the run function async, it\'ll return a promise allowing the program to automatically catch any synchronous errors. However, you\'ll have to do manual error handling if you go the then and catch route.',
endpoint: false, endpoint: false,
usage: "", usage: "",
permission: null, permission: -1,
aliases: [], aliases: [],
async run($: CommonLibrary): Promise<any> { async run($) {
// code // code
}, },
subcommands: { subcommands: {
@ -17,9 +16,9 @@ export default new Command({
'This is a named subcommand, meaning that the key name is what determines the keyword to use. With default settings for example, "$test layer".', 'This is a named subcommand, meaning that the key name is what determines the keyword to use. With default settings for example, "$test layer".',
endpoint: false, endpoint: false,
usage: "", usage: "",
permission: null, permission: -1,
aliases: [], aliases: [],
async run($: CommonLibrary): Promise<any> { async run($) {
// code // code
} }
}) })
@ -29,8 +28,8 @@ export default new Command({
'This is the subcommand for getting users by pinging them or copying their ID. With default settings for example, "$test 237359961842253835". The argument will be a user object and won\'t run if no user is found by that ID.', 'This is the subcommand for getting users by pinging them or copying their ID. With default settings for example, "$test 237359961842253835". The argument will be a user object and won\'t run if no user is found by that ID.',
endpoint: false, endpoint: false,
usage: "", usage: "",
permission: null, permission: -1,
async run($: CommonLibrary): Promise<any> { async run($) {
// code // code
} }
}), }),
@ -39,8 +38,8 @@ export default new Command({
'This is a numeric subcommand, meaning that any type of number (excluding Infinity/NaN) will route to this command if present. With default settings for example, "$test -5.2". The argument with the number is already parsed so you can just use it without converting it.', 'This is a numeric subcommand, meaning that any type of number (excluding Infinity/NaN) will route to this command if present. With default settings for example, "$test -5.2". The argument with the number is already parsed so you can just use it without converting it.',
endpoint: false, endpoint: false,
usage: "", usage: "",
permission: null, permission: -1,
async run($: CommonLibrary): Promise<any> { async run($) {
// code // code
} }
}), }),
@ -49,8 +48,8 @@ export default new Command({
"This is a generic subcommand, meaning that if there isn't a more specific subcommand that's called, it falls to this. With default settings for example, \"$test reeee\".", "This is a generic subcommand, meaning that if there isn't a more specific subcommand that's called, it falls to this. With default settings for example, \"$test reeee\".",
endpoint: false, endpoint: false,
usage: "", usage: "",
permission: null, permission: -1,
async run($: CommonLibrary): Promise<any> { async run($) {
// code // code
} }
}) })

View file

@ -1,23 +1,29 @@
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 (!$.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 changeVC = $.guild.channels.resolve(voiceChannel.id); const prevName = voiceChannel.name;
$.channel const newName = $.args.join(" ");
.send(`Changed channel name from "${voiceChannel}" to "${$.args.join(" ")}".`) await voiceChannel.setName(newName);
/// @ts-ignore await $.channel.send(`Changed channel name from "${prevName}" to "${newName}".`);
.then(changeVC?.setName($.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({
@ -12,7 +12,6 @@ export default new Command({
async run({guild, channel, message, args}) { async run({guild, channel, message, args}) {
let output = ""; let output = "";
for (const query of args) output += queryClosestEmoteByName(query).toString(); for (const query of args) output += queryClosestEmoteByName(query).toString();
if (botHasPermission(guild, Permissions.FLAGS.MANAGE_MESSAGES)) message.delete();
channel.send(output); channel.send(output);
} }
}) })

View file

@ -1,32 +1,114 @@
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 {TextChannel} from "discord.js";
import {DMChannel} from "discord.js";
import {NewsChannel} from "discord.js";
import {User} from "discord.js";
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,",
endpoint: true, usage: "<regex pattern> (-flags)",
async run($: CommonLibrary): Promise<any> { async run($) {
const nsfw: string | string[] = []; displayEmoteList($.client.emojis.cache.array(), $.channel, $.author);
const pages = $.client.emojis.cache.filter((x) => !nsfw.includes(x.guild.id), this).array(); },
const pagesSplit = $(pages).split(20); any: new Command({
$.log(pagesSplit); description:
var embed = new MessageEmbed().setTitle("**Emoji list!**").setColor("AQUA"); "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",
let desc = ""; async run($) {
// 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])) {
const guildID: string = $.args[0];
for (const emote of pagesSplit[0]) { displayEmoteList(
desc += `${emote} | ${emote.name}\n`; $.client.emojis.cache.filter((emote) => emote.guild.id === guildID).array(),
$.channel,
$.author
);
} else {
// Otherwise, search via a regex pattern
let flags: string | undefined = undefined;
if (/^-[dgimsuy]{1,7}$/.test($.args[$.args.length - 1])) {
flags = $.args.pop().substring(1);
} }
embed.setDescription(desc); let emoteCollection = $.client.emojis.cache.array();
const msg = await $.channel.send({embed}); // Creates a sandbox to stop a regular expression if it takes too much time to search.
// To avoid passing in a giant data structure, I'll just pass in the structure {[id: string]: [name: string]}.
//let emotes: {[id: string]: string} = {};
let emotes = new Map<string, string>();
for (const emote of emoteCollection) {
emotes.set(emote.id, emote.name);
}
// The result will be sandbox.emotes because it'll be modified in-place.
const sandbox = {
regex: new RegExp($.args.join(" "), flags),
emotes
};
const context = vm.createContext(sandbox);
if (vm.isContext(sandbox)) {
// Restrict an entire query to the timeout specified.
try {
const script = new vm.Script(
"for(const [id, name] of emotes.entries()) if(!regex.test(name)) emotes.delete(id);"
);
script.runInContext(context, {timeout: REGEX_TIMEOUT_MS});
emotes = sandbox.emotes;
emoteCollection = emoteCollection.filter((emote) => emotes.has(emote.id)); // Only allow emotes that haven't been deleted.
displayEmoteList(emoteCollection, $.channel, $.author);
} catch (error) {
if (error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") {
$.channel.send(
`The regular expression you entered exceeded the time limit of ${REGEX_TIMEOUT_MS} milliseconds.`
);
} else {
throw new Error(error);
}
}
} else {
$.channel.send("Failed to initialize sandbox.");
}
}
}
})
});
async function displayEmoteList(emotes: GuildEmoji[], channel: TextChannel | DMChannel | NewsChannel, author: User) {
emotes.sort((a, b) => {
const first = a.name.toLowerCase();
const second = b.name.toLowerCase();
if (first > second) return 1;
else if (first < second) return -1;
else return 0;
});
const sections = split(emotes, 20);
const pages = sections.length;
const embed = new MessageEmbed().setColor("AQUA");
// Gather the first page (if it even exists, which it might not if there no valid emotes appear)
if (pages > 0) {
paginate(channel, author.id, pages, (page, hasMultiplePages) => {
embed.setTitle(hasMultiplePages ? `**Emotes** (Page ${page + 1} of ${pages})` : "**Emotes**");
$.paginate(msg, $.author.id, pages.length, (page) => {
let desc = ""; let desc = "";
for (const emote of pagesSplit[page]) { for (const emote of sections[page]) {
desc += `${emote} | ${emote.name}\n`; desc += `${emote} ${emote.name} (**${emote.guild.name}**)\n`;
} }
embed.setDescription(desc); embed.setDescription(desc);
msg.edit(embed);
return embed;
}); });
} else {
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,26 @@
import $, {isType, parseVars, CommonLibrary} from "./lib"; import {parseVars} from "./libd";
import {Collection} from "discord.js"; import {Collection} from "discord.js";
import {PERMISSIONS} from "./permissions"; import {Client, Message, TextChannel, DMChannel, NewsChannel, Guild, User, GuildMember} from "discord.js";
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?: number;
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;
@ -29,22 +39,21 @@ export default class Command {
public readonly description: string; public readonly description: string;
public readonly endpoint: boolean; public readonly endpoint: boolean;
public readonly usage: string; public readonly usage: string;
public readonly permission: PERMISSIONS | null; public readonly permission: number; // -1 (default) indicates to inherit, 0 is the lowest rank, 1 is second lowest rank, and so on.
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;
public any: Command | null; public any: Command | null;
public static readonly TYPES = TYPES; public static readonly TYPES = TYPES;
public static readonly PERMISSIONS = PERMISSIONS;
constructor(options?: CommandOptions) { constructor(options?: CommandOptions) {
this.description = options?.description || "No description."; this.description = options?.description || "No description.";
this.endpoint = options?.endpoint || false; this.endpoint = options?.endpoint || false;
this.usage = options?.usage || ""; this.usage = options?.usage || "";
this.permission = options?.permission ?? null; this.permission = options?.permission ?? -1;
this.aliases = options?.aliases ?? []; this.aliases = options?.aliases ?? [];
this.originalCommandName = null; this.originalCommandName = null;
this.run = options?.run || "No action was set on this command!"; this.run = options?.run || "No action was set on this command!";
@ -68,11 +77,11 @@ export default class Command {
for (const alias of aliases) { for (const alias of aliases) {
if (baseSubcommands.includes(alias)) if (baseSubcommands.includes(alias))
$.warn( console.warn(
`"${alias}" in subcommand "${name}" was attempted to be declared as an alias but it already exists in the base commands! (Look at the next "Loading Command" line to see which command is affected.)` `"${alias}" in subcommand "${name}" was attempted to be declared as an alias but it already exists in the base commands! (Look at the next "Loading Command" line to see which command is affected.)`
); );
else if (this.subcommands.has(alias)) else if (this.subcommands.has(alias))
$.warn( console.warn(
`Duplicate alias "${alias}" at subcommand "${name}"! (Look at the next "Loading Command" line to see which command is affected.)` `Duplicate alias "${alias}" at subcommand "${name}"! (Look at the next "Loading Command" line to see which command is affected.)`
); );
else this.subcommands.set(alias, subcmd); else this.subcommands.set(alias, subcmd);
@ -81,26 +90,26 @@ export default class Command {
} }
if (this.user && this.user.aliases.length > 0) if (this.user && this.user.aliases.length > 0)
$.warn( console.warn(
`There are aliases defined for a "user"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.)` `There are aliases defined for a "user"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.)`
); );
if (this.number && this.number.aliases.length > 0) if (this.number && this.number.aliases.length > 0)
$.warn( console.warn(
`There are aliases defined for a "number"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.)` `There are aliases defined for a "number"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.)`
); );
if (this.any && this.any.aliases.length > 0) if (this.any && this.any.aliases.length > 0)
$.warn( console.warn(
`There are aliases defined for an "any"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.)` `There are aliases defined for an "any"-type subcommand, but those aliases won't be used. (Look at the next "Loading Command" line to see which command is affected.)`
); );
} }
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 +117,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 {
@ -179,7 +188,7 @@ export const loadableCommands = (async () => {
command.originalCommandName = commandName; command.originalCommandName = commandName;
if (commands.has(commandName)) { if (commands.has(commandName)) {
$.warn( console.warn(
`Command "${commandName}" already exists! Make sure to make each command uniquely identifiable across categories!` `Command "${commandName}" already exists! Make sure to make each command uniquely identifiable across categories!`
); );
} else { } else {
@ -188,7 +197,7 @@ export const loadableCommands = (async () => {
for (const alias of command.aliases) { for (const alias of command.aliases) {
if (commands.has(alias)) { if (commands.has(alias)) {
$.warn( console.warn(
`Top-level alias "${alias}" from command "${commandID}" already exists either as a command or alias!` `Top-level alias "${alias}" from command "${commandID}" already exists either as a command or alias!`
); );
} else { } else {
@ -199,9 +208,9 @@ export const loadableCommands = (async () => {
if (!(category in lists)) lists[category] = []; if (!(category in lists)) lists[category] = [];
lists[category].push(commandName); lists[category].push(commandName);
$.log(`Loading Command: ${commandID}`); console.log(`Loading Command: ${commandID}`);
} else { } else {
$.warn(`Command "${commandID}" has no default export which is a Command instance!`); console.warn(`Command "${commandID}" has no default export which is a Command instance!`);
} }
} }
} }
@ -224,3 +233,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,6 +1,5 @@
import {Client, ClientEvents, Constants} from "discord.js"; import {Client, ClientEvents, Constants} from "discord.js";
import Storage from "./storage"; import Storage from "./storage";
import $ from "./lib";
interface EventOptions<K extends keyof ClientEvents> { interface EventOptions<K extends keyof ClientEvents> {
readonly on?: (...args: ClientEvents[K]) => void; readonly on?: (...args: ClientEvents[K]) => void;
@ -30,9 +29,9 @@ export async function loadEvents(client: Client) {
if ((Object.values(Constants.Events) as string[]).includes(header)) { if ((Object.values(Constants.Events) as string[]).includes(header)) {
event.attach(client, header); event.attach(client, header);
$.log(`Loading Event: ${header}`); console.log(`Loading Event: ${header}`);
} else } else
$.warn( console.warn(
`"${header}" is not a valid event type! Did you misspell it? (Note: If you fixed the issue, delete "dist" because the compiler won't automatically delete any extra files.)` `"${header}" is not a valid event type! Did you misspell it? (Note: If you fixed the issue, delete "dist" because the compiler won't automatically delete any extra files.)`
); );
} }

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,568 +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 chalk from "chalk";
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;
log: (...args: any[]) => void;
warn: (...args: any[]) => void;
error: (...args: any[]) => void;
debug: (...args: any[]) => void;
ready: (...args: any[]) => 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
$.warn(
"No context was attached to $.handler! Make sure to use .catch($.handler.bind($)) or .catch(error => $.handler(error)) instead!"
);
$.error(error);
};
// Logs with different levels of verbosity.
export const logs: {[type: string]: string} = {
error: "",
warn: "",
info: "",
verbose: ""
};
let enabled = true;
export function setConsoleActivated(activated: boolean) {
enabled = activated;
}
// The custom console. In order of verbosity, error, warn, log, and debug. Ready is a variation of log.
// General Purpose Logger
$.log = (...args: any[]) => {
if (enabled) console.log(chalk.white.bgGray(formatTimestamp()), chalk.black.bgWhite("INFO"), ...args);
const text = `[${formatUTCTimestamp()}] [INFO] ${args.join(" ")}\n`;
logs.info += text;
logs.verbose += text;
};
// "It'll still work, but you should really check up on this."
$.warn = (...args: any[]) => {
if (enabled) console.warn(chalk.white.bgGray(formatTimestamp()), chalk.black.bgYellow("WARN"), ...args);
const text = `[${formatUTCTimestamp()}] [WARN] ${args.join(" ")}\n`;
logs.warn += text;
logs.info += text;
logs.verbose += text;
};
// Used for anything which prevents the program from actually running.
$.error = (...args: any[]) => {
if (enabled) console.error(chalk.white.bgGray(formatTimestamp()), chalk.white.bgRed("ERROR"), ...args);
const text = `[${formatUTCTimestamp()}] [ERROR] ${args.join(" ")}\n`;
logs.error += text;
logs.warn += text;
logs.info += text;
logs.verbose += text;
};
// Be as verbose as possible. If anything might help when debugging an error, then include it. This only shows in your console if you run this with "dev", but you can still get it from "logs.verbose".
// $.debug(`core/lib::parseArgs("testing \"in progress\"") = ["testing", "in progress"]`) --> <path>/::(<object>.)<function>(<args>) = <value>
// Would probably be more suited for debugging program logic rather than function logic, which can be checked using unit tests.
$.debug = (...args: any[]) => {
if (process.argv[2] === "dev" && enabled)
console.debug(chalk.white.bgGray(formatTimestamp()), chalk.white.bgBlue("DEBUG"), ...args);
const text = `[${formatUTCTimestamp()}] [DEBUG] ${args.join(" ")}\n`;
logs.verbose += text;
};
// Used once at the start of the program when the bot loads.
$.ready = (...args: any[]) => {
if (enabled) console.log(chalk.white.bgGray(formatTimestamp()), chalk.black.bgGreen("READY"), ...args);
const text = `[${formatUTCTimestamp()}] [READY] ${args.join(" ")}\n`;
logs.info += text;
logs.verbose += text;
};
export function formatTimestamp(now = new Date()) {
const year = now.getFullYear();
const month = (now.getMonth() + 1).toString().padStart(2, "0");
const day = now.getDate().toString().padStart(2, "0");
const hour = now.getHours().toString().padStart(2, "0");
const minute = now.getMinutes().toString().padStart(2, "0");
const second = now.getSeconds().toString().padStart(2, "0");
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}
export function formatUTCTimestamp(now = new Date()) {
const year = now.getUTCFullYear();
const month = (now.getUTCMonth() + 1).toString().padStart(2, "0");
const day = now.getUTCDate().toString().padStart(2, "0");
const hour = now.getUTCHours().toString().padStart(2, "0");
const minute = now.getUTCMinutes().toString().padStart(2, "0");
const second = now.getUTCSeconds().toString().padStart(2, "0");
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}
export function botHasPermission(guild: Guild | null, permission: number): boolean {
return !!(client.user && guild?.members.resolve(client.user)?.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 handle = (emote: string, reacterID: string) => {
switch (emote) {
case "⬅️":
turn(-1);
break;
case "➡️":
turn(1);
break;
}
};
// Listen for reactions and call the handler.
await message.react("⬅️");
await message.react("➡️");
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);
message.reactions.cache.get("⬅️")?.users.remove(message.author);
message.reactions.cache.get("➡️")?.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; export function replaceAll(text: string, before: string, after: string): string {
} else if (inVariable) token += c; while (text.indexOf(before) !== -1) text = text.replace(before, after);
else result += c; return text;
} }
return result; export function toTitleCase(text: string): string {
return text.replace(/([^\W_]+[^\s-]*) */g, (txt) => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase());
} }
export function isType(value: any, type: any): boolean { /** Returns a random element from this array. */
if (value === undefined && type === undefined) return true; export function random<T>(array: T[]): T {
else if (value === null && type === null) return true; return array[Math.floor(Math.random() * array.length)];
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. * 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 {
if (isType(value, type)) return value;
else return fallback;
}
}
export function clean(text: any) { for (let index = 0; index < amountOfSections; index++)
if (typeof text === "string") sections[index] = array.slice(index * lengthOfEachSection, (index + 1) * lengthOfEachSection);
return text.replace(/`/g, "`" + String.fromCharCode(8203)).replace(/@/g, "@" + String.fromCharCode(8203));
else return text;
}
export function trimArray(arr: any, maxLen = 10) { return sections;
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)
};

439
src/core/libd.ts Normal file
View file

@ -0,0 +1,439 @@
// Library for Discord-specific functions
import {
Message,
Guild,
GuildMember,
Permissions,
TextChannel,
DMChannel,
NewsChannel,
MessageOptions
} 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(
channel: TextChannel | DMChannel | NewsChannel,
senderID: string,
total: number,
callback: (page: number, hasMultiplePages: boolean) => MessageOptions & {split?: false},
duration = 60000
) {
const hasMultiplePages = total > 1;
const message = await channel.send(callback(0, hasMultiplePages));
if (hasMultiplePages) {
let page = 0;
const turn = (amount: number) => {
page += amount;
if (page < 0) page += total;
else if (page >= total) page -= total;
message.edit(callback(page, true));
};
const BACKWARDS_EMOJI = "⬅️";
const FORWARDS_EMOJI = "➡️";
const handle = (emote: string, reacterID: string) => {
if (senderID === reacterID) {
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);
const collector = message.createReactionCollector(
(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);
collector.resetTimer();
}
return false;
},
// Apparently, regardless of whether you put "time" or "idle", it won't matter to the collector.
// In order to actually reset the timer, you have to do it manually via collector.resetTimer().
{time: duration}
);
// When time's up, remove the bot's own reactions.
collector.on("end", () => {
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: string): Promise<{url: string}> {
return new Promise((resolve, reject) => {
get(url, (res) => {
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,76 +1,68 @@
import {GuildMember, Permissions} from "discord.js"; import {User, GuildMember, Permissions} from "discord.js";
import {Config} from "./structures"; import {Config} from "./structures";
import $ from "./lib";
export enum PERMISSIONS { interface PermissionLevel {
NONE, name: string;
MOD, check: (user: User, member: GuildMember | null) => boolean;
ADMIN,
OWNER,
BOT_SUPPORT,
BOT_ADMIN,
BOT_OWNER
} }
export const PermissionNames = [ export const PermissionLevels: PermissionLevel[] = [
"User", {
"Moderator",
"Administrator",
"Server Owner",
"Bot Support",
"Bot Admin",
"Bot Owner"
];
// Here is where you enter in the functions that check for permissions.
const PermissionChecker: ((member: GuildMember) => boolean)[] = [
// NONE // // NONE //
() => true, name: "User",
check: () => true
},
{
// MOD // // MOD //
(member) => name: "Moderator",
member.hasPermission(Permissions.FLAGS.MANAGE_ROLES) || check: (_, member) =>
!!member &&
(member.hasPermission(Permissions.FLAGS.MANAGE_ROLES) ||
member.hasPermission(Permissions.FLAGS.MANAGE_MESSAGES) || member.hasPermission(Permissions.FLAGS.MANAGE_MESSAGES) ||
member.hasPermission(Permissions.FLAGS.KICK_MEMBERS) || member.hasPermission(Permissions.FLAGS.KICK_MEMBERS) ||
member.hasPermission(Permissions.FLAGS.BAN_MEMBERS), member.hasPermission(Permissions.FLAGS.BAN_MEMBERS))
},
{
// ADMIN // // ADMIN //
(member) => member.hasPermission(Permissions.FLAGS.ADMINISTRATOR), name: "Administrator",
check: (_, member) => !!member && member.hasPermission(Permissions.FLAGS.ADMINISTRATOR)
},
{
// OWNER // // OWNER //
(member) => member.guild.ownerID === member.id, name: "Server Owner",
check: (_, member) => !!member && member.guild.ownerID === member.id
},
{
// BOT_SUPPORT // // BOT_SUPPORT //
(member) => Config.support.includes(member.id), name: "Bot Support",
check: (user) => Config.support.includes(user.id)
},
{
// BOT_ADMIN // // BOT_ADMIN //
(member) => Config.admins.includes(member.id), name: "Bot Admin",
check: (user) => Config.admins.includes(user.id)
},
{
// BOT_OWNER // // BOT_OWNER //
(member) => Config.owner === member.id name: "Bot Owner",
check: (user) => Config.owner === user.id
}
]; ];
// After checking the lengths of these three objects, use this as the length for consistency. // After checking the lengths of these three objects, use this as the length for consistency.
const length = Object.keys(PERMISSIONS).length / 2; const length = PermissionLevels.length;
export function hasPermission(member: GuildMember, permission: PERMISSIONS): boolean { export function hasPermission(member: GuildMember, permission: number): boolean {
for (let i = length - 1; i >= permission; i--) if (PermissionChecker[i](member)) return true; for (let i = length - 1; i >= permission; i--) if (PermissionLevels[i].check(member.user, member)) return true;
return false; return false;
} }
export function getPermissionLevel(member: GuildMember): number { export function getPermissionLevel(member: GuildMember): number {
for (let i = length - 1; i >= 0; i--) if (PermissionChecker[i](member)) return i; for (let i = length - 1; i >= 0; i--) if (PermissionLevels[i].check(member.user, member)) return i;
return 0; return 0;
} }
// Length Checking export function getPermissionName(level: number) {
(() => { if (level > length || length < 0) return "N/A";
const lenNames = PermissionNames.length; else return PermissionLevels[level].name;
const lenChecker = PermissionChecker.length; }
// By transitive property, lenNames and lenChecker have to be equal to each other as well.
if (length !== lenNames || length !== lenChecker)
$.error(
`Permission object lengths aren't equal! Enum Length (${length}), Names Length (${lenNames}), and Functions Length (${lenChecker}). This WILL cause problems!`
);
})();

View file

@ -1,5 +1,4 @@
import fs from "fs"; import fs from "fs";
import $ from "./lib";
const Storage = { const Storage = {
read(header: string): object { read(header: string): object {
@ -14,7 +13,7 @@ const Storage = {
data = JSON.parse(file); data = JSON.parse(file);
} catch (error) { } catch (error) {
if (process.argv[2] !== "dev") { if (process.argv[2] !== "dev") {
$.warn(`Malformed JSON data (header: ${header}), backing it up.`, file); console.warn(`Malformed JSON data (header: ${header}), backing it up.`, file);
fs.writeFile( fs.writeFile(
`${path}.backup`, `${path}.backup`,
file, file,
@ -30,7 +29,7 @@ const Storage = {
this.open("data"); this.open("data");
const path = `data/${header}.json`; const path = `data/${header}.json`;
if (process.argv[2] === "dev" || header === "config") { if (IS_DEV_MODE || header === "config") {
const result = JSON.stringify(data, null, "\t"); const result = JSON.stringify(data, null, "\t");
if (asynchronous) if (asynchronous)
@ -60,8 +59,8 @@ const Storage = {
export function generateHandler(message: string) { export function generateHandler(message: string) {
return (error: Error | null) => { return (error: Error | null) => {
if (error) $.error(error); if (error) console.error(error);
else $.debug(message); else console.debug(message);
}; };
} }

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";
@ -63,7 +63,7 @@ class StorageStructure extends GenericStructure {
/** Gets a user's profile if they exist and generate one if not. */ /** Gets a user's profile if they exist and generate one if not. */
public getUser(id: string): User { public getUser(id: string): User {
if (!/\d{17,19}/g.test(id)) if (!/\d{17,19}/g.test(id))
$.warn(`"${id}" is not a valid user ID! It will be erased when the data loads again.`); console.warn(`"${id}" is not a valid user ID! It will be erased when the data loads again.`);
if (id in this.users) return this.users[id]; if (id in this.users) return this.users[id];
else { else {
@ -76,7 +76,7 @@ class StorageStructure extends GenericStructure {
/** Gets a guild's settings if they exist and generate one if not. */ /** Gets a guild's settings if they exist and generate one if not. */
public getGuild(id: string): Guild { public getGuild(id: string): Guild {
if (!/\d{17,19}/g.test(id)) if (!/\d{17,19}/g.test(id))
$.warn(`"${id}" is not a valid guild ID! It will be erased when the data loads again.`); console.warn(`"${id}" is not a valid guild ID! It will be erased when the data loads again.`);
if (id in this.guilds) return this.guilds[id]; if (id in this.guilds) return this.guilds[id];
else { else {
@ -93,9 +93,9 @@ export let Storage = new StorageStructure(FileManager.read("storage"));
// This part will allow the user to manually edit any JSON files they want while the program is running which'll update the program's cache. // This part will allow the user to manually edit any JSON files they want while the program is running which'll update the program's cache.
// However, fs.watch is a buggy mess that should be avoided in production. While it helps test out stuff for development, it's not a good idea to have it running outside of development as it causes all sorts of issues. // However, fs.watch is a buggy mess that should be avoided in production. While it helps test out stuff for development, it's not a good idea to have it running outside of development as it causes all sorts of issues.
if (process.argv[2] === "dev") { if (IS_DEV_MODE) {
watch("data", (event, filename) => { watch("data", (event, filename) => {
$.debug("File Watcher:", event, filename); console.debug("File Watcher:", event, filename);
const header = filename.substring(0, filename.indexOf(".json")); const header = filename.substring(0, filename.indexOf(".json"));
switch (header) { switch (header) {

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,6 +1,5 @@
import Event from "../core/event"; import Event from "../core/event";
import {client} from "../index"; import {client} from "../index";
import $ from "../core/lib";
import * as discord from "discord.js"; import * as discord from "discord.js";
export default new Event<"channelCreate">({ export default new Event<"channelCreate">({
@ -8,7 +7,7 @@ export default new Event<"channelCreate">({
const botGuilds = client.guilds; const botGuilds = client.guilds;
if (channel instanceof discord.GuildChannel) { if (channel instanceof discord.GuildChannel) {
const createdGuild = await botGuilds.fetch(channel.guild.id); const createdGuild = await botGuilds.fetch(channel.guild.id);
$.log(`Channel created in '${createdGuild.name}' called '#${channel.name}'`); console.log(`Channel created in '${createdGuild.name}' called '#${channel.name}'`);
} }
} }
}); });

View file

@ -1,6 +1,5 @@
import Event from "../core/event"; import Event from "../core/event";
import {client} from "../index"; import {client} from "../index";
import $ from "../core/lib";
import * as discord from "discord.js"; import * as discord from "discord.js";
export default new Event<"channelDelete">({ export default new Event<"channelDelete">({
@ -8,7 +7,7 @@ export default new Event<"channelDelete">({
const botGuilds = client.guilds; const botGuilds = client.guilds;
if (channel instanceof discord.GuildChannel) { if (channel instanceof discord.GuildChannel) {
const createdGuild = await botGuilds.fetch(channel.guild.id); const createdGuild = await botGuilds.fetch(channel.guild.id);
$.log(`Channel deleted in '${createdGuild.name}' called '#${channel.name}'`); console.log(`Channel deleted in '${createdGuild.name}' called '#${channel.name}'`);
} }
} }
}); });

View file

@ -1,10 +1,9 @@
import Event from "../core/event"; import Event from "../core/event";
import $ from "../core/lib"; import {updateGlobalEmoteRegistry} from "../core/libd";
import {updateGlobalEmoteRegistry} from "../core/lib";
export default new Event<"emojiCreate">({ export default new Event<"emojiCreate">({
on(emote) { on(emote) {
$.log(`Updated emote registry. ${emote.name}`); console.log(`Updated emote registry. ${emote.name}`);
updateGlobalEmoteRegistry(); updateGlobalEmoteRegistry();
} }
}); });

View file

@ -1,10 +1,9 @@
import Event from "../core/event"; import Event from "../core/event";
import $ from "../core/lib"; import {updateGlobalEmoteRegistry} from "../core/libd";
import {updateGlobalEmoteRegistry} from "../core/lib";
export default new Event<"emojiDelete">({ export default new Event<"emojiDelete">({
on() { on() {
$.log("Updated emote registry."); console.log("Updated emote registry.");
updateGlobalEmoteRegistry(); updateGlobalEmoteRegistry();
} }
}); });

View file

@ -1,10 +1,9 @@
import Event from "../core/event"; import Event from "../core/event";
import $ from "../core/lib"; import {updateGlobalEmoteRegistry} from "../core/libd";
import {updateGlobalEmoteRegistry} from "../core/lib";
export default new Event<"emojiUpdate">({ export default new Event<"emojiUpdate">({
on() { on() {
$.log("Updated emote registry."); console.log("Updated emote registry.");
updateGlobalEmoteRegistry(); updateGlobalEmoteRegistry();
} }
}); });

View file

@ -1,10 +1,9 @@
import Event from "../core/event"; import Event from "../core/event";
import $ from "../core/lib"; import {updateGlobalEmoteRegistry} from "../core/libd";
import {updateGlobalEmoteRegistry} from "../core/lib";
export default new Event<"guildCreate">({ export default new Event<"guildCreate">({
on() { on() {
$.log("Updated emote registry."); console.log("Updated emote registry.");
updateGlobalEmoteRegistry(); updateGlobalEmoteRegistry();
} }
}); });

View file

@ -1,10 +1,9 @@
import Event from "../core/event"; import Event from "../core/event";
import $ from "../core/lib"; import {updateGlobalEmoteRegistry} from "../core/libd";
import {updateGlobalEmoteRegistry} from "../core/lib";
export default new Event<"guildDelete">({ export default new Event<"guildDelete">({
on() { on() {
$.log("Updated emote registry."); console.log("Updated emote registry.");
updateGlobalEmoteRegistry(); updateGlobalEmoteRegistry();
} }
}); });

View file

@ -1,14 +1,19 @@
import Event from "../core/event"; import Event from "../core/event";
import Command, {loadableCommands} from "../core/command"; import Command, {loadableCommands} from "../core/command";
import {hasPermission, getPermissionLevel, PermissionNames} from "../core/permissions"; import {hasPermission, getPermissionLevel, getPermissionName} 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";
export default new Event<"message">({ export default new Event<"message">({
async on(message) { async on(message) {
const commands = await loadableCommands; const commands = await loadableCommands;
if (message.content.toLowerCase().includes("remember to drink water")) {
message.react("🚱");
}
// Message Setup // // Message Setup //
if (message.author.bot) return; if (message.author.bot) return;
@ -24,6 +29,10 @@ export default new Event<"message">({
const clientUser = message.client.user; const clientUser = message.client.user;
let usesBotSpecificPrefix = false; let usesBotSpecificPrefix = false;
if (!message.content.startsWith(prefix)) {
return quote(message);
}
// If the client user exists, check if it starts with the bot-specific prefix. // If the client user exists, check if it starts with the bot-specific prefix.
if (clientUser) { if (clientUser) {
// If the prefix starts with the bot-specific prefix, go off that instead (these two options must mutually exclude each other). // If the prefix starts with the bot-specific prefix, go off that instead (these two options must mutually exclude each other).
@ -73,21 +82,21 @@ export default new Event<"message">({
); );
} }
$.log( console.log(
`${message.author.username}#${message.author.discriminator} executed the command "${header}" with arguments "${args}".` `${message.author.username}#${message.author.discriminator} executed the command "${header}" with arguments "${args}".`
); );
// Subcommand Recursion // // Subcommand Recursion //
let command = commands.get(header); let command = commands.get(header);
if (!command) return $.warn(`Command "${header}" was called but for some reason it's still undefined!`); if (!command) return console.warn(`Command "${header}" was called but for some reason it's still undefined!`);
const params: any[] = []; const params: any[] = [];
let isEndpoint = false; let isEndpoint = false;
let permLevel = command.permission ?? Command.PERMISSIONS.NONE; let permLevel = command.permission ?? 0;
for (let param of args) { for (let param of args) {
if (command.endpoint) { if (command.endpoint) {
if (command.subcommands.size > 0 || command.user || command.number || command.any) if (command.subcommands.size > 0 || command.user || command.number || command.any)
$.warn(`An endpoint cannot have subcommands! Check ${originalPrefix}${header} again.`); console.warn(`An endpoint cannot have subcommands! Check ${originalPrefix}${header} again.`);
isEndpoint = true; isEndpoint = true;
break; break;
} }
@ -108,12 +117,16 @@ export default new Event<"message">({
} }
if (!message.member) if (!message.member)
return $.warn("This command was likely called from a DM channel meaning the member object is null."); return console.warn("This command was likely called from a DM channel meaning the member object is null.");
if (!hasPermission(message.member, permLevel)) { if (!hasPermission(message.member, permLevel)) {
const userPermLevel = getPermissionLevel(message.member); const userPermLevel = getPermissionLevel(message.member);
return message.channel.send( return message.channel.send(
`You don't have access to this command! Your permission level is \`${PermissionNames[userPermLevel]}\` (${userPermLevel}), but this command requires a permission level of \`${PermissionNames[permLevel]}\` (${permLevel}).` `You don't have access to this command! Your permission level is \`${getPermissionName(
userPermLevel
)}\` (${userPermLevel}), but this command requires a permission level of \`${getPermissionName(
permLevel
)}\` (${permLevel}).`
); );
} }
@ -123,10 +136,7 @@ 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(
$.bind($),
{
args: params, args: params,
author: message.author, author: message.author,
channel: message.channel, channel: message.channel,
@ -134,9 +144,6 @@ export default new Event<"message">({
guild: message.guild, guild: message.guild,
member: message.member, member: message.member,
message: message 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,13 +1,12 @@
import Event from "../core/event"; import Event from "../core/event";
import {client} from "../index"; import {client} from "../index";
import $ from "../core/lib";
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() {
if (client.user) { if (client.user) {
$.ready(`Logged in as ${client.user.username}#${client.user.discriminator}.`); console.ready(`Logged in as ${client.user.username}#${client.user.discriminator}.`);
client.user.setActivity({ client.user.setActivity({
type: "LISTENING", type: "LISTENING",
name: `${Config.prefix}help` name: `${Config.prefix}help`

98
src/globals.ts Normal file
View file

@ -0,0 +1,98 @@
import chalk from "chalk";
declare global {
var IS_DEV_MODE: boolean;
var PERMISSIONS: typeof PermissionsEnum;
interface Console {
ready: (...data: any[]) => void;
}
}
enum PermissionsEnum {
NONE,
MOD,
ADMIN,
OWNER,
BOT_SUPPORT,
BOT_ADMIN,
BOT_OWNER
}
global.IS_DEV_MODE = process.argv[2] === "dev";
global.PERMISSIONS = PermissionsEnum;
const oldConsole = console;
export const logs: {[type: string]: string} = {
error: "",
warn: "",
info: "",
verbose: ""
};
function formatTimestamp(now = new Date()) {
const year = now.getFullYear();
const month = (now.getMonth() + 1).toString().padStart(2, "0");
const day = now.getDate().toString().padStart(2, "0");
const hour = now.getHours().toString().padStart(2, "0");
const minute = now.getMinutes().toString().padStart(2, "0");
const second = now.getSeconds().toString().padStart(2, "0");
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}
function formatUTCTimestamp(now = new Date()) {
const year = now.getUTCFullYear();
const month = (now.getUTCMonth() + 1).toString().padStart(2, "0");
const day = now.getUTCDate().toString().padStart(2, "0");
const hour = now.getUTCHours().toString().padStart(2, "0");
const minute = now.getUTCMinutes().toString().padStart(2, "0");
const second = now.getUTCSeconds().toString().padStart(2, "0");
return `${year}-${month}-${day} ${hour}:${minute}:${second}`;
}
// The custom console. In order of verbosity, error, warn, log, and debug. Ready is a variation of log.
console = {
...oldConsole,
// General Purpose Logger
log(...args: any[]) {
oldConsole.log(chalk.white.bgGray(formatTimestamp()), chalk.black.bgWhite("INFO"), ...args);
const text = `[${formatUTCTimestamp()}] [INFO] ${args.join(" ")}\n`;
logs.info += text;
logs.verbose += text;
},
// "It'll still work, but you should really check up on this."
warn(...args: any[]) {
oldConsole.warn(chalk.white.bgGray(formatTimestamp()), chalk.black.bgYellow("WARN"), ...args);
const text = `[${formatUTCTimestamp()}] [WARN] ${args.join(" ")}\n`;
logs.warn += text;
logs.info += text;
logs.verbose += text;
},
// Used for anything which prevents the program from actually running.
error(...args: any[]) {
oldConsole.error(chalk.white.bgGray(formatTimestamp()), chalk.white.bgRed("ERROR"), ...args);
const text = `[${formatUTCTimestamp()}] [ERROR] ${args.join(" ")}\n`;
logs.error += text;
logs.warn += text;
logs.info += text;
logs.verbose += text;
},
// Be as verbose as possible. If anything might help when debugging an error, then include it. This only shows in your console if you run this with "dev", but you can still get it from "logs.verbose".
// $.debug(`core/lib::parseArgs("testing \"in progress\"") = ["testing", "in progress"]`) --> <path>/::(<object>.)<function>(<args>) = <value>
// Would probably be more suited for debugging program logic rather than function logic, which can be checked using unit tests.
debug(...args: any[]) {
if (IS_DEV_MODE) oldConsole.debug(chalk.white.bgGray(formatTimestamp()), chalk.white.bgBlue("DEBUG"), ...args);
const text = `[${formatUTCTimestamp()}] [DEBUG] ${args.join(" ")}\n`;
logs.verbose += text;
},
// Used once at the start of the program when the bot loads.
ready(...args: any[]) {
oldConsole.log(chalk.white.bgGray(formatTimestamp()), chalk.black.bgGreen("READY"), ...args);
const text = `[${formatUTCTimestamp()}] [READY] ${args.join(" ")}\n`;
logs.info += text;
logs.verbose += text;
}
};
console.log("Loading globals...");

View file

@ -1,13 +1,42 @@
import {Client} from "discord.js"; import "./globals";
import * as discord from "discord.js";
import setup from "./setup"; import setup from "./setup";
import {Config} from "./core/structures"; import {Config} from "./core/structures";
import {loadEvents} from "./core/event"; import {loadEvents} from "./core/event";
import "discord.js-lavalink-lib"; import "discord.js-lavalink-lib";
import LavalinkMusic from "discord.js-lavalink-lib"; import LavalinkMusic from "discord.js-lavalink-lib";
declare module "discord.js" {
interface Presence {
patch(data: any): void;
}
}
// The terrible hacks were written by none other than The Noble Programmer On The White PC.
// NOTE: Terrible hack ahead!!! In order to reduce the memory usage of the bot
// we only store the information from presences that we actually end up using,
// which currently is only the (online/idle/dnd/offline/...) status (see
// `src/commands/info.ts`). What data is retrieved from the `data` object
// (which contains the data received from the Gateway) and how can be seen
// here:
// <https://github.com/discordjs/discord.js/blob/cee6cf70ce76e9b06dc7f25bfd77498e18d7c8d4/src/structures/Presence.js#L81-L110>.
const oldPresencePatch = discord.Presence.prototype.patch;
discord.Presence.prototype.patch = function patch(data: any) {
oldPresencePatch.call(this, {status: data.status});
};
// This is here in order to make it much less of a headache to access the client from other files. // This is here in order to make it much less of a headache to access the client from other files.
// This of course won't actually do anything until the setup process is complete and it logs in. // This of course won't actually do anything until the setup process is complete and it logs in.
export const client = new Client(); export const client = new discord.Client();
// NOTE: Terrible hack continued!!! Unfortunately we can't receive the presence
// data at all when the GUILD_PRESENCES intent is disabled, so while we do
// waste network bandwidth and the CPU time for decoding the incoming packets,
// the function which handles those packets is NOP-ed out, which, among other
// things, skips the code which caches the referenced users in the packet. See
// <https://github.com/discordjs/discord.js/blob/cee6cf70ce76e9b06dc7f25bfd77498e18d7c8d4/src/client/actions/PresenceUpdate.js#L7-L41>.
(client["actions"] as any)["PresenceUpdate"].handle = () => {};
(client as any).music = LavalinkMusic(client, { (client as any).music = LavalinkMusic(client, {
lavalink: { lavalink: {
@ -24,7 +53,7 @@ export const client = new Client();
} }
] ]
}, },
prefix: "!!", prefix: Config.prefix,
helpCmd: "mhelp", helpCmd: "mhelp",
admins: ["717352467280691331"] admins: ["717352467280691331"]
}); });

View file

@ -0,0 +1,61 @@
import {client} from "..";
import {Message, TextChannel, APIMessage, MessageEmbed} from "discord.js";
import {getPrefix} from "../core/structures";
import {DiscordAPIError} from "discord.js";
export default async function quote(message: Message) {
if (message.author.bot) return;
// const message_link_regex = message.content.match(/(!)?https?:\/\/\w+\.com\/channels\/(\d+)\/(\d+)\/(\d+)/)
const message_link_regex = message.content.match(
/([<!]?)https?:\/\/(?:ptb\.|canary\.|)discord(?:app)?\.com\/channels\/(\d+)\/(\d+)\/(\d+)(>?)/
);
if (message_link_regex == null) return;
const [, char, guildID, channelID, messageID] = message_link_regex;
if (char || message.content.startsWith(getPrefix(message.guild))) return;
try {
const channel = client.guilds.cache.get(guildID)?.channels.cache.get(channelID) as TextChannel;
const link_message = await channel.messages.fetch(messageID);
let rtmsg: string | APIMessage = "";
if (link_message.cleanContent) {
rtmsg = new APIMessage(message.channel as TextChannel, {
content: link_message.cleanContent,
disableMentions: "all",
files: link_message.attachments.array()
});
}
const embeds = [...link_message.embeds.filter((v) => v.type == "rich"), ...link_message.attachments.values()];
/// @ts-ignore
if (!link_message.cleanContent && embeds.empty) {
const Embed = new MessageEmbed().setDescription("🚫 The message is empty.");
return message.channel.send(Embed);
}
const infoEmbed = new MessageEmbed()
.setAuthor(
link_message.author.username,
link_message.author.displayAvatarURL({format: "png", dynamic: true, size: 4096})
)
.setTimestamp(link_message.createdTimestamp)
.setDescription(
`${link_message.cleanContent}\n\nSent in **${link_message.guild?.name}** | <#${link_message.channel.id}> ([link](https://discord.com/channels/${guildID}/${channelID}/${messageID}))`
);
if (link_message.attachments.size !== 0) {
const image = link_message.attachments.first();
/// @ts-ignore
infoEmbed.setImage(image.url);
}
await message.channel.send(infoEmbed);
} catch (error) {
if (error instanceof DiscordAPIError) {
message.channel.send("I don't have access to this channel, or something else went wrong.");
}
return console.error(error);
}
}

View file

@ -2,12 +2,11 @@ import {existsSync as exists, readFileSync as read, writeFile as write} from "fs
import inquirer from "inquirer"; import inquirer from "inquirer";
import Storage, {generateHandler} from "./core/storage"; import Storage, {generateHandler} from "./core/storage";
import {Config} from "./core/structures"; import {Config} from "./core/structures";
import $, {setConsoleActivated} from "./core/lib";
// The template should be built with a reductionist mentality. // The template should be built with a reductionist mentality.
// Provide everything the user needs and then let them remove whatever they want. // Provide everything the user needs and then let them remove whatever they want.
// That way, they aren't focusing on what's missing, but rather what they need for their command. // That way, they aren't focusing on what's missing, but rather what they need for their command.
if (process.argv[2] === "dev" && !exists("src/commands/test.ts")) { if (IS_DEV_MODE && !exists("src/commands/test.ts")) {
write( write(
"src/commands/test.ts", "src/commands/test.ts",
read("src/commands/template.ts"), read("src/commands/template.ts"),
@ -64,8 +63,19 @@ export default {
}, },
/** Prompt the user to set their token again. */ /** Prompt the user to set their token again. */
async again() { async again() {
$.error("It seems that the token you provided is invalid."); console.error("It seems that the token you provided is invalid.");
setConsoleActivated(false);
// Deactivate the console //
const oldConsole = console;
console = {
...oldConsole,
log() {},
warn() {},
error() {},
debug() {},
ready() {}
};
const answers = await inquirer.prompt(prompts.slice(0, 1)); const answers = await inquirer.prompt(prompts.slice(0, 1));
Config.token = answers.token as string; Config.token = answers.token as string;
Config.save(false); Config.save(false);