Class commands, improved sharding, and many other changes (#88)

* Load commands recursively

* Sort commands

* Missed a couple of spots

* missed even more spots apparently

* Ported commands in "fun" category to new class-based format, added babel eslint plugin

* Ported general commands, removed old/unneeded stuff, replaced moment with day, many more fixes I lost track of

* Missed a spot

* Removed unnecessary abort-controller package, add deprecation warning for mongo database

* Added imagereload, clarified premature end message

* Fixed docker-compose path issue, added total bot uptime to stats, more fixes for various parts

* Converted image commands into classes, fixed reload, ignore another WS event, cleaned up command handler and image runner

* Converted music/soundboard commands to class format

* Cleanup unnecessary logs

* awful tag command class port

* I literally somehow just learned that you can leave out the constructor in classes

* Pass client directly to commands/events, cleaned up command handler

* Migrated bot to eris-sharder, fixed some error handling stuff

* Remove unused modules

* Fixed type returning

* Switched back to Eris stable

* Some fixes and cleanup

* might wanna correct this

* Implement image command ratelimiting

* Added Bot token prefix, added imagestats, added running endpoint to API
This commit is contained in:
Essem 2021-04-12 11:16:12 -05:00 committed by GitHub
parent ff8a24d0e8
commit 40223ec8b5
No known key found for this signature in database
GPG key ID: 4AEE18F83AFDEB23
291 changed files with 5296 additions and 5171 deletions

View file

@ -0,0 +1,25 @@
const Command = require("../../classes/command.js");
class AvatarCommand extends Command {
async run() {
if (this.message.mentions[0] !== undefined) {
return this.message.mentions[0].dynamicAvatarURL(null, 1024);
} else if (this.client.users.get(this.args[0]) !== undefined) {
return this.client.users.get(this.args[0]).dynamicAvatarURL(null, 1024);
} else if (this.args.join(" ") !== "" && this.message.channel.guild) {
const userRegex = new RegExp(this.args.join("|"), "i");
const member = this.message.channel.guild.members.find(element => {
return userRegex.test(element.nick) ? userRegex.test(element.nick) : userRegex.test(element.username);
});
return member ? member.user.dynamicAvatarURL(null, 1024) : this.message.author.dynamicAvatarURL(null, 1024);
} else {
return this.message.author.dynamicAvatarURL(null, 1024);
}
}
static description = "Gets a user's avatar";
static aliases = ["pfp", "ava"];
static arguments = ["{mention/id}"];
}
module.exports = AvatarCommand;

View file

@ -0,0 +1,46 @@
const db = require("../../utils/database.js");
const Command = require("../../classes/command.js");
class ChannelCommand extends Command {
async run() {
if (!this.message.channel.guild) return `${this.message.author.mention}, this command only works in servers!`;
if (!this.message.member.permissions.has("administrator") && this.message.member.id !== process.env.OWNER) return `${this.message.author.mention}, you need to be an administrator to enable/disable me!`;
if (this.args.length === 0) return `${this.message.author.mention}, you need to provide whether I should be enabled or disabled in this channel!`;
if (this.args[0] !== "disable" && this.args[0] !== "enable") return `${this.message.author.mention}, that's not a valid option!`;
const guildDB = await db.getGuild(this.message.channel.guild.id);
if (this.args[0].toLowerCase() === "disable") {
let channel;
if (this.args[1] && this.args[1].match(/^<?[@#]?[&!]?\d+>?$/) && this.args[1] >= 21154535154122752n) {
const id = this.args[1].replaceAll("@", "").replaceAll("#", "").replaceAll("!", "").replaceAll("&", "").replaceAll("<", "").replaceAll(">", "");
if (guildDB.disabled.includes(id)) return `${this.message.author.mention}, I'm already disabled in this channel!`;
channel = this.message.channel.guild.channels.get(id);
} else {
if (guildDB.disabled.includes(this.message.channel.id)) return `${this.message.author.mention}, I'm already disabled in this channel!`;
channel = this.message.channel;
}
await db.disableChannel(channel);
return `${this.message.author.mention}, I have been disabled in this channel. To re-enable me, just run \`${guildDB.prefix}channel enable\`.`;
} else if (this.args[0].toLowerCase() === "enable") {
let channel;
if (this.args[1] && this.args[1].match(/^<?[@#]?[&!]?\d+>?$/) && this.args[1] >= 21154535154122752) {
const id = this.args[1].replaceAll("@", "").replaceAll("#", "").replaceAll("!", "").replaceAll("&", "").replaceAll("<", "").replaceAll(">", "");
if (!guildDB.disabled.includes(id)) return `${this.message.author.mention}, I'm not disabled in that channel!`;
channel = this.message.channel.guild.channels.get(id);
} else {
if (!guildDB.disabled.includes(this.message.channel.id)) return `${this.message.author.mention}, I'm not disabled in this channel!`;
channel = this.message.channel;
}
await db.enableChannel(channel);
return `${this.message.author.mention}, I have been re-enabled in this channel.`;
}
}
static description = "Enables/disables me in a channel";
static arguments = ["[enable/disable]", "{id}"];
}
module.exports = ChannelCommand;

46
commands/general/count.js Normal file
View file

@ -0,0 +1,46 @@
const paginator = require("../../utils/pagination/pagination.js");
const database = require("../../utils/database.js");
const Command = require("../../classes/command.js");
class CountCommand extends Command {
async run() {
if (this.message.channel.guild && !this.message.channel.permissionsOf(this.client.user.id).has("addReactions")) return `${this.message.author.mention}, I don't have the \`Add Reactions\` permission!`;
if (this.message.channel.guild && !this.message.channel.permissionsOf(this.client.user.id).has("embedLinks")) return `${this.message.author.mention}, I don't have the \`Embed Links\` permission!`;
const counts = await database.getCounts();
const countArray = [];
const sortedValues = counts.sort((a, b) => {
return b[1] - a[1];
});
for (const [key, value] of sortedValues) {
countArray.push(`**${key}**: ${value}`);
}
const embeds = [];
const groups = countArray.map((item, index) => {
return index % 15 === 0 ? countArray.slice(index, index + 15) : null;
}).filter((item) => {
return item;
});
for (const [i, value] of groups.entries()) {
embeds.push({
"embed": {
"title": "Command Usage Counts",
"color": 16711680,
"footer": {
"text": `Page ${i + 1} of ${groups.length}`
},
"description": value.join("\n"),
"author": {
"name": this.message.author.username,
"icon_url": this.message.author.avatarURL
}
}
});
}
return paginator(this.client, this.message, embeds);
}
static description = "Gets how many times every command was used";
static arguments = ["{mention/id}"];
}
module.exports = CountCommand;

View file

@ -0,0 +1,16 @@
const { clean } = require("../../utils/misc.js");
const Command = require("../../classes/command.js");
class DecodeCommand extends Command {
async run() {
if (this.args.length === 0) return `${this.message.author.mention}, you need to provide a string to decode!`;
const b64Decoded = Buffer.from(this.args.join(" "), "base64").toString("utf-8");
return `\`\`\`\n${await clean(b64Decoded)}\`\`\``;
}
static description = "Decodes a Base64 string";
static aliases = ["b64decode", "base64decode"];
static arguments = ["[text]"];
}
module.exports = DecodeCommand;

View file

@ -0,0 +1,20 @@
const fetch = require("node-fetch");
const Command = require("../../classes/command.js");
class DonateCommand extends Command {
async run() {
let prefix = "";
const patrons = await fetch("https://projectlounge.pw/patrons").then(data => data.json());
prefix = "Thanks to the following patrons for their support:\n";
for (const patron of patrons) {
prefix += `**- ${patron}**\n`;
}
prefix += "\n";
return `${prefix}Like esmBot? Consider supporting the developer on Patreon to help keep it running! https://patreon.com/TheEssem`;
}
static description = "Learn more about how you can support esmBot's development";
static aliases = ["support", "patreon", "patrons"];
}
module.exports = DonateCommand;

25
commands/general/emote.js Normal file
View file

@ -0,0 +1,25 @@
const emojiRegex = require("emoji-regex");
const Command = require("../../classes/command.js");
class EmoteCommand extends Command {
async run() {
if (this.args.length === 0) return `${this.message.author.mention}, you need to provide an emoji!`;
if (this.content.split(" ")[0].match(/^<a?:.+:\d+>$/)) {
return `https://cdn.discordapp.com/emojis/${this.content.split(" ")[0].replace(/^<(a)?:.+:(\d+)>$/, "$2")}.${this.content.split(" ")[0].replace(/^<(a)?:.+:(\d+)>$/, "$1") === "a" ? "gif" : "png"}`;
} else if (this.args[0].match(emojiRegex)) {
const codePoints = [];
for (const codePoint of this.args[0]) {
codePoints.push(codePoint.codePointAt(0).toString(16));
}
return `https://twemoji.maxcdn.com/v/latest/72x72/${codePoints.join("-").replace("-fe0f", "")}.png`;
} else {
return `${this.message.author.mention}, you need to provide a valid emoji to get an image!`;
}
}
static description = "Gets a raw emote image";
static aliases = ["e", "em", "hugemoji", "hugeemoji", "emoji"];
static arguments = ["[emote]"];
}
module.exports = EmoteCommand;

View file

@ -0,0 +1,15 @@
const Command = require("../../classes/command.js");
class EncodeCommand extends Command {
async run() {
if (this.args.length === 0) return `${this.message.author.mention}, you need to provide a string to encode!`;
const b64Encoded = Buffer.from(this.args.join(" ")).toString("base64");
return `\`\`\`\n${b64Encoded}\`\`\``;
}
static description = "Encodes a Base64 string";
static aliases = ["b64encode", "base64encode"];
static arguments = ["[text]"];
}
module.exports = EncodeCommand;

31
commands/general/eval.js Normal file
View file

@ -0,0 +1,31 @@
const { clean } = require("../../utils/misc.js");
const Command = require("../../classes/command.js");
class EvalCommand extends Command {
async run() {
if (this.message.author.id !== process.env.OWNER) return `${this.message.author.mention}, only the bot owner can use eval!`;
const code = this.args.join(" ");
try {
const evaled = eval(code);
const cleaned = await clean(evaled);
const sendString = `\`\`\`js\n${cleaned}\n\`\`\``;
if (sendString.length >= 2000) {
return {
text: "The result was too large, so here it is as a file:",
file: cleaned,
name: "result.txt"
};
} else {
return sendString;
}
} catch (err) {
return `\`ERROR\` \`\`\`xl\n${await clean(err)}\n\`\`\``;
}
}
static description = "Executes JavaScript code";
static aliases = ["run"];
static arguments = ["[code]"];
}
module.exports = EvalCommand;

34
commands/general/exec.js Normal file
View file

@ -0,0 +1,34 @@
const { clean } = require("../../utils/misc.js");
const util = require("util");
const exec = util.promisify(require("child_process").exec);
const Command = require("../../classes/command.js");
class ExecCommand extends Command {
async run() {
if (this.message.author.id !== process.env.OWNER) return `${this.message.author.mention}, only the bot owner can use exec!`;
const code = this.args.join(" ");
try {
const execed = await exec(code);
if (execed.stderr) return `\`ERROR\` \`\`\`xl\n${await clean(execed.stderr)}\n\`\`\``;
const cleaned = await clean(execed.stdout);
const sendString = `\`\`\`bash\n${cleaned}\n\`\`\``;
if (sendString.length >= 2000) {
return {
text: "The result was too large, so here it is as a file:",
file: cleaned,
name: "result.txt"
};
} else {
return sendString;
}
} catch (err) {
return `\`ERROR\` \`\`\`xl\n${await clean(err)}\n\`\`\``;
}
}
static description = "Executes a shell command";
static aliases = ["runcmd"];
static arguments = ["[command]"];
}
module.exports = ExecCommand;

100
commands/general/help.js Normal file
View file

@ -0,0 +1,100 @@
const database = require("../../utils/database.js");
const collections = require("../../utils/collections.js");
const misc = require("../../utils/misc.js");
const paginator = require("../../utils/pagination/pagination.js");
const help = require("../../utils/help.js");
const tips = ["You can change the bot's prefix using the prefix command.", "Image commands also work with images previously posted in that channel.", "You can use the tags commands to save things for later use.", "You can visit https://projectlounge.pw/esmBot/help.html for a web version of this command list.", "You can view a command's aliases by putting the command name after the help command (e.g. help image).", "Parameters wrapped in [] are required, while parameters wrapped in {} are optional.", "esmBot is hosted and paid for completely out-of-pocket by the main developer. If you want to support development, please consider donating! https://patreon.com/TheEssem"];
const Command = require("../../classes/command.js");
class HelpCommand extends Command {
async run() {
const { prefix } = this.message.channel.guild ? await database.getGuild(this.message.channel.guild.id) : "N/A";
const commands = collections.commands;
const aliases = collections.aliases;
if (this.args.length !== 0 && (commands.has(this.args[0].toLowerCase()) || aliases.has(this.args[0].toLowerCase()))) {
const command = aliases.has(this.args[0].toLowerCase()) ? collections.aliases.get(this.args[0].toLowerCase()) : this.args[0].toLowerCase();
const info = collections.info.get(command);
const countDB = await database.getCounts();
const counts = countDB.reduce((acc, val) => {
const [key, value] = val;
acc[key] = value;
return acc;
}, {});
const embed = {
"embed": {
"author": {
"name": "esmBot Help",
"icon_url": this.client.user.avatarURL
},
"title": `${this.message.channel.guild ? prefix : ""}${command}`,
"url": "https://projectlounge.pw/esmBot/help.html",
"description": command === "tags" ? "The main tags command. Check the help page for more info: https://projectlounge.pw/esmBot/help.html" : info.description,
"color": 16711680,
"fields": [{
"name": "Aliases",
"value": info.aliases ? info.aliases.join(", ") : "None"
}, {
"name": "Times Used",
"value": counts[command],
"inline": true
}, {
"name": "Parameters",
"value": command === "tags" ? "[name]" : (info.params ? (typeof info.params === "object" ? info.params.join(" ") : info.params) : "None"),
"inline": true
}]
}
};
return embed;
} else {
const pages = [];
for (const category of Object.keys(help.categories)) {
const splitPages = help.categories[category].map((item, index) => {
return index % 15 === 0 ? help.categories[category].slice(index, index + 15) : null;
}).filter((item) => {
return item;
});
const categoryStringArray = category.split("-");
for (const index of categoryStringArray.keys()) {
categoryStringArray[index] = categoryStringArray[index].charAt(0).toUpperCase() + categoryStringArray[index].slice(1);
}
for (const page of splitPages) {
pages.push({
title: categoryStringArray.join(" "),
page: page
});
}
}
const embeds = [];
for (const [i, value] of pages.entries()) {
embeds.push({
"embed": {
"author": {
"name": "esmBot Help",
"icon_url": this.client.user.avatarURL
},
"title": value.title,
"description": value.page.join("\n"),
"color": 16711680,
"footer": {
"text": `Page ${i + 1} of ${pages.length}`
},
"fields": [{
"name": "Prefix",
"value": this.message.channel.guild ? prefix : "N/A"
}, {
"name": "Tip",
"value": misc.random(tips)
}]
}
});
}
return paginator(this.client, this.message, embeds);
}
}
static description = "Gets a list of commands";
static aliases = ["commands"];
static arguments = ["{command}"];
}
module.exports = HelpCommand;

40
commands/general/image.js Normal file
View file

@ -0,0 +1,40 @@
const paginator = require("../../utils/pagination/pagination.js");
const { image_search } = require("duckduckgo-images-api");
const Command = require("../../classes/command.js");
class ImageSearchCommand extends Command {
async run() {
if (this.message.channel.guild && !this.message.channel.permissionsOf(this.client.user.id).has("addReactions")) return `${this.message.author.mention}, I don't have the \`Add Reactions\` permission!`;
if (this.message.channel.guild && !this.message.channel.permissionsOf(this.client.user.id).has("embedLinks")) return `${this.message.author.mention}, I don't have the \`Embed Links\` permission!`;
if (this.args.length === 0) return `${this.message.author.mention}, you need to provide something to search for!`;
const embeds = [];
const images = await image_search({ query: this.args.join(" "), moderate: true });
if (images.error && images.error.code === 403) return `${this.message.author.mention}, the daily search quota has been exceeded. Check back later.`;
if (images.length === 0) return `${this.message.author.mention}, I couldn't find any results!`;
for (const [i, value] of images.entries()) {
embeds.push({
"embed": {
"title": "Search Results",
"color": 16711680,
"footer": {
"text": `Page ${i + 1} of ${images.length}`
},
"image": {
"url": value.image
},
"author": {
"name": this.message.author.username,
"icon_url": this.message.author.avatarURL
}
}
});
}
return paginator(this.client, this.message, embeds);
}
static description = "Searches for images on DuckDuckGo";
static aliases = ["im", "photo", "img"];
static arguments = ["[query]"];
}
module.exports = ImageSearchCommand;

View file

@ -0,0 +1,30 @@
const image = require("../../utils/image.js");
const logger = require("../../utils/logger.js");
const Command = require("../../classes/command.js");
class ImageReloadCommand extends Command {
async run() {
if (this.message.author.id !== process.env.OWNER) return `${this.message.author.mention}, only the bot owner can reload the image servers!`;
await image.disconnect();
await image.repopulate();
let amount = 0;
for (const server of image.servers) {
try {
await image.connect(server);
amount += 1;
} catch (e) {
logger.error(e);
}
}
if (amount > 0) {
return `Successfully connected to ${amount} image servers.`;
} else {
return `${this.message.author.mention}, I couldn't connect to any image servers!`;
}
}
static description = "Attempts to reconnect to all available image processing servers";
static aliases = ["magickconnect", "magick"];
}
module.exports = ImageReloadCommand;

View file

@ -0,0 +1,31 @@
const image = require("../../utils/image.js");
const Command = require("../../classes/command.js");
class ImageStatsCommand extends Command {
async run() {
const embed = {
embed: {
"author": {
"name": "esmBot Image Statistics",
"icon_url": this.client.user.avatarURL
},
"color": 16711680,
"description": `The bot is currently connected to ${image.connections.length} image server(s).`,
"fields": []
}
};
const servers = await image.getStatus();
for (let i = 0; i < servers.length; i++) {
embed.embed.fields.push({
name: `Server ${i + 1}`,
value: `Running Jobs: ${servers[i].runningJobs}\nQueued: ${servers[i].queued}\nMax Jobs: ${servers[i].max}`
});
}
return embed;
}
static description = "Gets some statistics about the image servers";
static aliases = ["imgstat", "imstats", "imgstats", "imstat"];
}
module.exports = ImageStatsCommand;

46
commands/general/info.js Normal file
View file

@ -0,0 +1,46 @@
const { version } = require("../../package.json");
const Command = require("../../classes/command.js");
class InfoCommand extends Command {
async run() {
return {
"embed": {
"color": 16711680,
"author": {
"name": "esmBot Info/Credits",
"icon_url": this.client.user.avatarURL
},
"fields": [{
"name": " Version:",
"value": `v${version}${process.env.NODE_ENV === "development" ? "-dev" : ""}`
},
{
"name": "📝 Credits:",
"value": "Bot by **[Essem](https://essem.space)** and **[various contributors](https://github.com/esmBot/esmBot/graphs/contributors)**\nIcon by **[MintBorrow](https://mintborrow.newgrounds.com)**"
},
{
"name": "💬 Total Servers:",
"value": this.client.guilds.size
},
{
"name": "✅ Official Server:",
"value": "[Click here!](https://projectlounge.pw/support)"
},
{
"name": "💻 Source Code:",
"value": "[Click here!](https://github.com/esmBot/esmBot)"
},
{
"name": "🐦 Twitter:",
"value": "[Click here!](https://twitter.com/esmBot_)"
}
]
}
};
}
static description = "Gets some info and credits about me";
static aliases = ["botinfo", "credits"];
}
module.exports = InfoCommand;

View file

@ -0,0 +1,12 @@
const Command = require("../../classes/command.js");
class InviteCommand extends Command {
async run() {
return `${this.message.author.mention}, you can invite me to your server here: <https://projectlounge.pw/invite>`;
}
static description = "Gets my invite link";
static aliases = ["botinfo", "credits"];
}
module.exports = InviteCommand;

View file

@ -0,0 +1,22 @@
const urlCheck = require("../../utils/urlcheck.js");
const fetch = require("node-fetch");
const Command = require("../../classes/command.js");
class LengthenCommand extends Command {
async run() {
this.message.channel.sendTyping();
if (this.args.length === 0 || !urlCheck(this.args[0])) return `${this.message.author.mention}, you need to provide a short URL to lengthen!`;
if (urlCheck(this.args[0])) {
const url = await fetch(encodeURI(this.args[0]), { redirect: "manual" });
return url.headers.get("location") || this.args[0];
} else {
return `${this.message.author.mention}, that isn't a URL!`;
}
}
static description = "Lengthens a short URL";
static aliases = ["longurl", "lengthenurl", "longuri", "lengthenuri", "unshorten"];
static arguments = ["[url]"];
}
module.exports = LengthenCommand;

13
commands/general/ping.js Normal file
View file

@ -0,0 +1,13 @@
const Command = require("../../classes/command.js");
class PingCommand extends Command {
async run() {
const pingMessage = await this.message.channel.createMessage("🏓 Ping?");
return pingMessage.edit(`🏓 Pong!\n\`\`\`\nLatency: ${pingMessage.timestamp - this.message.timestamp}ms${this.message.channel.guild ? `\nShard Latency: ${Math.round(this.client.shards.get(this.client.guildShardMap[this.message.channel.guild.id]).latency)}ms` : ""}\n\`\`\``);
}
static description = "Pings Discord's servers";
static aliases = ["pong"];
}
module.exports = PingCommand;

View file

@ -0,0 +1,22 @@
const database = require("../../utils/database.js");
const Command = require("../../classes/command.js");
class PrefixCommand extends Command {
async run() {
if (!this.message.channel.guild) return `${this.message.author.mention}, this command only works in servers!`;
const guild = await database.getGuild(this.message.channel.guild.id);
if (this.args.length !== 0) {
if (!this.message.member.permissions.has("administrator") && this.message.member.id !== process.env.OWNER) return `${this.message.author.mention}, you need to be an administrator to change the bot prefix!`;
await database.setPrefix(this.args[0], this.message.channel.guild);
return `The prefix has been changed to ${this.args[0]}.`;
} else {
return `${this.message.author.mention}, the current prefix is \`${guild.prefix}\`.`;
}
}
static description = "Checks/changes the server prefix";
static aliases = ["setprefix", "changeprefix", "checkprefix"];
static arguments = ["{prefix}"];
}
module.exports = PrefixCommand;

View file

@ -0,0 +1,37 @@
const qrcode = require("qrcode");
const stream = require("stream");
const Command = require("../../classes/command.js");
class QrCreateCommand extends Command {
async run() {
if (this.args.length === 0) return `${this.message.author.mention}, you need to provide some text to generate a QR code!`;
this.message.channel.sendTyping();
const writable = new stream.PassThrough();
qrcode.toFileStream(writable, this.content, { margin: 1 });
const file = await this.streamToBuf(writable);
return {
file: file,
name: "qr.png"
};
}
streamToBuf(stream) {
return new Promise((resolve, reject) => {
const chunks = [];
stream.on("data", (chunk) => {
chunks.push(chunk);
});
stream.once("error", (error) => {
reject(error);
});
stream.once("end", () => {
resolve(Buffer.concat(chunks));
});
});
}
static description = "Generates a QR code";
static arguments = ["[text]"];
}
module.exports = QrCreateCommand;

View file

@ -0,0 +1,22 @@
const jsqr = require("jsqr");
const fetch = require("node-fetch");
const sharp = require("sharp");
const { clean } = require("../../utils/misc.js");
const Command = require("../../classes/command.js");
class QrReadCommand extends Command {
async run() {
const image = await require("../../utils/imagedetect.js")(this.client, this.message);
if (image === undefined) return `${this.message.author.mention}, you need to provide an image with a QR code to read!`;
this.message.channel.sendTyping();
const data = await (await fetch(image.path)).buffer();
const rawData = await sharp(data).ensureAlpha().raw().toBuffer({ resolveWithObject: true });
const qrBuffer = jsqr(rawData.data, rawData.info.width, rawData.info.height);
if (!qrBuffer) return `${this.message.author.mention}, I couldn't find a QR code!`;
return `\`\`\`\n${await clean(qrBuffer.data)}\n\`\`\``;
}
static description = "Reads a QR code";
}
module.exports = QrReadCommand;

View file

@ -0,0 +1,20 @@
const handler = require("../../utils/handler.js");
const collections = require("../../utils/collections.js");
const Command = require("../../classes/command.js");
class ReloadCommand extends Command {
async run() {
if (this.message.author.id !== process.env.OWNER) return `${this.message.author.mention}, only the bot owner can reload commands!`;
if (this.args.length === 0) return `${this.message.author.mention}, you need to provide a command to reload!`;
const result = await handler.unload(this.args[0]);
if (result) return result;
const result2 = await handler.load(collections.paths.get(this.args[0]));
if (result2) return result2;
return `${this.message.author.mention}, the command \`${this.args[0]}\` has been reloaded.`;
}
static description = "Reloads a command";
static arguments = ["[command]"];
}
module.exports = ReloadCommand;

View file

@ -0,0 +1,19 @@
const handler = require("../../utils/handler.js");
const collections = require("../../utils/collections.js");
const Command = require("../../classes/command.js");
class RestartCommand extends Command {
async run() {
if (this.message.author.id !== process.env.OWNER) return `${this.message.author.mention}, only the bot owner can restart me!`;
await this.message.channel.createMessage(`${this.message.author.mention}, esmBot is restarting.`);
for (const command of collections.commands) {
await handler.unload(command);
}
process.exit(1);
}
static description = "Restarts me";
static aliases = ["reboot"];
}
module.exports = RestartCommand;

View file

@ -0,0 +1,52 @@
const Command = require("../../classes/command.js");
class ServerInfoCommand extends Command {
async run() {
if (!this.message.channel.guild) return `${this.message.author.mention}, this command only works in servers!`;
const owner = await this.message.channel.guild.members.get(this.message.channel.guild.ownerID);
return {
"embed": {
"title": this.message.channel.guild.name,
"thumbnail": {
"url": this.message.channel.guild.iconURL
},
"color": 16711680,
"fields": [
{
"name": "🔢 **ID:**",
"value": this.message.channel.guild.id
},
{
"name": "👤 **Owner:**",
"value": owner ? `${owner.user.username}#${owner.user.discriminator}` : this.message.channel.guild.ownerID
},
{
"name": "🗺 **Region:**",
"value": this.message.channel.guild.region
},
{
"name": "🗓 **Created on:**",
"value": new Date(this.message.channel.guild.createdAt).toString()
},
{
"name": "👥 **Users:**",
"value": this.message.channel.guild.memberCount
},
{
"name": "💬 **Channels:**",
"value": this.message.channel.guild.channels.size
},
{
"name": "😃 **Emojis:**",
"value": this.message.channel.guild.emojis.length
}
]
}
};
}
static description = "Gets some info about the server";
static aliases = ["server"];
}
module.exports = ServerInfoCommand;

View file

@ -0,0 +1,15 @@
const Command = require("../../classes/command.js");
class SnowflakeCommand extends Command {
async run() {
if (!this.args[0]) return `${this.message.author.mention}, you need to provide a snowflake ID!`;
if (!this.args[0].match(/^<?[@#]?[&!]?\d+>?$/) && this.args[0] < 21154535154122752) return `${this.message.author.mention}, that's not a valid snowflake!`;
return new Date((this.args[0].replaceAll("@", "").replaceAll("#", "").replaceAll("!", "").replaceAll("&", "").replaceAll("<", "").replaceAll(">", "") / 4194304) + 1420070400000).toUTCString();
}
static description = "Converts a Discord snowflake id into a timestamp";
static aliases = ["timestamp", "snowstamp", "snow"];
static arguments = ["[id]"];
}
module.exports = SnowflakeCommand;

View file

@ -0,0 +1,20 @@
const soundPlayer = require("../../utils/soundplayer.js");
const Command = require("../../classes/command.js");
class SoundReloadCommand extends Command {
async run() {
if (this.message.author.id !== process.env.OWNER) return `${this.message.author.mention}, only the bot owner can reload Lavalink!`;
const soundStatus = await soundPlayer.checkStatus();
if (!soundStatus) {
const length = await soundPlayer.connect(this.client);
return `Successfully connected to ${length} Lavalink node(s).`;
} else {
return `${this.message.author.mention}, I couldn't connect to any Lavalink nodes!`;
}
}
static description = "Attempts to reconnect to all available Lavalink nodes";
static aliases = ["lava", "lavalink", "lavaconnect", "soundconnect"];
}
module.exports = SoundReloadCommand;

59
commands/general/stats.js Normal file
View file

@ -0,0 +1,59 @@
const { version } = require("../../package.json");
const day = require("dayjs");
day.extend(require("dayjs/plugin/duration"));
const os = require("os");
const Command = require("../../classes/command.js");
class StatsCommand extends Command {
async run() {
const duration = day.duration(this.client.uptime).format(" D [days], H [hrs], m [mins], s [secs]");
const uptime = day.duration(process.uptime(), "seconds").format(" D [days], H [hrs], m [mins], s [secs]");
return {
embed: {
"author": {
"name": "esmBot Statistics",
"icon_url": this.client.user.avatarURL
},
"color": 16711680,
"fields": [{
"name": "Version",
"value": `v${version}${process.env.NODE_ENV === "development" ? "-dev" : ""}`
},
{
"name": "Memory Usage",
"value": `${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)} MB`
},
{
"name": "Shard",
"value": this.client.guildShardMap[this.message.channel.guild.id]
},
{
"name": "Uptime",
"value": uptime
},
{
"name": "Connection Uptime",
"value": duration
},
{
"name": "Host",
"value": `${os.type()} ${os.release()} (${os.arch()})`
},
{
"name": "Library",
"value": `Eris ${require("eris").VERSION}`
},
{
"name": "Node.js Version",
"value": process.version
}
]
}
};
}
static description = "Gets some statistics about me";
static aliases = ["status", "stat"];
}
module.exports = StatsCommand;

View file

@ -0,0 +1,57 @@
const Command = require("../../classes/command.js");
class UserInfoCommand extends Command {
async run() {
const getUser = this.message.mentions.length >= 1 ? this.message.mentions[0] : (this.args.length !== 0 ? this.client.users.get(this.args[0]) : this.message.author);
let user;
if (getUser) {
user = getUser;
} else if (this.args.join(" ") !== "") {
const userRegex = new RegExp(this.args.join("|"), "i");
const member = this.client.users.find(element => {
return userRegex.test(element.username);
});
user = member ? member : this.message.author;
} else {
user = this.message.author;
}
const member = this.message.channel.guild ? this.message.channel.guild.members.get(user.id) : undefined;
return {
"embed": {
"title": `${user.username}#${user.discriminator}`,
"thumbnail": {
"url": user.avatarURL
},
"color": 16711680,
"fields": [
{
"name": "🔢 **ID:**",
"value": user.id
},
{
"name": "📛 **Nickname:**",
"value": member ? (member.nick ? member.nick : "None") : "N/A"
},
{
"name": "🤖 **Bot:**",
"value": user.bot ? "Yes" : "No"
},
{
"name": "🗓️ **Joined Discord on:**",
"value": new Date(user.createdAt).toString()
},
{
"name": "💬 **Joined this server on:**",
"value": member ? new Date(member.joinedAt).toString() : "N/A"
}
]
}
};
}
static description = "Gets info about a user";
static aliases = ["user"];
static arguments = ["[mention/id]"];
}
module.exports = UserInfoCommand;

View file

@ -0,0 +1,32 @@
const fetch = require("node-fetch");
const { decodeEntities } = require("../../utils/misc.js");
const paginator = require("../../utils/pagination/pagination.js");
const Command = require("../../classes/command.js");
class YouTubeCommand extends Command {
async run() {
if (this.args.length === 0) return `${this.message.author.mention}, you need to provide something to search for!`;
this.message.channel.sendTyping();
const messages = [];
const request = await fetch(`https://www.googleapis.com/youtube/v3/search?part=snippet&q=${encodeURIComponent(this.args.join(" "))}&key=${process.env.GOOGLE}&maxResults=50`);
const result = await request.json();
if (result.error && result.error.code === 403) return `${this.message.author.mention}, I've exceeded my YouTube API search quota for the day. Check back later.`;
for (const [i, value] of result.items.entries()) {
if (value.id.kind === "youtube#channel") {
messages.push(`Page ${i + 1} of ${result.items.length}\n<:youtube:637020823005167626> **${decodeEntities(value.snippet.title).replaceAll("*", "\\*")}**\nhttps://youtube.com/channel/${value.id.channelId}`);
} else if (value.id.kind === "youtube#playlist") {
messages.push(`Page ${i + 1} of ${result.items.length}\n<:youtube:637020823005167626> **${decodeEntities(value.snippet.title).replaceAll("*", "\\*")}**\nCreated by **${decodeEntities(value.snippet.channelTitle).replaceAll("*", "\\*")}**\nhttps://youtube.com/playlist?list=${value.id.playlistId}`);
} else {
messages.push(`Page ${i + 1} of ${result.items.length}\n<:youtube:637020823005167626> **${decodeEntities(value.snippet.title).replaceAll("*", "\\*")}**\nUploaded by **${decodeEntities(value.snippet.channelTitle).replaceAll("*", "\\*")}** on **${value.snippet.publishedAt.split("T")[0]}**\nhttps://youtube.com/watch?v=${value.id.videoId}`);
}
}
return paginator(this.client, this.message, messages);
}
static description = "Searches YouTube";
static aliases = ["yt", "video", "ytsearch"];
static arguments = ["[query]"];
static requires = "google";
}
module.exports = YouTubeCommand;