Separated command handler from utility modules and fixed lingering errors in commands

This commit is contained in:
WatDuhHekBro 2021-04-04 23:35:12 -05:00
parent 6ed4c0988f
commit 5c3896c2db
38 changed files with 344 additions and 407 deletions

View File

@ -1,5 +1,5 @@
import Command from "../../core/command"; import {Command, NamedCommand} from "../../core";
import {random} from "../../core/lib"; import {random} from "../../lib";
const responses = [ const responses = [
"Most likely,", "Most likely,",
@ -24,16 +24,16 @@ const responses = [
"Very doubtful," "Very doubtful,"
]; ];
export default new Command({ export default new NamedCommand({
description: "Answers your question in an 8-ball manner.", description: "Answers your question in an 8-ball manner.",
endpoint: false, endpoint: false,
usage: "<question>", usage: "<question>",
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($) { async run({message, channel, guild, author, member, client, args}) {
const sender = $.message.author; const sender = message.author;
$.channel.send(`${random(responses)} <@${sender.id}>`); channel.send(`${random(responses)} <@${sender.id}>`);
} }
}) })
}); });

View File

@ -1,7 +1,6 @@
import {User} from "discord.js"; import {User} from "discord.js";
import Command from "../../core/command"; import {Command, NamedCommand} from "../../core";
import {random} from "../../core/lib"; import {random, parseVars} from "../../lib";
import {parseVars} from "../../core/lib";
const cookies = [ const cookies = [
`has given %target% a chocolate chip cookie!`, `has given %target% a chocolate chip cookie!`,
@ -26,29 +25,29 @@ const cookies = [
`bakes %target% fresh cookies, it smells amazing.` `bakes %target% fresh cookies, it smells amazing.`
]; ];
export default new Command({ export default new NamedCommand({
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!",
subcommands: { subcommands: {
all: new Command({ all: new NamedCommand({
async run($) { async run({message, channel, guild, author, member, client, args}) {
$.channel.send(`${$.author} gave everybody a cookie!`); 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($) { async run({message, channel, guild, author, member, client, args}) {
const sender = $.author; const sender = author;
const mention: User = $.args[0]; const mention: User = args[0];
if (mention.id == sender.id) { if (mention.id == sender.id) {
$.channel.send("You can't give yourself cookies!"); channel.send("You can't give yourself cookies!");
return; return;
} }
$.channel.send( channel.send(
`:cookie: <@${sender.id}> ${parseVars(random(cookies), { `:cookie: <@${sender.id}> ${parseVars(random(cookies), {
target: mention.toString() target: mention.toString()
})}` })}`

View File

@ -1,11 +1,10 @@
import Command from "../../core/command"; import {Command, NamedCommand, callMemberByUsername} from "../../core";
import {isAuthorized, getMoneyEmbed} from "./modules/eco-utils"; import {isAuthorized, getMoneyEmbed} from "./modules/eco-utils";
import {DailyCommand, PayCommand, GuildCommand, LeaderboardCommand} from "./modules/eco-core"; import {DailyCommand, PayCommand, GuildCommand, LeaderboardCommand} from "./modules/eco-core";
import {BuyCommand, ShopCommand} from "./modules/eco-shop"; import {BuyCommand, ShopCommand} from "./modules/eco-shop";
import {MondayCommand} from "./modules/eco-extras"; import {MondayCommand} from "./modules/eco-extras";
import {callMemberByUsername} from "../../core/libd";
export default new Command({ export default new NamedCommand({
description: "Economy command for Monika.", description: "Economy command for Monika.",
async run({guild, channel, author}) { async run({guild, channel, author}) {
if (isAuthorized(guild, channel)) channel.send(getMoneyEmbed(author)); if (isAuthorized(guild, channel)) channel.send(getMoneyEmbed(author));

View File

@ -1,10 +1,9 @@
import Command from "../../../core/command"; import {Command, NamedCommand, prompt} from "../../../core";
import {prompt} from "../../../core/libd"; import {pluralise} from "../../../lib";
import {pluralise} from "../../../core/lib"; import {Storage} from "../../../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";
export const DailyCommand = new Command({ export const DailyCommand = new NamedCommand({
description: "Pick up your daily Mons. The cooldown is per user and every 22 hours to allow for some leeway.", description: "Pick up your daily Mons. The cooldown is per user and every 22 hours to allow for some leeway.",
aliases: ["get"], aliases: ["get"],
async run({author, channel, guild}) { async run({author, channel, guild}) {
@ -38,7 +37,7 @@ export const DailyCommand = new Command({
} }
}); });
export const GuildCommand = new Command({ export const GuildCommand = new NamedCommand({
description: "Get info on the guild's economy as a whole.", description: "Get info on the guild's economy as a whole.",
async run({guild, channel}) { async run({guild, channel}) {
if (isAuthorized(guild, channel)) { if (isAuthorized(guild, channel)) {
@ -75,7 +74,7 @@ export const GuildCommand = new Command({
} }
}); });
export const LeaderboardCommand = new Command({ export const LeaderboardCommand = new NamedCommand({
description: "See the richest players.", description: "See the richest players.",
aliases: ["top"], aliases: ["top"],
async run({guild, channel, client}) { async run({guild, channel, client}) {
@ -109,7 +108,7 @@ export const LeaderboardCommand = new Command({
} }
}); });
export const PayCommand = new Command({ export const PayCommand = new NamedCommand({
description: "Send money to someone.", description: "Send money to someone.",
usage: "<user> <amount>", usage: "<user> <amount>",
run: "Who are you sending this money to?", run: "Who are you sending this money to?",

View File

@ -1,10 +1,10 @@
import Command from "../../../core/command"; import {Command, NamedCommand} from "../../../core/command";
import {Storage} from "../../../core/structures"; import {Storage} from "../../../structures";
import {isAuthorized, getMoneyEmbed} from "./eco-utils"; import {isAuthorized, getMoneyEmbed} from "./eco-utils";
const WEEKDAY = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"]; const WEEKDAY = ["Sunday", "Monday", "Tuesday", "Wednesday", "Thursday", "Friday", "Saturday"];
export const MondayCommand = new Command({ export const MondayCommand = new NamedCommand({
description: "Use this on a UTC Monday to get an extra Mon. Does not affect your 22 hour timer for `eco daily`.", description: "Use this on a UTC Monday to get an extra Mon. Does not affect your 22 hour timer for `eco daily`.",
async run({guild, channel, author}) { async run({guild, channel, author}) {
if (isAuthorized(guild, channel)) { if (isAuthorized(guild, channel)) {

View File

@ -1,5 +1,5 @@
import {Message} from "discord.js"; import {Message} from "discord.js";
import {random} from "../../../core/lib"; import {random} from "../../../lib";
export interface ShopItem { export interface ShopItem {
cost: number; cost: number;

View File

@ -1,12 +1,11 @@
import Command from "../../../core/command"; import {Command, NamedCommand, paginate} from "../../../core";
import {pluralise, split} from "../../../core/lib"; import {pluralise, split} from "../../../lib";
import {paginate} from "../../../core/libd"; import {Storage, getPrefix} from "../../../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";
import {EmbedField} from "discord.js"; import {EmbedField} from "discord.js";
export const ShopCommand = new Command({ export const ShopCommand = new NamedCommand({
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)) {
@ -45,7 +44,7 @@ export const ShopCommand = new Command({
} }
}); });
export const BuyCommand = new Command({ export const BuyCommand = new NamedCommand({
description: "Buys an item from the shop.", description: "Buys an item from the shop.",
usage: "<item>", usage: "<item>",
async run({guild, channel, args, message, author}) { async run({guild, channel, args, message, author}) {

View File

@ -1,5 +1,5 @@
import {pluralise} from "../../../core/lib"; import {pluralise} from "../../../lib";
import {Storage} from "../../../core/structures"; import {Storage} from "../../../structures";
import {User, Guild, TextChannel, DMChannel, NewsChannel} from "discord.js"; import {User, Guild, TextChannel, DMChannel, NewsChannel} from "discord.js";
export const ECO_EMBED_COLOR = 0xf1c40f; export const ECO_EMBED_COLOR = 0xf1c40f;

View File

@ -1,6 +1,6 @@
import {URL} from "url"; import {URL} from "url";
import Command from "../../core/command"; import {Command, NamedCommand} from "../../core";
import {getContent} from "../../core/lib"; import {getContent} from "../../lib";
const endpoints: {sfw: {[key: string]: string}} = { const endpoints: {sfw: {[key: string]: string}} = {
sfw: { sfw: {
@ -34,26 +34,26 @@ const endpoints: {sfw: {[key: string]: string}} = {
} }
}; };
export default new Command({ export default new NamedCommand({
description: "Provides you with a random image with the selected argument.", description: "Provides you with a random image with the selected argument.",
async run($) { async run({message, channel, guild, author, member, client, args}) {
$.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($) { async run({message, channel, guild, author, member, client, args}) {
const arg = $.args[0]; const arg = args[0];
if (!(arg in endpoints.sfw)) { if (!(arg in endpoints.sfw)) {
$.channel.send("Couldn't find that endpoint!"); channel.send("Couldn't find that endpoint!");
return; return;
} }
let url = new URL(`https://nekos.life/api/v2${endpoints.sfw[arg]}`); 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,5 +1,5 @@
import Command from "../../core/command"; import {Command, NamedCommand} from "../../core";
import {random} from "../../core/lib"; import {random} from "../../lib";
const responses = [ const responses = [
"boomer", "boomer",
@ -59,9 +59,9 @@ const responses = [
"large man" "large man"
]; ];
export default new Command({ export default new NamedCommand({
description: "Sends random ok message.", description: "Sends random ok message.",
async run($) { async run({message, channel, guild, author, member, client, args}) {
$.channel.send(`ok ${random(responses)}`); channel.send(`ok ${random(responses)}`);
} }
}); });

View File

@ -1,12 +1,12 @@
import Command from "../../core/command"; import {Command, NamedCommand} from "../../core";
import {getContent} from "../../core/lib"; import {getContent} from "../../lib";
import {URL} from "url"; import {URL} from "url";
export default new Command({ export default new NamedCommand({
description: "OwO-ifies the input.", description: "OwO-ifies the input.",
async run($) { async run({message, channel, guild, author, member, client, args}) {
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())) as any; // Apparently, the object in question is {owo: string}. const content = (await getContent(url.toString())) as any; // Apparently, the object in question is {owo: string}.
$.channel.send(content.owo); channel.send(content.owo);
} }
}); });

View File

@ -1,25 +1,25 @@
import {MessageEmbed} from "discord.js"; import {MessageEmbed} from "discord.js";
import Command from "../../core/command"; import {Command, NamedCommand} from "../../core";
export default new Command({ export default new NamedCommand({
description: "Create a poll.", description: "Create a poll.",
usage: "<question>", usage: "<question>",
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($) { async run({message, channel, guild, author, member, client, args}) {
const embed = new MessageEmbed() const embed = new MessageEmbed()
.setAuthor( .setAuthor(
`Poll created by ${$.message.author.username}`, `Poll created by ${message.author.username}`,
$.message.guild?.iconURL({dynamic: true}) ?? undefined message.guild?.iconURL({dynamic: true}) ?? undefined
) )
.setColor(0xffffff) .setColor(0xffffff)
.setFooter("React to vote.") .setFooter("React to vote.")
.setDescription($.args.join(" ")); .setDescription(args.join(" "));
const msg = await $.channel.send(embed); const msg = await channel.send(embed);
await msg.react("✅"); await msg.react("✅");
await msg.react("⛔"); await msg.react("⛔");
$.message.delete({ message.delete({
timeout: 1000 timeout: 1000
}); });
} }

View File

@ -1,8 +1,6 @@
import Command, {handler} from "../../core/command"; import {Command, NamedCommand, botHasPermission, getPermissionLevel, getPermissionName} from "../../core";
import {clean} from "../../core/lib"; import {clean} from "../../lib";
import {botHasPermission} from "../../core/libd"; import {Config, Storage} from "../../structures";
import {Config, Storage} from "../../core/structures";
import {getPermissionLevel, getPermissionName} from "../../core/permissions";
import {Permissions} from "discord.js"; import {Permissions} from "discord.js";
import {logs} from "../../modules/globals"; import {logs} from "../../modules/globals";
@ -20,59 +18,55 @@ function getLogBuffer(type: string) {
const activities = ["playing", "listening", "streaming", "watching"]; const activities = ["playing", "listening", "streaming", "watching"];
const statuses = ["online", "idle", "dnd", "invisible"]; const statuses = ["online", "idle", "dnd", "invisible"];
export default new Command({ export default new NamedCommand({
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($) { async run({message, channel, guild, author, member, client, args}) {
if (!$.member) { if (!member)
$.channel.send("Couldn't find a member object for you! Did you make sure you used this in a server?"); return channel.send("Couldn't find a member object for you! Did you make sure you used this in a server?");
return; const permLevel = getPermissionLevel(author, member);
} return channel.send(`${author}, your permission level is \`${getPermissionName(permLevel)}\` (${permLevel}).`);
const permLevel = getPermissionLevel($.member);
$.channel.send(
`${$.author.toString()}, your permission level is \`${getPermissionName(permLevel)}\` (${permLevel}).`
);
}, },
subcommands: { subcommands: {
set: new Command({ set: new NamedCommand({
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: PERMISSIONS.ADMIN, permission: PERMISSIONS.ADMIN,
subcommands: { subcommands: {
prefix: new Command({ prefix: new NamedCommand({
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($) { async run({message, channel, guild, author, member, client, args}) {
Storage.getGuild($.guild?.id || "N/A").prefix = null; Storage.getGuild(guild?.id || "N/A").prefix = null;
Storage.save(); Storage.save();
$.channel.send( channel.send(
`The custom prefix for this guild has been removed. My prefix is now back to \`${Config.prefix}\`.` `The custom prefix for this guild has been removed. My prefix is now back to \`${Config.prefix}\`.`
); );
}, },
any: new Command({ any: new Command({
async run($) { async run({message, channel, guild, author, member, client, args}) {
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]}\`.`);
} }
}) })
}) })
} }
}), }),
diag: new Command({ diag: new NamedCommand({
description: 'Requests a debug log with the "info" verbosity level.', description: 'Requests a debug log with the "info" verbosity level.',
permission: PERMISSIONS.BOT_SUPPORT, permission: PERMISSIONS.BOT_SUPPORT,
async run($) { async run({message, channel, guild, author, member, client, args}) {
$.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($) { async run({message, channel, guild, author, member, client, args}) {
const type = $.args[0]; const type = args[0];
if (type in logs) $.channel.send(getLogBuffer(type)); if (type in logs) channel.send(getLogBuffer(type));
else else
$.channel.send( channel.send(
`Couldn't find a verbosity level named \`${type}\`! The available types are \`[${Object.keys( `Couldn't find a verbosity level named \`${type}\`! The available types are \`[${Object.keys(
logs logs
).join(", ")}]\`.` ).join(", ")}]\`.`
@ -80,75 +74,72 @@ export default new Command({
} }
}) })
}), }),
status: new Command({ status: new NamedCommand({
description: "Changes the bot's status.", description: "Changes the bot's status.",
permission: PERMISSIONS.BOT_SUPPORT, permission: PERMISSIONS.BOT_SUPPORT,
async run($) { async run({message, channel, guild, author, member, client, args}) {
$.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($) { async run({message, channel, guild, author, member, client, args}) {
if (!statuses.includes($.args[0])) { if (!statuses.includes(args[0])) {
$.channel.send("That status doesn't exist!"); return channel.send("That status doesn't exist!");
return;
} else { } else {
$.client.user?.setStatus($.args[0]); client.user?.setStatus(args[0]);
$.channel.send(`Setting status to \`${$.args[0]}\`...`); return channel.send(`Setting status to \`${args[0]}\`...`);
} }
} }
}) })
}), }),
purge: new Command({ purge: new NamedCommand({
description: "Purges the bot's own messages.", description: "Purges the bot's own messages.",
permission: PERMISSIONS.BOT_SUPPORT, permission: PERMISSIONS.BOT_SUPPORT,
async run($) { async run({message, channel, guild, author, member, client, args}) {
// It's probably better to go through the bot's own messages instead of calling bulkDelete which requires MANAGE_MESSAGES. // It's probably better to go through the bot's own messages instead of calling bulkDelete which requires MANAGE_MESSAGES.
if (botHasPermission($.guild, Permissions.FLAGS.MANAGE_MESSAGES) && $.channel.type !== "dm") { 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 $.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 $.channel.bulkDelete(travMessages); await channel.bulkDelete(travMessages);
} else { } else {
$.channel.send( channel.send(
"This command must be executed in a guild where I have the `MANAGE_MESSAGES` permission." "This command must be executed in a guild where I have the `MANAGE_MESSAGES` permission."
); );
} }
} }
}), }),
clear: new Command({ clear: new NamedCommand({
description: "Clears a given amount of messages.", description: "Clears a given amount of messages.",
usage: "<amount>", usage: "<amount>",
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($) { async run({message, channel, guild, author, member, client, args}) {
if ($.channel.type === "dm") { if (channel.type === "dm") return channel.send("Can't clear messages in the DMs!");
await $.channel.send("Can't clear messages in the DMs!"); message.delete();
return; const fetched = await channel.messages.fetch({
} limit: args[0]
$.message.delete();
const fetched = await $.channel.messages.fetch({
limit: $.args[0]
}); });
await $.channel.bulkDelete(fetched); await channel.bulkDelete(fetched);
return;
} }
}) })
}), }),
eval: new Command({ eval: new NamedCommand({
description: "Evaluate code.", description: "Evaluate code.",
usage: "<code>", usage: "<code>",
permission: 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}) { async run({message, channel, guild, author, member, client, args}) {
try { try {
const code = args.join(" "); const code = args.join(" ");
let evaled = eval(code); let evaled = eval(code);
@ -160,49 +151,46 @@ export default new Command({
} }
} }
}), }),
nick: new Command({ nick: new NamedCommand({
description: "Change the bot's nickname.", description: "Change the bot's nickname.",
permission: PERMISSIONS.BOT_SUPPORT, permission: PERMISSIONS.BOT_SUPPORT,
async run($) { async run({message, channel, guild, author, member, client, args}) {
const nickName = $.args.join(" "); const nickName = args.join(" ");
await $.guild?.me?.setNickname(nickName); await guild?.me?.setNickname(nickName);
if (botHasPermission($.guild, Permissions.FLAGS.MANAGE_MESSAGES)) if (botHasPermission(guild, Permissions.FLAGS.MANAGE_MESSAGES)) message.delete({timeout: 5000});
$.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 NamedCommand({
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: PERMISSIONS.BOT_SUPPORT, permission: PERMISSIONS.BOT_SUPPORT,
async run($) { async run({message, channel, guild, author, member, client, args}) {
const guildList = $.client.guilds.cache.array().map((e) => e.name); const guildList = client.guilds.cache.array().map((e) => e.name);
$.channel.send(guildList, {split: true}); channel.send(guildList, {split: true});
} }
}), }),
activity: new Command({ activity: new NamedCommand({
description: "Set the activity of the bot.", description: "Set the activity of the bot.",
permission: PERMISSIONS.BOT_SUPPORT, permission: PERMISSIONS.BOT_SUPPORT,
usage: "<type> <string>", usage: "<type> <string>",
async run($) { async run({message, channel, guild, author, member, client, args}) {
$.client.user?.setActivity(".help", { client.user?.setActivity(".help", {
type: "LISTENING" type: "LISTENING"
}); });
$.channel.send("Activity set to default."); channel.send("Activity set to default.");
}, },
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($) { async run({message, channel, guild, author, member, client, args}) {
const type = $.args[0]; const type = args[0];
if (activities.includes(type)) { if (activities.includes(type)) {
$.client.user?.setActivity($.args.slice(1).join(" "), { client.user?.setActivity(args.slice(1).join(" "), {
type: $.args[0].toUpperCase() type: args[0].toUpperCase()
}); });
$.channel.send( channel.send(`Set activity to \`${args[0].toUpperCase()}\` \`${args.slice(1).join(" ")}\`.`);
`Set activity to \`${$.args[0].toUpperCase()}\` \`${$.args.slice(1).join(" ")}\`.`
);
} else } else
$.channel.send( channel.send(
`Couldn't find an activity type named \`${type}\`! The available types are \`[${activities.join( `Couldn't find an activity type named \`${type}\`! The available types are \`[${activities.join(
", " ", "
)}]\`.` )}]\`.`

View File

@ -1,13 +1,11 @@
import {Command, NamedCommand} from "../../core/command"; import {Command, NamedCommand, loadableCommands, categories, getPermissionName} from "../../core";
import {toTitleCase} from "../../core/lib"; import {toTitleCase} from "../../lib";
import {loadableCommands, categories} from "../../core/loader";
import {getPermissionName} from "../../core/permissions";
export default new NamedCommand({ export default new NamedCommand({
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($) { async run({message, channel, guild, author, member, client, args}) {
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/...])\``;
@ -22,17 +20,17 @@ export default new NamedCommand({
} }
} }
$.channel.send(output, {split: true}); channel.send(output, {split: true});
}, },
any: new Command({ any: new Command({
async run($) { async run({message, channel, guild, author, member, client, args}) {
// Setup the root command // Setup the root command
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") return channel.send(`No command found by the name \`${header}\`.`);
if (!(command instanceof NamedCommand)) if (!(command instanceof NamedCommand))
return $.channel.send(`Command is not a proper instance of NamedCommand.`); return channel.send(`Command is not a proper instance of NamedCommand.`);
if (command.name) header = command.name; if (command.name) header = command.name;
// Search categories // Search categories
@ -45,9 +43,9 @@ export default new NamedCommand({
} }
// Gather info // Gather info
const result = await command.resolveInfo($.args); const result = await command.resolveInfo(args);
if (result.type === "error") return $.channel.send(result.message); if (result.type === "error") return channel.send(result.message);
let append = ""; let append = "";
command = result.command; command = result.command;
@ -79,7 +77,7 @@ export default new NamedCommand({
aliases = formattedAliases.join(", ") || "None"; aliases = formattedAliases.join(", ") || "None";
} }
return $.channel.send( return channel.send(
`Command: \`${header}\`\nAliases: ${aliases}\nCategory: \`${category}\`\nPermission Required: \`${getPermissionName( `Command: \`${header}\`\nAliases: ${aliases}\nCategory: \`${category}\`\nPermission Required: \`${getPermissionName(
result.permission result.permission
)}\` (${result.permission})\nDescription: ${command.description}\n${append}`, )}\` (${result.permission})\nDescription: ${command.description}\n${append}`,

View File

@ -1,56 +1,7 @@
import Command from "../core/command"; import {Command, NamedCommand} from "../core";
export default new Command({ export default new NamedCommand({
description: async run({message, channel, guild, author, member, client, args}) {
'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,
usage: "",
permission: -1,
aliases: [],
async run($) {
// code // code
}, }
subcommands: {
layer: new Command({
description:
'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,
usage: "",
permission: -1,
aliases: [],
async run($) {
// code
}
})
},
user: new Command({
description:
'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,
usage: "",
permission: -1,
async run($) {
// code
}
}),
number: new Command({
description:
'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,
usage: "",
permission: -1,
async run($) {
// code
}
}),
any: new Command({
description:
"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,
usage: "",
permission: -1,
async run($) {
// code
}
})
}); });

View File

@ -1,29 +1,29 @@
import Command from "../../core/command"; import {Command, NamedCommand} from "../../core";
export default new Command({ export default new NamedCommand({
description: "Renames current voice channel.", description: "Renames current voice channel.",
usage: "<name>", usage: "<name>",
async run($) { async run({message, channel, guild, author, member, client, args}) {
const voiceChannel = $.message.member?.voice.channel; const voiceChannel = message.member?.voice.channel;
if (!voiceChannel) { if (!voiceChannel) {
$.channel.send("You are not in a voice channel."); channel.send("You are not in a voice channel.");
return; return;
} }
if (!voiceChannel.guild.me?.hasPermission("MANAGE_CHANNELS")) { if (!voiceChannel.guild.me?.hasPermission("MANAGE_CHANNELS")) {
$.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; return;
} }
if ($.args.length === 0) { if (args.length === 0) {
$.channel.send("Please provide a new voice channel name."); channel.send("Please provide a new voice channel name.");
return; return;
} }
const prevName = voiceChannel.name; const prevName = voiceChannel.name;
const newName = $.args.join(" "); const newName = args.join(" ");
await voiceChannel.setName(newName); await voiceChannel.setName(newName);
await $.channel.send(`Changed channel name from "${prevName}" to "${newName}".`); await channel.send(`Changed channel name from "${prevName}" to "${newName}".`);
} }
}); });

View File

@ -1,9 +1,7 @@
import Command from "../../core/command"; import {Command, NamedCommand} from "../../core";
import {queryClosestEmoteByName} from "./modules/emote-utils"; import {queryClosestEmoteByName} from "./modules/emote-utils";
import {botHasPermission} from "../../core/libd";
import {Permissions} from "discord.js";
export default new Command({ export default new NamedCommand({
description: "Send the specified emote.", description: "Send the specified emote.",
run: "Please provide a command name.", run: "Please provide a command name.",
any: new Command({ any: new Command({

View File

@ -1,29 +1,26 @@
import {MessageEmbed, version as djsversion} from "discord.js"; import {MessageEmbed, version as djsversion, Guild} 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, NamedCommand, getMemberByUsername} from "../../core";
import {formatBytes, trimArray} from "../../core/lib"; import {formatBytes, trimArray} from "../../lib";
import {getMemberByUsername} from "../../core/libd";
import {verificationLevels, filterLevels, regions} from "../../defs/info"; import {verificationLevels, filterLevels, regions} from "../../defs/info";
import moment from "moment"; import moment, {utc} from "moment";
import utc from "moment";
import {Guild} from "discord.js";
export default new Command({ export default new NamedCommand({
description: "Command to provide all sorts of info about the current server, a user, etc.", description: "Command to provide all sorts of info about the current server, a user, etc.",
run: "Please provide an argument.\nFor help, run `%prefix%help info`.", run: "Please provide an argument.\nFor help, run `%prefix%help info`.",
subcommands: { subcommands: {
avatar: new Command({ avatar: new NamedCommand({
description: "Shows your own, or another user's avatar.", description: "Shows your own, or another user's avatar.",
usage: "(<user>)", usage: "(<user>)",
async run($) { async run({message, channel, guild, author, member, client, args}) {
$.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($) { async run({message, channel, guild, author, member, client, args}) {
$.channel.send( channel.send(
$.args[0].displayAvatarURL({ args[0].displayAvatarURL({
dynamic: true, dynamic: true,
size: 2048 size: 2048
}) })
@ -32,39 +29,39 @@ export default new Command({
}), }),
any: new Command({ any: new Command({
description: "Shows another user's avatar by searching their name", description: "Shows another user's avatar by searching their name",
async run($) { async run({message, channel, guild, author, member, client, args}) {
if ($.guild) { if (guild) {
const name = $.args.join(" "); const name = args.join(" ");
const member = await getMemberByUsername($.guild, name); const member = await getMemberByUsername(guild, name);
if (member) { if (member) {
$.channel.send( channel.send(
member.user.displayAvatarURL({ member.user.displayAvatarURL({
dynamic: true, dynamic: true,
size: 2048 size: 2048
}) })
); );
} else { } else {
$.channel.send(`No user found by the name \`${name}\`!`); channel.send(`No user found by the name \`${name}\`!`);
} }
} }
} }
}) })
}), }),
bot: new Command({ bot: new NamedCommand({
description: "Displays info about the bot.", description: "Displays info about the bot.",
async run($) { async run({message, channel, guild, author, member, client, args}) {
const core = os.cpus()[0]; const core = os.cpus()[0];
const embed = new MessageEmbed() const embed = new MessageEmbed()
.setColor($.guild?.me?.displayHexColor || "BLUE") .setColor(guild?.me?.displayHexColor || "BLUE")
.addField("General", [ .addField("General", [
`** Client:** ${$.client.user?.tag} (${$.client.user?.id})`, `** Client:** ${client.user?.tag} (${client.user?.id})`,
`** Servers:** ${$.client.guilds.cache.size.toLocaleString()}`, `** Servers:** ${client.guilds.cache.size.toLocaleString()}`,
`** Users:** ${$.client.guilds.cache `** Users:** ${client.guilds.cache
.reduce((a: any, b: {memberCount: any}) => a + b.memberCount, 0) .reduce((a: any, b: {memberCount: any}) => a + b.memberCount, 0)
.toLocaleString()}`, .toLocaleString()}`,
`** Channels:** ${$.client.channels.cache.size.toLocaleString()}`, `** Channels:** ${client.channels.cache.size.toLocaleString()}`,
`** Creation Date:** ${utc($.client.user?.createdTimestamp).format("Do MMMM YYYY HH:mm:ss")}`, `** Creation Date:** ${utc(client.user?.createdTimestamp).format("Do MMMM YYYY HH:mm:ss")}`,
`** Node.JS:** ${process.version}`, `** Node.JS:** ${process.version}`,
`** Version:** v${process.env.npm_package_version}`, `** Version:** v${process.env.npm_package_version}`,
`** Discord.JS:** ${djsversion}`, `** Discord.JS:** ${djsversion}`,
@ -84,45 +81,45 @@ export default new Command({
`\u3000 • Used: ${formatBytes(process.memoryUsage().heapUsed)}` `\u3000 • Used: ${formatBytes(process.memoryUsage().heapUsed)}`
]) ])
.setTimestamp(); .setTimestamp();
const avatarURL = $.client.user?.displayAvatarURL({ const avatarURL = client.user?.displayAvatarURL({
dynamic: true, dynamic: true,
size: 2048 size: 2048
}); });
if (avatarURL) embed.setThumbnail(avatarURL); if (avatarURL) embed.setThumbnail(avatarURL);
$.channel.send(embed); channel.send(embed);
} }
}), }),
guild: new Command({ guild: new NamedCommand({
description: "Displays info about the current guild or another guild.", description: "Displays info about the current guild or another guild.",
usage: "(<guild name>/<guild ID>)", usage: "(<guild name>/<guild ID>)",
async run($) { async run({message, channel, guild, author, member, client, args}) {
if ($.guild) { if (guild) {
$.channel.send(await getGuildInfo($.guild, $.guild)); channel.send(await getGuildInfo(guild, guild));
} else { } else {
$.channel.send("Please execute this command in a guild."); channel.send("Please execute this command in a guild.");
} }
}, },
any: new Command({ any: new Command({
description: "Display info about a guild by finding its name or ID.", description: "Display info about a guild by finding its name or ID.",
async run($) { async run({message, channel, guild, author, member, client, args}) {
// If a guild ID is provided (avoid the "number" subcommand because of inaccuracies), search for that guild // If a guild ID is provided (avoid the "number" subcommand because of inaccuracies), search for that guild
if ($.args.length === 1 && /^\d{17,19}$/.test($.args[0])) { if (args.length === 1 && /^\d{17,19}$/.test(args[0])) {
const id = $.args[0]; const id = args[0];
const guild = $.client.guilds.cache.get(id); const guild = client.guilds.cache.get(id);
if (guild) { if (guild) {
$.channel.send(await getGuildInfo(guild, $.guild)); channel.send(await getGuildInfo(guild, guild));
} else { } else {
$.channel.send(`None of the servers I'm in matches the guild ID \`${id}\`!`); channel.send(`None of the servers I'm in matches the guild ID \`${id}\`!`);
} }
} else { } else {
const query: string = $.args.join(" ").toLowerCase(); const query: string = args.join(" ").toLowerCase();
const guild = $.client.guilds.cache.find((guild) => guild.name.toLowerCase().includes(query)); const guild = client.guilds.cache.find((guild) => guild.name.toLowerCase().includes(query));
if (guild) { if (guild) {
$.channel.send(await getGuildInfo(guild, $.guild)); channel.send(await getGuildInfo(guild, guild));
} else { } else {
$.channel.send(`None of the servers I'm in matches the query \`${query}\`!`); channel.send(`None of the servers I'm in matches the query \`${query}\`!`);
} }
} }
} }
@ -131,12 +128,12 @@ export default new Command({
}, },
user: new Command({ user: new Command({
description: "Displays info about mentioned user.", description: "Displays info about mentioned user.",
async run($) { async run({message, channel, guild, author, client, args}) {
// Transforms the User object into a GuildMember object of the current guild. // Transforms the User object into a GuildMember object of the current guild.
const member = await $.guild?.members.fetch($.args[0]); const member = await guild?.members.fetch(args[0]);
if (!member) { if (!member) {
$.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; return;
@ -166,16 +163,14 @@ export default new Command({
`** Game:** ${member.user.presence.activities || "Not playing a game."}` `** Game:** ${member.user.presence.activities || "Not playing a game."}`
]) ])
.addField("Member", [ .addField("Member", [
`** Highest Role:** ${ `** Highest Role:** ${member.roles.highest.id === guild?.id ? "None" : member.roles.highest.name}`,
member.roles.highest.id === $.guild?.id ? "None" : member.roles.highest.name
}`,
`** Server Join Date:** ${moment(member.joinedAt).format("LL LTS")}`, `** Server Join Date:** ${moment(member.joinedAt).format("LL LTS")}`,
`** Hoist Role:** ${member.roles.hoist ? member.roles.hoist.name : "None"}`, `** Hoist Role:** ${member.roles.hoist ? member.roles.hoist.name : "None"}`,
`** Roles:** [${roles.length}]: ${ `** Roles:** [${roles.length}]: ${
roles.length == 0 ? "None" : roles.length <= 10 ? roles.join(", ") : trimArray(roles).join(", ") roles.length == 0 ? "None" : roles.length <= 10 ? roles.join(", ") : trimArray(roles).join(", ")
}` }`
]); ]);
$.channel.send(embed); channel.send(embed);
} }
}) })
}); });

View File

@ -1,44 +1,38 @@
import {GuildEmoji} from "discord.js"; import {GuildEmoji, MessageEmbed, TextChannel, DMChannel, NewsChannel, User} from "discord.js";
import {MessageEmbed} from "discord.js"; import {Command, NamedCommand, paginate} from "../../core";
import Command from "../../core/command"; import {split} from "../../lib";
import {split} from "../../core/lib";
import {paginate} from "../../core/libd";
import vm from "vm"; import vm from "vm";
import {TextChannel} from "discord.js";
import {DMChannel} from "discord.js";
import {NewsChannel} from "discord.js";
import {User} from "discord.js";
const REGEX_TIMEOUT_MS = 1000; const REGEX_TIMEOUT_MS = 1000;
export default new Command({ export default new NamedCommand({
description: "Lists all emotes the bot has in it's registry,", description: "Lists all emotes the bot has in it's registry,",
usage: "<regex pattern> (-flags)", usage: "<regex pattern> (-flags)",
async run($) { async run({message, channel, guild, author, member, client, args}) {
displayEmoteList($.client.emojis.cache.array(), $.channel, $.author); displayEmoteList(client.emojis.cache.array(), channel, author);
}, },
any: new Command({ any: new Command({
description: description:
"Filters emotes by via a regular expression. Flags can be added by adding a dash at the end. For example, to do a case-insensitive search, do %prefix%lsemotes somepattern -i", "Filters emotes by via a regular expression. Flags can be added by adding a dash at the end. For example, to do a case-insensitive search, do %prefix%lsemotes somepattern -i",
async run($) { async run({message, channel, guild, author, member, client, args}) {
// If a guild ID is provided, filter all emotes by that guild (but only if there aren't any arguments afterward) // If a guild ID is provided, filter all emotes by that guild (but only if there aren't any arguments afterward)
if ($.args.length === 1 && /^\d{17,19}$/.test($.args[0])) { if (args.length === 1 && /^\d{17,19}$/.test(args[0])) {
const guildID: string = $.args[0]; const guildID: string = args[0];
displayEmoteList( displayEmoteList(
$.client.emojis.cache.filter((emote) => emote.guild.id === guildID).array(), client.emojis.cache.filter((emote) => emote.guild.id === guildID).array(),
$.channel, channel,
$.author author
); );
} else { } else {
// Otherwise, search via a regex pattern // Otherwise, search via a regex pattern
let flags: string | undefined = undefined; let flags: string | undefined = undefined;
if (/^-[dgimsuy]{1,7}$/.test($.args[$.args.length - 1])) { if (/^-[dgimsuy]{1,7}$/.test(args[args.length - 1])) {
flags = $.args.pop().substring(1); flags = args.pop().substring(1);
} }
let emoteCollection = $.client.emojis.cache.array(); let emoteCollection = client.emojis.cache.array();
// Creates a sandbox to stop a regular expression if it takes too much time to search. // 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]}. // 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: {[id: string]: string} = {};
@ -50,7 +44,7 @@ export default new Command({
// The result will be sandbox.emotes because it'll be modified in-place. // The result will be sandbox.emotes because it'll be modified in-place.
const sandbox = { const sandbox = {
regex: new RegExp($.args.join(" "), flags), regex: new RegExp(args.join(" "), flags),
emotes emotes
}; };
const context = vm.createContext(sandbox); const context = vm.createContext(sandbox);
@ -64,10 +58,10 @@ export default new Command({
script.runInContext(context, {timeout: REGEX_TIMEOUT_MS}); script.runInContext(context, {timeout: REGEX_TIMEOUT_MS});
emotes = sandbox.emotes; emotes = sandbox.emotes;
emoteCollection = emoteCollection.filter((emote) => emotes.has(emote.id)); // Only allow emotes that haven't been deleted. emoteCollection = emoteCollection.filter((emote) => emotes.has(emote.id)); // Only allow emotes that haven't been deleted.
displayEmoteList(emoteCollection, $.channel, $.author); displayEmoteList(emoteCollection, channel, author);
} catch (error) { } catch (error) {
if (error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") { if (error.code === "ERR_SCRIPT_EXECUTION_TIMEOUT") {
$.channel.send( channel.send(
`The regular expression you entered exceeded the time limit of ${REGEX_TIMEOUT_MS} milliseconds.` `The regular expression you entered exceeded the time limit of ${REGEX_TIMEOUT_MS} milliseconds.`
); );
} else { } else {
@ -75,7 +69,7 @@ export default new Command({
} }
} }
} else { } else {
$.channel.send("Failed to initialize sandbox."); channel.send("Failed to initialize sandbox.");
} }
} }
} }

View File

@ -1,17 +1,17 @@
import Command from "../../core/command"; import {Command, NamedCommand} from "../../core";
import {Message, Channel, TextChannel} from "discord.js"; import {Message, Channel, TextChannel} from "discord.js";
import {queryClosestEmoteByName} from "./modules/emote-utils"; import {queryClosestEmoteByName} from "./modules/emote-utils";
export default new Command({ export default new NamedCommand({
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($) { async run({message, channel, guild, author, member, client, args}) {
let target: Message | undefined; let target: Message | undefined;
let distance = 1; let distance = 1;
if ($.args.length >= 2) { if (args.length >= 2) {
const last = $.args[$.args.length - 1]; // Because this is optional, do not .pop() unless you're sure it's a message link indicator. const last = args[args.length - 1]; // Because this is optional, do not .pop() unless you're sure it's a message link indicator.
const URLPattern = /^(?:https:\/\/discord.com\/channels\/(\d{17,19})\/(\d{17,19})\/(\d{17,19}))$/; const URLPattern = /^(?:https:\/\/discord.com\/channels\/(\d{17,19})\/(\d{17,19})\/(\d{17,19}))$/;
const copyIDPattern = /^(?:(\d{17,19})-(\d{17,19}))$/; const copyIDPattern = /^(?:(\d{17,19})-(\d{17,19}))$/;
@ -21,66 +21,65 @@ export default new Command({
const guildID = match[1]; const guildID = match[1];
const channelID = match[2]; const channelID = match[2];
const messageID = match[3]; const messageID = match[3];
let guild = $.guild; let tmpChannel: Channel | undefined = channel;
let channel: Channel | undefined = $.channel;
if (guild?.id !== guildID) { if (guild?.id !== guildID) {
try { try {
guild = await $.client.guilds.fetch(guildID); guild = await client.guilds.fetch(guildID);
} catch { } catch {
return $.channel.send(`\`${guildID}\` is an invalid guild ID!`); return channel.send(`\`${guildID}\` is an invalid guild ID!`);
} }
} }
if (channel.id !== channelID) channel = guild.channels.cache.get(channelID); if (tmpChannel.id !== channelID) tmpChannel = guild.channels.cache.get(channelID);
if (!channel) return $.channel.send(`\`${channelID}\` is an invalid channel ID!`); if (!tmpChannel) return channel.send(`\`${channelID}\` is an invalid channel ID!`);
if ($.message.id !== messageID) { if (message.id !== messageID) {
try { try {
target = await (channel as TextChannel).messages.fetch(messageID); target = await (tmpChannel as TextChannel).messages.fetch(messageID);
} catch { } catch {
return $.channel.send(`\`${messageID}\` is an invalid message ID!`); return channel.send(`\`${messageID}\` is an invalid message ID!`);
} }
} }
$.args.pop(); args.pop();
} }
// <Channel ID>-<Message ID> ("Copy ID" Button) // <Channel ID>-<Message ID> ("Copy ID" Button)
else if (copyIDPattern.test(last)) { else if (copyIDPattern.test(last)) {
const match = copyIDPattern.exec(last)!; const match = copyIDPattern.exec(last)!;
const channelID = match[1]; const channelID = match[1];
const messageID = match[2]; const messageID = match[2];
let channel: Channel | undefined = $.channel; let tmpChannel: Channel | undefined = channel;
if (channel.id !== channelID) channel = $.guild?.channels.cache.get(channelID); if (tmpChannel.id !== channelID) tmpChannel = guild?.channels.cache.get(channelID);
if (!channel) return $.channel.send(`\`${channelID}\` is an invalid channel ID!`); if (!tmpChannel) return channel.send(`\`${channelID}\` is an invalid channel ID!`);
if ($.message.id !== messageID) { if (message.id !== messageID) {
try { try {
target = await (channel as TextChannel).messages.fetch(messageID); target = await (tmpChannel as TextChannel).messages.fetch(messageID);
} catch { } catch {
return $.channel.send(`\`${messageID}\` is an invalid message ID!`); return channel.send(`\`${messageID}\` is an invalid message ID!`);
} }
} }
$.args.pop(); args.pop();
} }
// <Message ID> // <Message ID>
else if (/^\d{17,19}$/.test(last)) { else if (/^\d{17,19}$/.test(last)) {
try { try {
target = await $.channel.messages.fetch(last); target = await channel.messages.fetch(last);
} catch { } catch {
return $.channel.send(`No valid message found by the ID \`${last}\`!`); return channel.send(`No valid message found by the ID \`${last}\`!`);
} }
$.args.pop(); args.pop();
} }
// The entire string has to be a number for this to match. Prevents leaCheeseAmerican1 from triggering this. // The entire string has to be a number for this to match. Prevents leaCheeseAmerican1 from triggering this.
else if (/^\d+$/.test(last)) { else if (/^\d+$/.test(last)) {
distance = parseInt(last); distance = parseInt(last);
if (distance >= 0 && distance <= 99) $.args.pop(); if (distance >= 0 && distance <= 99) args.pop();
else return $.channel.send("Your distance must be between 0 and 99!"); else return channel.send("Your distance must be between 0 and 99!");
} }
} }
@ -88,13 +87,13 @@ export default new Command({
// Messages are ordered from latest to earliest. // Messages are ordered from latest to earliest.
// You also have to add 1 as well because fetchMessages includes your own message. // You also have to add 1 as well because fetchMessages includes your own message.
target = ( target = (
await $.message.channel.messages.fetch({ await message.channel.messages.fetch({
limit: distance + 1 limit: distance + 1
}) })
).last(); ).last();
} }
for (const search of $.args) { for (const search of args) {
// Even though the bot will always grab *some* emote, the user can choose not to keep that emote there if it isn't what they want // Even though the bot will always grab *some* emote, the user can choose not to keep that emote there if it isn't what they want
const emote = queryClosestEmoteByName(search); const emote = queryClosestEmoteByName(search);
const reaction = await target!.react(emote); const reaction = await target!.react(emote);
@ -102,7 +101,7 @@ export default new Command({
// This part is called with a promise because you don't want to wait 5 seconds between each reaction. // This part is called with a promise because you don't want to wait 5 seconds between each reaction.
setTimeout(() => { setTimeout(() => {
// This reason for this null assertion is that by the time you use this command, the client is going to be loaded. // This reason for this null assertion is that by the time you use this command, the client is going to be loaded.
reaction.users.remove($.client.user!); reaction.users.remove(client.user!);
}, 5000); }, 5000);
} }

View File

@ -1,13 +1,13 @@
import Command from "../../core/command"; import {Command, NamedCommand} from "../../core";
export default new Command({ export default new NamedCommand({
description: "Repeats your message.", description: "Repeats your message.",
usage: "<message>", usage: "<message>",
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($) { async run({message, channel, guild, author, member, client, args}) {
$.channel.send(`*${$.author} says:*\n${$.args.join(" ")}`); channel.send(`*${author} says:*\n${args.join(" ")}`);
} }
}) })
}); });

View File

@ -1,33 +1,33 @@
import Command, {handler} from "../../core/command"; import {Command, NamedCommand} from "../../core";
import {pluralise} from "../../core/lib"; import {pluralise} from "../../lib";
import moment from "moment"; import moment from "moment";
import {Collection, TextChannel} from "discord.js"; import {Collection, TextChannel} from "discord.js";
const lastUsedTimestamps: {[id: string]: number} = {}; const lastUsedTimestamps: {[id: string]: number} = {};
export default new Command({ export default new NamedCommand({
description: description:
"Scans all text channels in the current guild and returns the number of times each emoji specific to the guild has been used. Has a cooldown of 24 hours per guild.", "Scans all text channels in the current guild and returns the number of times each emoji specific to the guild has been used. Has a cooldown of 24 hours per guild.",
async run($) { async run({message, channel, guild, author, member, client, args}) {
if (!$.guild) { if (!guild) {
$.channel.send(`You must use this command on a server!`); channel.send(`You must use this command on a server!`);
return; 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();
const cooldown = 86400000; // 24 hours const cooldown = 86400000; // 24 hours
const lastUsedTimestamp = lastUsedTimestamps[$.guild.id] ?? 0; const lastUsedTimestamp = lastUsedTimestamps[guild.id] ?? 0;
const difference = startTime - lastUsedTimestamp; const difference = startTime - lastUsedTimestamp;
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) {
$.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}.`
); );
return; return;
} else lastUsedTimestamps[$.guild.id] = startTime; } else lastUsedTimestamps[guild.id] = startTime;
const stats: { const stats: {
[id: string]: { [id: string]: {
@ -39,20 +39,20 @@ export default new Command({
} = {}; } = {};
let totalUserEmoteUsage = 0; let totalUserEmoteUsage = 0;
// IMPORTANT: You MUST check if the bot actually has access to the channel in the first place. It will get the list of all channels, but that doesn't mean it has access to every channel. Without this, it'll require admin access and throw an annoying unhelpful DiscordAPIError: Missing Access otherwise. // IMPORTANT: You MUST check if the bot actually has access to the channel in the first place. It will get the list of all channels, but that doesn't mean it has access to every channel. Without this, it'll require admin access and throw an annoying unhelpful DiscordAPIError: Missing Access otherwise.
const allTextChannelsInCurrentGuild = $.guild.channels.cache.filter( const allTextChannelsInCurrentGuild = guild.channels.cache.filter(
(channel) => channel.type === "text" && channel.viewable (channel) => channel.type === "text" && channel.viewable
) as Collection<string, TextChannel>; ) as Collection<string, TextChannel>;
let messagesSearched = 0; let messagesSearched = 0;
let channelsSearched = 0; let channelsSearched = 0;
let currentChannelName = ""; let currentChannelName = "";
const totalChannels = allTextChannelsInCurrentGuild.size; const totalChannels = allTextChannelsInCurrentGuild.size;
const statusMessage = await $.channel.send("Gathering emotes..."); const statusMessage = await channel.send("Gathering emotes...");
let warnings = 0; let warnings = 0;
$.channel.startTyping(); channel.startTyping();
// Initialize the emote stats object with every emote in the current guild. // Initialize the emote stats object with every emote in the current guild.
// The goal here is to cut the need to access guild.emojis.get() which'll make it faster and easier to work with. // The goal here is to cut the need to access guild.emojis.get() which'll make it faster and easier to work with.
for (let emote of $.guild.emojis.cache.values()) { for (let emote of guild.emojis.cache.values()) {
// If you don't include the "a" for animated emotes, it'll not only not show up, but also cause all other emotes in the same message to not show up. The emote name is self-correcting but it's better to keep the right value since it'll be used to calculate message lengths that fit. // If you don't include the "a" for animated emotes, it'll not only not show up, but also cause all other emotes in the same message to not show up. The emote name is self-correcting but it's better to keep the right value since it'll be used to calculate message lengths that fit.
stats[emote.id] = { stats[emote.id] = {
name: emote.name, name: emote.name,
@ -70,7 +70,7 @@ export default new Command({
for (const channel of allTextChannelsInCurrentGuild.values()) { for (const channel of allTextChannelsInCurrentGuild.values()) {
currentChannelName = channel.name; currentChannelName = channel.name;
let selected = channel.lastMessageID ?? $.message.id; let selected = channel.lastMessageID ?? message.id;
let continueLoop = true; let continueLoop = true;
while (continueLoop) { while (continueLoop) {
@ -167,7 +167,7 @@ export default new Command({
)}.` )}.`
); );
console.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.
// This can work outside the loop now that it's synchronous, and now it's clearer what code is meant to execute at the end. // This can work outside the loop now that it's synchronous, and now it's clearer what code is meant to execute at the end.
@ -186,6 +186,6 @@ export default new Command({
); );
} }
$.channel.send(lines, {split: true}).catch(handler.bind($)); await channel.send(lines, {split: true});
} }
}); });

View File

@ -1,18 +1,18 @@
import Command from "../../core/command"; import {Command, NamedCommand} from "../../core";
import * as https from "https"; import * as https from "https";
export default new Command({ export default new NamedCommand({
description: "Shortens a given URL.", description: "Shortens a given URL.",
run: "Please provide a URL.", run: "Please provide a URL.",
any: new Command({ any: new Command({
async run($) { async run({message, channel, guild, author, member, client, args}) {
https.get("https://is.gd/create.php?format=simple&url=" + encodeURIComponent($.args[0]), function (res) { https.get("https://is.gd/create.php?format=simple&url=" + encodeURIComponent(args[0]), function (res) {
var body = ""; var body = "";
res.on("data", function (chunk) { res.on("data", function (chunk) {
body += chunk; body += chunk;
}); });
res.on("end", function () { res.on("end", function () {
$.channel.send(`<${body}>`); channel.send(`<${body}>`);
}); });
}); });
} }

View File

@ -1,6 +1,5 @@
import Command from "../../core/command"; import {Command, NamedCommand, ask, askYesOrNo, askMultipleChoice, prompt, callMemberByUsername} from "../../core";
import {ask, askYesOrNo, askMultipleChoice, prompt, callMemberByUsername} from "../../core/libd"; import {Storage} from "../../structures";
import {Storage} from "../../core/structures";
import {User} from "discord.js"; import {User} from "discord.js";
import moment from "moment"; import moment from "moment";
@ -167,7 +166,7 @@ function getTimeEmbed(user: User) {
return embed; return embed;
} }
export default new Command({ export default new NamedCommand({
description: "Show others what time it is for you.", description: "Show others what time it is for you.",
aliases: ["tz"], aliases: ["tz"],
async run({channel, author}) { async run({channel, author}) {
@ -175,7 +174,7 @@ export default new Command({
}, },
subcommands: { subcommands: {
// Welcome to callback hell. We hope you enjoy your stay here! // Welcome to callback hell. We hope you enjoy your stay here!
setup: new Command({ setup: new NamedCommand({
description: "Registers your timezone information for the bot.", description: "Registers your timezone information for the bot.",
async run({author, channel}) { async run({author, channel}) {
const profile = Storage.getUser(author.id); const profile = Storage.getUser(author.id);
@ -327,7 +326,7 @@ export default new Command({
); );
} }
}), }),
delete: new Command({ delete: new NamedCommand({
description: "Delete your timezone information.", description: "Delete your timezone information.",
async run({channel, author}) { async run({channel, author}) {
prompt( prompt(
@ -344,7 +343,7 @@ export default new Command({
); );
} }
}), }),
utc: new Command({ utc: new NamedCommand({
description: "Displays UTC time.", description: "Displays UTC time.",
async run({channel}) { async run({channel}) {
const time = moment().utc(); const time = moment().utc();
@ -370,7 +369,7 @@ export default new Command({
}); });
} }
}), }),
daylight: new Command({ daylight: new NamedCommand({
description: "Provides information on the daylight savings region", description: "Provides information on the daylight savings region",
run: DST_NOTE_INFO run: DST_NOTE_INFO
}) })

View File

@ -1,19 +1,21 @@
import {parseVars} from "./lib"; import {parseVars} from "../lib";
import {Collection} from "discord.js"; import {
import {Client, Message, TextChannel, DMChannel, NewsChannel, Guild, User, GuildMember, GuildChannel} from "discord.js"; Collection,
import {getPrefix} from "../core/structures"; Client,
Message,
TextChannel,
DMChannel,
NewsChannel,
Guild,
User,
GuildMember,
GuildChannel
} from "discord.js";
import {getPrefix} from "../structures";
import {SingleMessageOptions} from "./libd"; import {SingleMessageOptions} from "./libd";
import {hasPermission, getPermissionLevel, getPermissionName} from "./permissions"; import {hasPermission, getPermissionLevel, getPermissionName} from "./permissions";
import {client} from "../index"; import {client} from "../index";
export enum TYPES {
SUBCOMMAND,
USER,
NUMBER,
ANY,
NONE
}
// RegEx patterns used for identifying/extracting each type from a string argument. // RegEx patterns used for identifying/extracting each type from a string argument.
const patterns = { const patterns = {
channel: /^<#(\d{17,19})>$/, channel: /^<#(\d{17,19})>$/,
@ -30,7 +32,7 @@ const patterns = {
// Therefore, there won't by any type narrowing on channel or guild of CommandMenu until this is fixed. // Therefore, there won't by any type narrowing on channel or guild of CommandMenu until this is fixed.
// Otherwise, you'd have to define channelType for every single subcommand, which would get very tedious. // Otherwise, you'd have to define channelType for every single subcommand, which would get very tedious.
// Just use type assertions when you specify a channel type. // Just use type assertions when you specify a channel type.
export enum CHANNEL_TYPE { enum CHANNEL_TYPE {
ANY, ANY,
GUILD, GUILD,
DM DM

View File

@ -1,8 +1,7 @@
import {client} from "../index"; import {client} from "../index";
import {loadableCommands} from "./loader"; import {loadableCommands} from "./loader";
import {Permissions, Message} from "discord.js"; import {Permissions, Message} from "discord.js";
import {getPrefix} from "./structures"; import {getPrefix} from "../structures";
import {Config} from "./structures";
import {defaultMetadata} from "./command"; import {defaultMetadata} from "./command";
// For custom message events that want to cancel the command handler on certain conditions. // For custom message events that want to cancel the command handler on certain conditions.
@ -99,13 +98,3 @@ client.on("message", async (message) => {
); );
} }
}); });
client.once("ready", () => {
if (client.user) {
console.ready(`Logged in as ${client.user.tag}.`);
client.user.setActivity({
type: "LISTENING",
name: `${Config.prefix}help`
});
}
});

15
src/core/index.ts Normal file
View File

@ -0,0 +1,15 @@
export {Command, NamedCommand} from "./command";
export {addInterceptRule} from "./handler";
export {
SingleMessageOptions,
botHasPermission,
paginate,
prompt,
ask,
askYesOrNo,
askMultipleChoice,
getMemberByUsername,
callMemberByUsername
} from "./libd";
export {loadableCommands, categories} from "./loader";
export {hasPermission, getPermissionLevel, getPermissionName} from "./permissions";

View File

@ -1,5 +1,5 @@
import {User, GuildMember, Permissions} from "discord.js"; import {User, GuildMember, Permissions} from "discord.js";
import {Config} from "./structures"; import {Config} from "../structures";
interface PermissionLevel { interface PermissionLevel {
name: string; name: string;

View File

@ -2,7 +2,7 @@
import "./modules/globals"; import "./modules/globals";
import {Client} from "discord.js"; import {Client} from "discord.js";
import setup from "./modules/setup"; import setup from "./modules/setup";
import {Config} from "./core/structures"; import {Config} from "./structures";
// 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.
@ -16,6 +16,7 @@ setup.init().then(() => {
// Initialize Modules // // Initialize Modules //
import "./core/handler"; // Command loading will start as soon as an instance of "core/command" is loaded, which is loaded in "core/handler". import "./core/handler"; // Command loading will start as soon as an instance of "core/command" is loaded, which is loaded in "core/handler".
import "./core/eventListeners"; import "./core/eventListeners";
import "./modules/ready";
import "./modules/presence"; import "./modules/presence";
import "./modules/lavalink"; import "./modules/lavalink";
import "./modules/emoteRegistry"; import "./modules/emoteRegistry";

View File

@ -1,6 +1,6 @@
// Library for pure functions // Library for pure functions
import {get} from "https"; import {get} from "https";
import FileManager from "./storage"; import FileManager from "./modules/storage";
/** /**
* Splits a command by spaces while accounting for quotes which capture string arguments. * Splits a command by spaces while accounting for quotes which capture string arguments.

View File

@ -1,6 +1,6 @@
import {client} from "../index"; import {client} from "../index";
import FileManager from "../core/storage"; import FileManager from "./storage";
import {EmoteRegistryDump} from "../core/structures"; import {EmoteRegistryDump} from "../structures";
function updateGlobalEmoteRegistry(): void { function updateGlobalEmoteRegistry(): void {
const data: EmoteRegistryDump = {version: 1, list: []}; const data: EmoteRegistryDump = {version: 1, list: []};

View File

@ -1,5 +1,5 @@
import attachClientToLavalink from "discord.js-lavalink-lib"; import attachClientToLavalink from "discord.js-lavalink-lib";
import {Config} from "../core/structures"; import {Config} from "../structures";
import {client} from "../index"; import {client} from "../index";
// Although the example showed to do "client.music = LavaLink(...)" and "(client as any).music = Lavalink(...)" was done to match that, nowhere in the library is client.music ever actually used nor does the function return anything. In other words, client.music is undefined and is never used. // Although the example showed to do "client.music = LavaLink(...)" and "(client as any).music = Lavalink(...)" was done to match that, nowhere in the library is client.music ever actually used nor does the function return anything. In other words, client.music is undefined and is never used.

View File

@ -1,6 +1,6 @@
import {client} from "../index"; import {client} from "../index";
import {TextChannel, APIMessage, MessageEmbed} from "discord.js"; import {TextChannel, APIMessage, MessageEmbed} from "discord.js";
import {getPrefix} from "../core/structures"; import {getPrefix} from "../structures";
import {DiscordAPIError} from "discord.js"; import {DiscordAPIError} from "discord.js";
client.on("message", async (message) => { client.on("message", async (message) => {

12
src/modules/ready.ts Normal file
View File

@ -0,0 +1,12 @@
import {client} from "../index";
import {Config} from "../structures";
client.once("ready", () => {
if (client.user) {
console.ready(`Logged in as ${client.user.tag}.`);
client.user.setActivity({
type: "LISTENING",
name: `${Config.prefix}help`
});
}
});

View File

@ -1,7 +1,7 @@
import {existsSync as exists, readFileSync as read, writeFile as write} from "fs"; 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 "./storage";
import {Config} from "../core/structures"; import {Config} from "../structures";
// 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.

View File

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