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

@ -3,12 +3,19 @@
"es6": true,
"node": true
},
"extends": ["eslint:recommended", "plugin:promise/recommended"],
"extends": ["eslint:recommended"],
"parser": "@babel/eslint-parser",
"parserOptions": {
"sourceType": "module",
"ecmaVersion": 2019
"ecmaVersion": 12,
"requireConfigFile": false,
"babelOptions": {
"plugins": [
"@babel/plugin-proposal-class-properties"
]
}
},
"plugins": ["promise"],
"plugins": ["@babel"],
"rules": {
"no-console": "off",
"indent": [

View File

@ -30,19 +30,16 @@ const acceptJob = async (uuid, sock) => {
msg: jobs[uuid].msg,
num: jobs[uuid].num
}, sock);
jobAmount--;
if (queue.length > 0) {
acceptJob(queue[0], sock);
}
log(`Job ${uuid} has finished`);
} catch (err) {
console.error(`Error on job ${uuid}:`, err);
delete jobs[uuid];
sock.write(Buffer.concat([Buffer.from([0x2]), Buffer.from(uuid), Buffer.from(err.message)]));
} finally {
jobAmount--;
if (queue.length > 0) {
acceptJob(queue[0], sock);
}
delete jobs[uuid];
sock.write(Buffer.concat([Buffer.from([0x2]), Buffer.from(uuid), Buffer.from(err.toString())]));
}
};
@ -55,6 +52,22 @@ const httpServer = http.createServer((req, res) => {
if (reqUrl.pathname === "/status") {
log(`Sending server status to ${req.socket.remoteAddress}:${req.socket.remotePort} via HTTP`);
return res.end(Buffer.from((MAX_JOBS - jobAmount).toString()));
} else if (reqUrl.pathname === "/running") {
log(`Sending currently running jobs to ${req.socket.remoteAddress}:${req.socket.remotePort} via HTTP`);
const keys = Object.keys(jobs);
const newObject = { queued: queue.length, runningJobs: jobAmount, max: MAX_JOBS };
for (const key of keys) {
const validKeys = Object.keys(jobs[key]).filter((value) => value !== "addr" && value !== "port" && value !== "data" && value !== "ext");
newObject[key] = {};
for (const validKey of validKeys) {
if (validKey === "msg") {
newObject[key][validKey] = JSON.parse(jobs[key][validKey]);
} else {
newObject[key][validKey] = jobs[key][validKey];
}
}
}
return res.end(JSON.stringify(newObject));
} else if (reqUrl.pathname === "/image") {
if (!reqUrl.searchParams.has("id")) {
res.statusCode = 400;
@ -134,7 +147,7 @@ const server = net.createServer((sock) => { // Create a TCP socket/server to lis
for (const job of Object.keys(jobs)) {
if (jobs[job].addr === sock.remoteAddress && jobs[job].port === sock.remotePort) {
delete jobs[job];
sock.write(Buffer.concat([Buffer.from([0x2]), Buffer.from(job), Buffer.from("Job ended prematurely")]));
sock.write(Buffer.concat([Buffer.from([0x2]), Buffer.from(job), Buffer.from("Job ended prematurely (not really an error; just run your image job again)")]));
}
}
process.exit(0);
@ -149,28 +162,22 @@ server.listen(8080, () => {
log("TCP listening on port 8080");
});
const runJob = (job, sock) => {
return new Promise((resolve, reject) => {
log(`Job ${job.uuid} starting...`, job.num);
const runJob = async (job, sock) => {
log(`Job ${job.uuid} starting...`, job.num);
const object = JSON.parse(job.msg);
// If the image has a path, it must also have a type
if (object.path && !object.type) {
reject(new TypeError("Unknown image type"));
}
const object = JSON.parse(job.msg);
// If the image has a path, it must also have a type
if (object.path && !object.type) {
throw new TypeError("Unknown image type");
}
log(`Job ${job.uuid} started`, job.num);
run(object).then((data) => {
log(`Sending result of job ${job.uuid} back to the bot`, job.num);
jobs[job.uuid].data = data.buffer;
jobs[job.uuid].ext = data.fileExtension;
sock.write(Buffer.concat([Buffer.from([0x1]), Buffer.from(job.uuid)]), (e) => {
if (e) return reject(e);
return resolve();
});
return;
}).catch(e => {
reject(e);
});
log(`Job ${job.uuid} started`, job.num);
const data = await run(object).catch(e => { throw e; });
log(`Sending result of job ${job.uuid} back to the bot`, job.num);
jobs[job.uuid].data = data.buffer;
jobs[job.uuid].ext = data.fileExtension;
sock.write(Buffer.concat([Buffer.from([0x1]), Buffer.from(job.uuid)]), (e) => {
if (e) throw e;
});
return;
};

98
app.js
View File

@ -1,80 +1,34 @@
if (process.platform === "win32") console.log("\x1b[1m\x1b[31m\x1b[40m" + `WIN32 IS NOT OFFICIALLY SUPPORTED!
if (process.platform === "win32") console.error("\x1b[1m\x1b[31m\x1b[40m" + `WIN32 IS NOT OFFICIALLY SUPPORTED!
Although there's a (very) slim chance of it working, multiple aspects of the bot are built with UNIX-like systems in mind and could break on Win32-based systems. If you want to run the bot on Windows, using Windows Subsystem for Linux is highly recommended.
The bot will continue to run past this message, but keep in mind that it could break at any time. Continue running at your own risk; alternatively, stop the bot using Ctrl+C and install WSL.` + "\x1b[0m");
// load config from .env file
require("dotenv").config();
// turn fs.readdir into a promise
const readdir = require("util").promisify(require("fs").readdir);
// fancy loggings
const logger = require("./utils/logger.js");
// start the client
const client = require("./utils/client.js");
// initialize command loader
const handler = require("./utils/handler.js");
const sound = require("./utils/soundplayer.js");
const image = require("./utils/image.js");
const { Master } = require("eris-sharder");
// registers stuff and connects the bot
async function init() {
logger.log("info", "Starting esmBot...");
// register commands and their info
const commands = await readdir("./commands/");
const soundStatus = await sound.checkStatus();
logger.log("info", `Attempting to load ${commands.length} commands...`);
for (const commandFile of commands) {
logger.log("info", `Loading command ${commandFile}...`);
try {
await handler.load(commandFile, soundStatus);
} catch (e) {
logger.error(`Failed to register command ${commandFile.split(".")[0]}: ${e}`);
}
new Master(`Bot ${process.env.TOKEN}`, "/shard.js", {
name: "esmBot",
clientOptions: {
disableEvents: {
CHANNEL_DELETE: true,
CHANNEL_UPDATE: true,
GUILD_BAN_REMOVE: true,
GUILD_MEMBER_ADD: true,
GUILD_MEMBER_REMOVE: true,
GUILD_MEMBER_UPDATE: true,
GUILD_ROLE_CREATE: true,
GUILD_ROLE_DELETE: true,
GUILD_ROLE_UPDATE: true,
TYPING_START: true,
MESSAGE_DELETE_BULK: true
},
allowedMentions: {
everyone: false,
roles: false,
users: true,
repliedUser: true
},
guildSubscriptions: false
}
// register events
const events = await readdir("./events/");
logger.log("info", `Attempting to load ${events.length} events...`);
for (const file of events) {
logger.log("info", `Loading event ${file}...`);
const eventName = file.split(".")[0];
const event = require(`./events/${file}`);
client.on(eventName, event);
}
// connect to image api if enabled
if (process.env.API === "true") {
for (const server of image.servers) {
try {
await image.connect(server);
} catch (e) {
logger.error(e);
}
}
}
// login
client.connect();
// post to DBL
if (process.env.NODE_ENV === "production" && process.env.DBL !== "") {
require("./utils/dbl.js");
}
// handle ctrl+c and pm2 stop
process.on("SIGINT", () => {
logger.log("info", "SIGINT detected, shutting down...");
client.editStatus("dnd", {
name: "Restarting/shutting down..."
});
for (const command of commands) {
handler.unload(command);
}
client.disconnect();
require("./utils/database.js").stop();
process.exit(0);
});
}
// launch the bot
init();
});

19
classes/command.js Normal file
View File

@ -0,0 +1,19 @@
class Command {
constructor(client, message, args, content) {
this.client = client;
this.message = message;
this.args = args;
this.content = content;
}
async run() {
return "It works!";
}
static description = "No description found";
static aliases = [];
static arguments = [];
static requires = [];
}
module.exports = Command;

125
classes/imageCommand.js Normal file
View File

@ -0,0 +1,125 @@
const Command = require("./command.js");
const magick = require("../utils/image.js");
const imageDetect = require("../utils/imagedetect.js");
const collections = require("../utils/collections.js");
class ImageCommand extends Command {
/*this.embed = {
"title": "Your image is being generated! (PRELIMINARY EMBED)",
"description": "The current progress is outlined below:",
"color": 16711680,
"footer": {
"text": "Step 2/3"
},
"author": {
"name": "Processing...",
"icon_url": "https://cdn.discordapp.com/avatars/429305856241172480/a20f739886ae47cfb10fa069416e8ed3.jpg"
},
"fields": [
{
"name": "Downloading...",
"value": "✅ Done!"
},
{
"name": "Processing...",
"value": "<a:processing:818243325891051581> In progress"
},
{
"name": "Uploading...",
"value": "<a:processing:818243325891051581> Waiting for previous steps to complete"
}
]
};*/
criteria() {
return true;
}
async run() {
// check if this command has already been run in this channel with the same arguments, and we are awaiting its result
// if so, don't re-run it
if (collections.runningCommands.has(this.message.author.id) && (new Date(collections.runningCommands.get(this.message.author.id)) - new Date(this.message.createdAt)) < 5000) {
return `${this.message.author.mention}, please slow down a bit.`;
}
// before awaiting the command result, add this command to the set of running commands
collections.runningCommands.set(this.message.author.id, this.message.createdAt);
const magickParams = {
cmd: this.constructor.command
};
if (this.constructor.requiresImage) {
try {
const image = await imageDetect(this.client, this.message);
if (image === undefined) {
collections.runningCommands.delete(this.message.author.id);
return `${this.message.author.mention}, ${this.constructor.noImage}`;
}
magickParams.path = image.path;
magickParams.type = image.type;
magickParams.url = image.url; // technically not required but can be useful for text filtering
magickParams.delay = image.delay;
if (this.constructor.requiresGIF) magickParams.onlyGIF = true;
} catch (e) {
collections.runningCommands.delete(this.message.author.id);
throw e;
}
}
if (this.constructor.requiresText) {
if (this.args.length === 0 || !this.criteria(this.args)) {
collections.runningCommands.delete(this.message.author.id);
return `${this.message.author.mention}, ${this.constructor.noText}`;
}
}
switch (typeof this.params) {
case "function":
Object.assign(magickParams, this.params(this.args, magickParams.url));
break;
case "object":
Object.assign(magickParams, this.params);
break;
}
let status;
if (magickParams.type === "image/gif") {
status = await this.processMessage(this.message);
} else {
this.message.channel.sendTyping();
}
try {
const { buffer, type } = await magick.run(magickParams).catch(e => {
throw e;
});
if (status && status.channel.messages.get(status.id)) await status.delete();
if (type === "nogif" && this.constructor.requiresGIF) return `${this.message.author.mention}, that isn't a GIF!`;
return {
file: buffer,
name: `${this.constructor.command}.${type}`
};
} catch (e) {
if (status && status.channel.messages.get(status.id)) await status.delete();
if (e.toString().includes("Not connected to image server")) return `${this.message.author.mention}, I'm still trying to connect to the image servers. Please wait a little bit.`;
throw e;
} finally {
collections.runningCommands.delete(this.message.author.id);
}
}
processMessage(message) {
return message.channel.createMessage(`${process.env.PROCESSING_EMOJI || "<a:processing:479351417102925854>"} Processing... This might take a while`);
}
static requiresImage = true;
static requiresText = false;
static requiresGIF = false;
static noImage = "you need to provide an image!";
static noText = "you need to provide some text!";
static command = "";
}
module.exports = ImageCommand;

13
classes/musicCommand.js Normal file
View File

@ -0,0 +1,13 @@
const Command = require("./command.js");
const soundPlayer = require("../utils/soundplayer.js");
class MusicCommand extends Command {
constructor(client, message, args, content) {
super(client, message, args, content);
this.connection = soundPlayer.players.get(message.channel.guild.id);
}
static requires = ["sound"];
}
module.exports = MusicCommand;

View File

@ -1,32 +0,0 @@
const { random } = require("../utils/misc.js");
const responses = [
"It is certain",
"It is decidedly so",
"Without a doubt",
"Yes, definitely",
"You may rely on it",
"As I see it, yes",
"Most likely",
"Outlook good",
"Yes",
"Signs point to yes",
"Reply hazy, try again",
"Ask again later",
"Better not tell you now",
"Cannot predict now",
"Concentrate and ask again",
"Don't count on it",
"My reply is no",
"My sources say no",
"Outlook not so good",
"Very doubtful"
];
exports.run = async () => {
return `🎱 ${random(responses)}`;
};
exports.aliases = ["magicball", "magikball", "magic8ball", "magik8ball", "eightball"];
exports.category = 4;
exports.help = "Asks the magic 8-ball a question";
exports.params = "{text}";

View File

@ -1,22 +0,0 @@
const magick = require("../utils/image.js");
exports.run = async (message) => {
message.channel.sendTyping();
const image = await require("../utils/imagedetect.js")(message);
if (image === undefined) return `${message.author.mention}, you need to provide an image to add a 9GAG watermark!`;
const { buffer, type } = await magick.run({
cmd: "watermark",
path: image.path,
water: "./assets/images/9gag.png",
gravity: 6,
type: image.type
});
return {
file: buffer,
name: `9gag.${type}`
};
};
exports.aliases = ["ninegag", "gag"];
exports.category = 5;
exports.help = "Adds the 9gag watermark to an image";

View File

@ -1,22 +0,0 @@
const client = require("../utils/client.js");
exports.run = async (message, args) => {
if (message.mentions[0] !== undefined) {
return message.mentions[0].dynamicAvatarURL(null, 1024);
} else if (client.users.get(args[0]) !== undefined) {
return client.users.get(args[0]).dynamicAvatarURL(null, 1024);
} else if (args.join(" ") !== "" && message.channel.guild) {
const userRegex = new RegExp(args.join("|"), "i");
const member = 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) : message.author.dynamicAvatarURL(null, 1024);
} else {
return message.author.dynamicAvatarURL(null, 1024);
}
};
exports.aliases = ["pfp", "ava"];
exports.category = 1;
exports.help = "Gets a user's avatar";
exports.params = "{mention/id}";

View File

@ -1,23 +0,0 @@
const magick = require("../utils/image.js");
exports.run = async (message) => {
message.channel.sendTyping();
const image = await require("../utils/imagedetect.js")(message);
if (image === undefined) return `${message.author.mention}, you need to provide an image to add a Bandicam watermark!`;
const { buffer, type } = await magick.run({
cmd: "watermark",
path: image.path,
water: "./assets/images/bandicam.png",
gravity: 2,
resize: true,
type: image.type
});
return {
file: buffer,
name: `bandicam.${type}`
};
};
exports.aliases = ["bandi"];
exports.category = 5;
exports.help = "Adds the Bandicam watermark to an image";

View File

@ -1,19 +0,0 @@
const fetch = require("node-fetch");
exports.run = async (message) => {
message.channel.sendTyping();
const imageData = await fetch("http://shibe.online/api/birds");
const json = await imageData.json();
return {
embed: {
color: 16711680,
image: {
url: json[0]
}
}
};
};
exports.aliases = ["birb", "birds", "birbs"];
exports.category = 4;
exports.help = "Gets a random bird picture";

View File

@ -1,20 +0,0 @@
const magick = require("../utils/image.js");
exports.run = async (message) => {
message.channel.sendTyping();
const image = await require("../utils/imagedetect.js")(message);
if (image === undefined) return `${message.author.mention}, you need to provide an image to blur!`;
const { buffer, type } = await magick.run({
cmd: "blur",
path: image.path,
sharp: false,
type: image.type
});
return {
file: buffer,
name: `blur.${type}`
};
};
exports.category = 5;
exports.help = "Blurs an image";

View File

@ -1,20 +0,0 @@
const magick = require("../utils/image.js");
exports.run = async (message) => {
message.channel.sendTyping();
const image = await require("../utils/imagedetect.js")(message);
if (image === undefined) return `${message.author.mention}, you need to provide an image to make blurple!`;
const { buffer, type } = await magick.run({
cmd: "blurple",
path: image.path,
type: image.type
});
return {
file: buffer,
name: `blurple.${type}`
};
};
exports.aliases = ["blurp"];
exports.category = 5;
exports.help = "Turns an image blurple";

View File

@ -1,10 +0,0 @@
const soundPlayer = require("../utils/soundplayer.js");
exports.run = async (message) => {
return await soundPlayer.play("./assets/audio/boi.ogg", message);
};
exports.aliases = ["boy", "neutron", "hugh"];
exports.category = 6;
exports.help = "Plays the \"boi\" sound effect";
exports.requires = "sound";

View File

@ -1,10 +0,0 @@
const soundPlayer = require("../utils/soundplayer.js");
exports.run = async (message) => {
return await soundPlayer.play("./assets/audio/boom.ogg", message);
};
exports.aliases = ["thud", "vine"];
exports.category = 6;
exports.help = "Plays the Vine boom sound effect";
exports.requires = "sound";

View File

@ -1,10 +0,0 @@
const soundPlayer = require("../utils/soundplayer.js");
exports.run = async (message) => {
return await soundPlayer.play("./assets/audio/bruh.ogg", message);
};
exports.aliases = ["bro"];
exports.category = 6;
exports.help = "Plays the \"bruh\" sound effect";
exports.requires = "sound";

View File

@ -1,24 +0,0 @@
const magick = require("../utils/image.js");
exports.run = async (message, args) => {
const image = await require("../utils/imagedetect.js")(message);
if (image === undefined) return `${message.author.mention}, you need to provide an image/GIF to add a caption!`;
const newArgs = args.filter(item => !item.includes(image.url) );
if (args.length === 0) return `${message.author.mention}, you need to provide some text to add a caption!`;
const processMessage = await message.channel.createMessage(`${process.env.PROCESSING_EMOJI || "<a:processing:479351417102925854>"} Processing... This might take a while`);
const { buffer, type } = await magick.run({
cmd: "caption",
path: image.path,
caption: newArgs.join(" ").replaceAll("&", "\\&amp;").replaceAll(">", "\\&gt;").replaceAll("<", "\\&lt;").replaceAll("\"", "\\&quot;").replaceAll("'", "\\&apos;").replaceAll("%", "\\%"),
type: image.type
});
if (processMessage.channel.messages.get(processMessage.id)) await processMessage.delete();
return {
file: buffer,
name: `caption.${type}`
};
};
exports.aliases = ["gifc", "gcaption", "ifcaption", "ifunnycaption"];
exports.category = 5;
exports.help = "Adds a caption to an image/GIF";

View File

@ -1,24 +0,0 @@
const magick = require("../utils/image.js");
const words = ["me irl", "dank", "follow my second account @esmBot_", "2016", "meme", "wholesome", "reddit", "instagram", "twitter", "facebook", "fortnite", "minecraft", "relatable", "gold", "funny", "template", "hilarious", "memes", "deep fried", "2020", "leafy", "pewdiepie"];
exports.run = async (message, args) => {
const image = await require("../utils/imagedetect.js")(message);
if (image === undefined) return `${message.author.mention}, you need to provide an image/GIF to add a caption!`;
const newArgs = args.filter(item => !item.includes(image.url) );
const processMessage = await message.channel.createMessage(`${process.env.PROCESSING_EMOJI || "<a:processing:479351417102925854>"} Processing... This might take a while`);
const { buffer, type } = await magick.run({
cmd: "captionTwo",
path: image.path,
caption: newArgs.length !== 0 ? newArgs.join(" ").replaceAll("&", "\\&amp;").replaceAll(">", "\\&gt;").replaceAll("<", "\\&lt;").replaceAll("\"", "\\&quot;").replaceAll("'", "\\&apos;").replaceAll("%", "\\%") : words.sort(() => 0.5 - Math.random()).slice(0, Math.floor(Math.random() * words.length + 1)).join(" "),
type: image.type
});
if (processMessage.channel.messages.get(processMessage.id)) await processMessage.delete();
return {
file: buffer,
name: `caption2.${type}`
};
};
exports.aliases = ["tags2", "meirl", "memecaption", "medotmecaption"];
exports.category = 5;
exports.help = "Adds a me.me caption/tag list to an image/GIF";

View File

@ -1,24 +0,0 @@
const fetch = require("node-fetch");
exports.run = async (message) => {
message.channel.sendTyping();
const data = await fetch("https://api.thecatapi.com/v1/images/search?format=json", {
headers: {
"x-api-key": process.env.CAT
}
});
const json = await data.json();
return {
embed: {
color: 16711680,
image: {
url: json[0].url
}
}
};
};
exports.aliases = ["kitters", "kitties", "kitty", "cattos", "catto", "cats"];
exports.category = 4;
exports.help = "Gets a random cat picture";
exports.requires = "cat";

View File

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

View File

@ -1,20 +0,0 @@
const magick = require("../utils/image.js");
exports.run = async (message) => {
message.channel.sendTyping();
const image = await require("../utils/imagedetect.js")(message);
if (image === undefined) return `${message.author.mention}, you need to provide an image to add radial blur!`;
const { buffer, type} = await magick.run({
cmd: "circle",
path: image.path,
type: image.type
});
return {
file: buffer,
name: `circle.${type}`
};
};
exports.aliases = ["cblur", "radial", "radialblur"];
exports.category = 5;
exports.help = "Applies a radial blur effect on an image";

View File

@ -1,42 +0,0 @@
const client = require("../utils/client.js");
const paginator = require("../utils/pagination/pagination.js");
const database = require("../utils/database.js");
exports.run = async (message) => {
if (message.channel.guild && !message.channel.guild.members.get(client.user.id).permissions.has("addReactions") && !message.channel.permissionsOf(client.user.id).has("addReactions")) return `${message.author.mention}, I don't have the \`Add Reactions\` permission!`;
if (message.channel.guild && !message.channel.guild.members.get(client.user.id).permissions.has("embedLinks") && !message.channel.permissionsOf(client.user.id).has("embedLinks")) return `${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": message.author.username,
"icon_url": message.author.avatarURL
}
}
});
}
return paginator(message, embeds);
};
exports.category = 1;
exports.help = "Gets how many times every command was used";

View File

@ -1,20 +0,0 @@
const cowsay = require("cowsay2");
const cows = require("cowsay2/cows");
exports.run = async (message, args) => {
if (args.length === 0) {
return `${message.author.mention}, you need to provide some text for the cow to say!`;
} else if (cows[args[0].toLowerCase()] != undefined) {
const cow = cows[args.shift().toLowerCase()];
return `\`\`\`\n${cowsay.say(args.join(" "), {
cow
})}\n\`\`\``;
} else {
return `\`\`\`\n${cowsay.say(args.join(" "))}\n\`\`\``;
}
};
exports.aliases = ["cow"];
exports.category = 4;
exports.help = "Makes an ASCII cow say a message";
exports.params = "[text]";

View File

@ -1,19 +0,0 @@
const magick = require("../utils/image.js");
exports.run = async (message) => {
message.channel.sendTyping();
const image = await require("../utils/imagedetect.js")(message);
if (image === undefined) return `${message.author.mention}, you need to provide an image to crop!`;
const { buffer, type } = await magick.run({
cmd: "crop",
path: image.path,
type: image.type
});
return {
file: buffer,
name: `crop.${type}`
};
};
exports.category = 5;
exports.help = "Crops an image to 1:1";

View File

@ -1,7 +0,0 @@
exports.run = async (message) => {
return `${message.author.mention}, my DBL page can be found here: <https://top.gg/bot/429305856241172480>`;
};
exports.aliases = ["discordbotlist", "botlist", "discordbots"];
exports.category = 1;
exports.help = "Gets my top.gg page";

View File

@ -1,12 +0,0 @@
const { clean } = require("../utils/misc.js");
exports.run = async (message, args) => {
if (args.length === 0) return `${message.author.mention}, you need to provide a string to decode!`;
const b64Decoded = Buffer.from(args.join(" "), "base64").toString("utf-8");
return `\`\`\`\n${await clean(b64Decoded)}\`\`\``;
};
exports.aliases = ["b64decode", "base64decode"];
exports.category = 1;
exports.help = "Decodes a Base64 string";
exports.params = "[text]";

View File

@ -1,23 +0,0 @@
const magick = require("../utils/image.js");
exports.run = async (message) => {
message.channel.sendTyping();
const image = await require("../utils/imagedetect.js")(message);
if (image === undefined) return `${message.author.mention}, you need to provide an image to add a DeviantArt watermark!`;
const { buffer, type } = await magick.run({
cmd: "watermark",
path: image.path,
water: "./assets/images/deviantart.png",
gravity: 5,
resize: true,
type: image.type
});
return {
file: buffer,
name: `deviantart.${type}`
};
};
exports.aliases = ["da", "deviant"];
exports.category = 5;
exports.help = "Adds a DeviantArt watermark to an image";

View File

@ -1,12 +0,0 @@
exports.run = async (message, args) => {
if (args.length === 0 || !args[0].match(/^\d+$/)) {
return `🎲 The dice landed on ${Math.floor(Math.random() * 6) + 1}.`;
} else {
return `🎲 The dice landed on ${Math.floor(Math.random() * parseInt(args[0])) + 1}.`;
}
};
exports.aliases = ["roll", "die", "rng", "random"];
exports.category = 4;
exports.help = "Rolls the dice";
exports.params = "{number}";

View File

@ -1,19 +0,0 @@
const fetch = require("node-fetch");
exports.run = async (message) => {
message.channel.sendTyping();
const imageData = await fetch("https://dog.ceo/api/breeds/image/random");
const json = await imageData.json();
return {
embed: {
color: 16711680,
image: {
url: json.message
}
}
};
};
exports.aliases = ["doggos", "doggo", "pupper", "puppers", "dogs", "puppy", "puppies", "pups", "pup"];
exports.category = 4;
exports.help = "Gets a random dog picture";

View File

@ -1,20 +0,0 @@
const client = require("../utils/client.js");
exports.run = async () => {
let prefix = "";
if (client.guilds.has("592399417676529688")) {
const patrons = client.guilds.get("592399417676529688").members.filter((i) => {
return i.roles.includes("741386733047906475");
});
prefix = "Thanks to the following patrons for their support:\n";
for (const patron of patrons) {
prefix += `**- ${patron.username}**\n`;
}
prefix += "\n";
}
return `${prefix}Like esmBot? Consider supporting the developer on Patreon to help keep it running! https://patreon.com/TheEssem`;
};
exports.aliases = ["support", "patreon", "patrons"];
exports.category = 1;
exports.help = "Learn more about how you can support esmBot's development";

View File

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

View File

@ -1,10 +0,0 @@
exports.run = async (message, args) => {
if (args.length === 0) return `${message.author.mention}, you need to provide a string to encode!`;
const b64Encoded = Buffer.from(args.join(" ")).toString("base64");
return `\`\`\`\n${b64Encoded}\`\`\``;
};
exports.aliases = ["b64encode", "base64encode"];
exports.category = 1;
exports.help = "Encodes a Base64 string";
exports.params = "[text]";

View File

@ -1,27 +0,0 @@
const { clean } = require("../utils/misc.js");
exports.run = async (message, args) => {
if (message.author.id !== process.env.OWNER) return `${message.author.mention}, only the bot owner can use eval!`;
const code = 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\`\`\``;
}
};
exports.aliases = ["run"];
exports.category = 8;
exports.help = "Executes JavaScript code";
exports.params = "[code]";

View File

@ -1,30 +0,0 @@
const { clean } = require("../utils/misc.js");
const util = require("util");
const exec = util.promisify(require("child_process").exec);
exports.run = async (message, args) => {
if (message.author.id !== process.env.OWNER) return `${message.author.mention}, only the bot owner can use exec!`;
const code = 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\`\`\``;
}
};
exports.aliases = ["runcmd"];
exports.category = 8;
exports.help = "Executes a terminal command";
exports.params = "[command]";

View File

@ -1,21 +0,0 @@
const magick = require("../utils/image.js");
exports.run = async (message) => {
message.channel.sendTyping();
const image = await require("../utils/imagedetect.js")(message);
if (image === undefined) return `${message.author.mention}, you need to provide an image to explode!`;
const { buffer, type } = await magick.run({
cmd: "explode",
path: image.path,
amount: -1,
type: image.type
});
return {
file: buffer,
name: `explode.${type}`
};
};
exports.aliases = ["exp"];
exports.category = 5;
exports.help = "Explodes an image";

View File

@ -1,9 +0,0 @@
const soundPlayer = require("../utils/soundplayer.js");
exports.run = async (message) => {
return await soundPlayer.play("./assets/audio/explosion.ogg", message);
};
exports.category = 6;
exports.help = "Plays an explosion sound effect";
exports.requires = "sound";

View File

@ -1,10 +0,0 @@
const soundPlayer = require("../utils/soundplayer.js");
exports.run = async (message) => {
return await soundPlayer.play("./assets/audio/ping.ogg", message);
};
exports.aliases = ["notification", "notif"];
exports.category = 6;
exports.help = "Plays a Discord ping sound effect";
exports.requires = "sound";

View File

@ -1,10 +0,0 @@
const soundPlayer = require("../utils/soundplayer.js");
exports.run = async (message) => {
return await soundPlayer.play("./assets/audio/fart.ogg", message);
};
exports.aliases = ["toot"];
exports.category = 6;
exports.help = "Plays a fart sound effect";
exports.requires = "sound";

View File

@ -1,10 +0,0 @@
const soundPlayer = require("../utils/soundplayer.js");
exports.run = async (message) => {
return await soundPlayer.play("./assets/audio/fbi.ogg", message);
};
exports.aliases = ["openup"];
exports.category = 6;
exports.help = "Plays the \"FBI OPEN UP\" sound effect";
exports.requires = "sound";

View File

@ -1,40 +0,0 @@
const client = require("../utils/client.js");
const regex = /(\b(https?|ftp|file):\/\/[-A-Z0-9+&@#/%?=~_|!:,.;]*[-A-Z0-9+&@#/%=~_|])/ig;
exports.run = async (message, args) => {
if (args.length !== 0) {
if (regex.test(args.join(" "))) return `${message.author.mention}, you can't send a message containing a URL. If you want to report an issue, please join the esmBot Support server instead.`;
const feedbackChannel = client.guilds.get("592399417676529688").channels.get("592429860769497098");
feedbackChannel.createMessage({
embed: {
color: 16711680,
timestamp: new Date(),
thumbnail: {
url: message.author.avatarURL
},
author: {
name: "esmBot Feedback",
icon_url: client.user.avatarURL
},
fields: [{
name: "👥 Author:",
value: `${message.author.username}#${message.author.discriminator}`
}, {
name: "👪 Server:",
value: message.channel.guild ? message.channel.guild.name : "N/A"
}, {
name: "💬 Message:",
value: args.join(" ")
}]
}
});
return `${message.author.mention}, your feedback has been sent!`;
} else {
return `${message.author.mention}, you need to provide some feedback to send!`;
}
};
exports.aliases = ["request", "report", "complain", "compliment"];
exports.category = 1;
exports.help = "Leaves some feedback for the bot owner";
exports.params = "[message]";

View File

@ -1,36 +0,0 @@
const magick = require("../utils/image.js");
const fs = require("fs");
const emojiRegex = require("emoji-regex");
const emoji = require("node-emoji");
exports.run = async (message, args) => {
message.channel.sendTyping();
const image = await require("../utils/imagedetect.js")(message);
if (image === undefined) return `${message.author.mention}, you need to provide an image to overlay a flag onto!`;
if (args.length === 0 || !args[0].match(emojiRegex)) return `${message.author.mention}, you need to provide an emoji of a flag to overlay!`;
const flag = emoji.unemojify(args[0]).replaceAll(":", "").replace("flag-", "");
let path = `./assets/images/region-flags/png/${flag.toUpperCase()}.png`;
if (flag === "🏴‍☠️") path = "./assets/images/pirateflag.png";
if (flag === "rainbow-flag") path = "./assets/images/rainbowflag.png";
if (flag === "checkered_flag") path = "./assets/images/checkeredflag.png";
if (flag === "🏳️‍⚧️") path = "./assets/images/transflag.png";
try {
await fs.promises.access(path);
} catch (e) {
return `${message.author.mention}, that isn't a flag!`;
}
const { buffer, type } = await magick.run({
cmd: "flag",
path: image.path,
overlay: path,
type: image.type
});
return {
file: buffer,
name: `flag.${type}`
};
};
exports.params = "[flag]";
exports.category = 5;
exports.help = "Overlays a flag onto an image";

View File

@ -1,19 +0,0 @@
const magick = require("../utils/image.js");
exports.run = async (message) => {
message.channel.sendTyping();
const image = await require("../utils/imagedetect.js")(message);
if (image === undefined) return `${message.author.mention}, you need to provide an image to flip!`;
const { buffer, type } = await magick.run({
cmd: "flip",
path: image.path,
type: image.type
});
return {
file: buffer,
name: `flip.${type}`
};
};
exports.category = 5;
exports.help = "Flips an image";

View File

@ -1,21 +0,0 @@
const magick = require("../utils/image.js");
exports.run = async (message) => {
message.channel.sendTyping();
const image = await require("../utils/imagedetect.js")(message);
if (image === undefined) return `${message.author.mention}, you need to provide an image to flop!`;
const { buffer, type } = await magick.run({
cmd: "flip",
path: image.path,
flop: true,
type: image.type
});
return {
file: buffer,
name: `flop.${type}`
};
};
exports.aliases = ["flip2"];
exports.category = 5;
exports.help = "Flops an image";

View File

@ -1,23 +0,0 @@
const magick = require("../utils/image.js");
exports.run = async (message) => {
message.channel.sendTyping();
const image = await require("../utils/imagedetect.js")(message);
if (image === undefined) return `${message.author.mention}, you need to provide a GIF to freeze!`;
const { buffer, type } = await magick.run({
cmd: "freeze",
path: image.path,
loop: false,
onlyGIF: true,
type: image.type
});
if (type === "nogif") return `${message.author.mention}, that isn't a GIF!`;
return {
file: buffer,
name: `freeze.${type}`
};
};
exports.aliases = ["noloop", "once"];
exports.category = 5;
exports.help = "Makes a GIF only play once";

View File

@ -1,9 +0,0 @@
exports.run = async (message, args) => {
if (args.length === 0) return `${message.author.mention}, you need to provide some text to convert to fullwidth!`;
return args.join("").replaceAll(/[A-Za-z0-9]/g, (s) => { return String.fromCharCode(s.charCodeAt(0) + 0xFEE0); });
};
exports.aliases = ["aesthetic", "aesthetics", "aes"];
exports.category = 4;
exports.help = "Converts a message to fullwidth/aesthetic text";
exports.params = "[text]";

37
commands/fun/8ball.js Normal file
View File

@ -0,0 +1,37 @@
const Command = require("../../classes/command.js");
const { random } = require("../../utils/misc.js");
class EightBallCommand extends Command {
static responses = [
"It is certain",
"It is decidedly so",
"Without a doubt",
"Yes, definitely",
"You may rely on it",
"As I see it, yes",
"Most likely",
"Outlook good",
"Yes",
"Signs point to yes",
"Reply hazy, try again",
"Ask again later",
"Better not tell you now",
"Cannot predict now",
"Concentrate and ask again",
"Don't count on it",
"My reply is no",
"My sources say no",
"Outlook not so good",
"Very doubtful"
];
async run() {
return `🎱 ${random(EightBallCommand.responses)}`;
}
static description = "Asks the magic 8-ball a question";
static aliases = ["magicball", "magikball", "magic8ball", "magik8ball", "eightball"];
static arguments = ["{text}"];
}
module.exports = EightBallCommand;

23
commands/fun/bird.js Normal file
View File

@ -0,0 +1,23 @@
const fetch = require("node-fetch");
const Command = require("../../classes/command.js");
class BirdCommand extends Command {
async run() {
this.message.channel.sendTyping();
const imageData = await fetch("http://shibe.online/api/birds");
const json = await imageData.json();
return {
embed: {
color: 16711680,
image: {
url: json[0]
}
}
};
}
static description = "Gets a random bird picture";
static aliases = ["birb", "birds", "birbs"];
}
module.exports = BirdCommand;

28
commands/fun/cat.js Normal file
View File

@ -0,0 +1,28 @@
const fetch = require("node-fetch");
const Command = require("../../classes/command.js");
class CatCommand extends Command {
async run() {
this.message.channel.sendTyping();
const data = await fetch("https://api.thecatapi.com/v1/images/search?format=json", {
headers: {
"x-api-key": process.env.CAT
}
});
const json = await data.json();
return {
embed: {
color: 16711680,
image: {
url: json[0].url
}
}
};
}
static description = "Gets a random cat picture";
static aliases = ["kitters", "kitties", "kitty", "cattos", "catto", "cats", "cta"];
static requires = ["cat"];
}
module.exports = CatCommand;

22
commands/fun/cowsay.js Normal file
View File

@ -0,0 +1,22 @@
const cowsay = require("cowsay2");
const cows = require("cowsay2/cows");
const Command = require("../../classes/command.js");
class CowsayCommand extends Command {
async run() {
if (this.args.length === 0) {
return `${this.message.author.mention}, you need to provide some text for the cow to say!`;
} else if (cows[this.args[0].toLowerCase()] != undefined) {
const cow = cows[this.args.shift().toLowerCase()];
return `\`\`\`\n${cowsay.say(this.args.join(" "), { cow })}\n\`\`\``;
} else {
return `\`\`\`\n${cowsay.say(this.args.join(" "))}\n\`\`\``;
}
}
static description = "Makes an ASCII cow say a message";
static aliases = ["cow"];
static arguments = ["{cow}", "[text]"];
}
module.exports = CowsayCommand;

17
commands/fun/dice.js Normal file
View File

@ -0,0 +1,17 @@
const Command = require("../../classes/command.js");
class DiceCommand extends Command {
async run() {
if (this.args.length === 0 || !this.args[0].match(/^\d+$/)) {
return `🎲 The dice landed on ${Math.floor(Math.random() * 6) + 1}.`;
} else {
return `🎲 The dice landed on ${Math.floor(Math.random() * parseInt(this.args[0])) + 1}.`;
}
}
static description = "Rolls the dice";
static aliases = ["roll", "die", "rng", "random"];
static arguments = ["{number}"];
}
module.exports = DiceCommand;

24
commands/fun/dog.js Normal file
View File

@ -0,0 +1,24 @@
const fetch = require("node-fetch");
const Command = require("../../classes/command.js");
class DogCommand extends Command {
async run() {
this.message.channel.sendTyping();
const imageData = await fetch("https://dog.ceo/api/breeds/image/random");
const json = await imageData.json();
return {
embed: {
color: 16711680,
image: {
url: json.message
}
}
};
}
static description = "Gets a random dog picture";
static aliases = ["doggos", "doggo", "pupper", "puppers", "dogs", "puppy", "puppies", "pups", "pup"];
static arguments = ["{number}"];
}
module.exports = DogCommand;

14
commands/fun/fullwidth.js Normal file
View File

@ -0,0 +1,14 @@
const Command = require("../../classes/command.js");
class FullwidthCommand extends Command {
async run() {
if (this.args.length === 0) return `${this.message.author.mention}, you need to provide some text to convert to fullwidth!`;
return this.args.join("").replaceAll(/[A-Za-z0-9]/g, (s) => { return String.fromCharCode(s.charCodeAt(0) + 0xFEE0); });
}
static description = "Converts a message to fullwidth/aesthetic text";
static aliases = ["aesthetic", "aesthetics", "aes"];
static arguments = ["[text]"];
}
module.exports = FullwidthCommand;

20
commands/fun/homebrew.js Normal file
View File

@ -0,0 +1,20 @@
const ImageCommand = require("../../classes/imageCommand.js");
class HomebrewCommand extends ImageCommand {
params(args) {
return {
caption: args.join(" ").toLowerCase().replaceAll("\n", " ")
};
}
static description = "Creates a Homebrew Channel edit";
static aliases = ["hbc", "brew", "wiibrew"];
static arguments = ["[text]"];
static requiresImage = false;
static requiresText = true;
static noText = "you need to provide some text to make a Homebrew Channel edit!";
static command = "homebrew";
}
module.exports = HomebrewCommand;

20
commands/fun/mc.js Normal file
View File

@ -0,0 +1,20 @@
const fetch = require("node-fetch");
const Command = require("../../classes/command.js");
class MCCommand extends Command {
async run() {
if (this.args.length === 0) return `${this.message.author.mention}, you need to provide some text to generate a Minecraft achievement!`;
this.message.channel.sendTyping();
const request = await fetch(`https://www.minecraftskinstealer.com/achievement/a.php?i=13&h=Achievement+get%21&t=${encodeURIComponent(this.args.join("+"))}`);
return {
file: await request.buffer(),
name: "mc.png"
};
}
static description = "Generates a Minecraft achievement image";
static aliases = ["ach", "achievement", "minecraft"];
static arguments = ["[text]"];
}
module.exports = MCCommand;

39
commands/fun/retro.js Normal file
View File

@ -0,0 +1,39 @@
const magick = require("../../utils/image.js");
const wrap = require("../../utils/wrap.js");
const Command = require("../../classes/command.js");
class RetroCommand extends Command {
async run() {
if (this.args.length === 0) return `${this.message.author.mention}, you need to provide some text to generate some retro text!`;
this.message.channel.sendTyping();
let [line1, line2, line3] = this.args.join(" ").replaceAll("&", "\\&amp;").replaceAll(">", "\\&gt;").replaceAll("<", "\\&lt;").replaceAll("\"", "\\&quot;").replaceAll("'", "\\&apos;").replaceAll("%", "\\%").split(",").map(elem => elem.trim());
if (!line2 && line1.length > 15) {
const [split1, split2, split3] = wrap(line1, { width: 15, indent: "" }).split("\n");
line1 = split1;
line2 = split2 ? split2 : "";
line3 = split3 ? split3 : "";
} else {
if (!line2) {
line2 = "";
}
if (!line3) {
line3 = "";
}
}
const { buffer } = await magick.run({
cmd: "retro",
line1,
line2,
line3
});
return {
file: buffer,
name: "retro.png"
};
}
static description = "Generates a retro text image (separate lines with a comma)";
static arguments = ["[text]", "{middle text}", "{bottom text}"];
}
module.exports = RetroCommand;

34
commands/fun/rps.js Normal file
View File

@ -0,0 +1,34 @@
const misc = require("../../utils/misc.js");
const Command = require("../../classes/command.js");
class RPSCommand extends Command {
async run() {
if (this.args.length === 0 || (this.args[0] !== "rock" && this.args[0] !== "paper" && this.args[0] !== "scissors")) return `${this.message.author.mention}, you need to choose whether you want to be rock, paper, or scissors!`;
let emoji;
let winOrLose;
const result = misc.random(["rock", "paper", "scissors"]);
switch (result) {
case "rock":
emoji = "✊";
if (this.args[0].toLowerCase() === "paper") winOrLose = 1;
break;
case "paper":
emoji = "✋";
if (this.args[0].toLowerCase() === "scissors") winOrLose = 1;
break;
case "scissors":
emoji = "✌";
if (this.args[0].toLowerCase() === "rock") winOrLose = 1;
break;
default:
break;
}
return this.args[0].toLowerCase() === result ? `${emoji} I chose ${result}. It's a tie!` : `${emoji} I chose ${result}. ${winOrLose ? "You win!" : "You lose!"}`;
}
static description = "Plays rock, paper, scissors with me";
static aliases = ["rockpaperscissors"];
static arguments = ["[rock/paper/scissors]"];
}
module.exports = RPSCommand;

21
commands/fun/sonic.js Normal file
View File

@ -0,0 +1,21 @@
const wrap = require("../../utils/wrap.js");
const ImageCommand = require("../../classes/imageCommand.js");
class SonicCommand extends ImageCommand {
params(args) {
const cleanedMessage = args.join(" ").replaceAll("&", "\\&amp;").replaceAll(">", "\\&gt;").replaceAll("<", "\\&lt;").replaceAll("\"", "\\&quot;").replaceAll("'", "\\&apos;").replaceAll("%", "\\%");
return {
text: wrap(cleanedMessage, {width: 15, indent: ""})
};
}
static description = "Creates a Sonic speech bubble image";
static arguments = ["[text]"];
static requiresImage = false;
static requiresText = true;
static noText = "you need to provide some text to make a Sonic meme!";
static command = "sonic";
}
module.exports = SonicCommand;

28
commands/fun/wikihow.js Normal file
View File

@ -0,0 +1,28 @@
const fetch = require("node-fetch");
const Command = require("../../classes/command.js");
class WikihowCommand extends Command {
async run() {
this.message.channel.sendTyping();
const request = await fetch("https://hargrimm-wikihow-v1.p.rapidapi.com/images?count=1", {
headers: {
"X-RapidAPI-Key": process.env.MASHAPE,
"X-RapidAPI-Host": "hargrimm-wikihow-v1.p.rapidapi.com",
"Accept": "application/json"
}
});
const json = await request.json();
const image = await fetch(json["1"]);
const imageBuffer = await image.buffer();
return {
file: imageBuffer,
name: json["1"].split("/")[json["1"].split("/").length - 1]
};
}
static description = "Gets a random WikiHow image";
static aliases = ["wiki"];
static requires = ["mashape"];
}
module.exports = WikihowCommand;

31
commands/fun/xkcd.js Normal file
View File

@ -0,0 +1,31 @@
const fetch = require("node-fetch");
const Command = require("../../classes/command.js");
class XKCDCommand extends Command {
async run() {
const url = this.args.length > 0 && this.args[0].match(/^\d+$/) ? `http://xkcd.com/${this.args[0]}/info.0.json` : "http://xkcd.com/info.0.json";
try {
const request = await fetch(url);
const json = await request.json();
const embed = {
"embed": {
"title": json.safe_title,
"url": `https://xkcd.com/${json.num}`,
"color": 16711680,
"description": json.alt,
"image": {
"url": json.img
}
}
};
return embed;
} catch {
return `${this.message.author.mention}, I couldn't get that XKCD!`;
}
}
static description = "Gets an XKCD comic";
static arguments = ["{id}"];
}
module.exports = XKCDCommand;

View File

@ -1,23 +0,0 @@
const magick = require("../utils/image.js");
exports.run = async (message) => {
message.channel.sendTyping();
const image = await require("../utils/imagedetect.js")(message);
if (image === undefined) return `${message.author.mention}, you need to provide an image to add New Funky Mode!`;
const { buffer, type } = await magick.run({
cmd: "watermark",
path: image.path,
water: "./assets/images/funky.png",
gravity: 3,
resize: true,
type: image.type
});
return {
file: buffer,
name: `funky.${type}`
};
};
exports.aliases = ["funkymode", "newfunkymode", "funkykong"];
exports.category = 5;
exports.help = "Adds the New Funky Mode banner to an image";

View File

@ -1,20 +0,0 @@
const magick = require("../utils/image.js");
exports.run = async (message) => {
message.channel.sendTyping();
const image = await require("../utils/imagedetect.js")(message);
if (image === undefined) return `${message.author.mention}, you need to provide an image to make a GameXplain thumbnail meme!`;
const { buffer, type } = await magick.run({
cmd: "gamexplain",
path: image.path,
type: image.type
});
return {
file: buffer,
name: `gamexplain.${type}`
};
};
exports.aliases = ["gx"];
exports.category = 5;
exports.help = "Makes a GameXplain thumbnail from an image";

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;

View File

@ -1,21 +0,0 @@
const magick = require("../utils/image.js");
exports.run = async (message) => {
const image = await require("../utils/imagedetect.js")(message);
if (image === undefined) return `${message.author.mention}, you need to provide an image to spin!`;
const processMessage = await message.channel.createMessage(`${process.env.PROCESSING_EMOJI || "<a:processing:479351417102925854>"} Processing... This might take a while`);
const { buffer } = await magick.run({
cmd: "globe",
path: image.path,
type: image.type
});
if (processMessage.channel.messages.get(processMessage.id)) await processMessage.delete();
return {
file: buffer,
name: "globe.gif"
};
};
exports.aliases = ["rotate", "sphere"];
exports.category = 5;
exports.help = "Spins an image";

View File

@ -1,21 +0,0 @@
const magick = require("../utils/image.js");
exports.run = async (message) => {
message.channel.sendTyping();
const image = await require("../utils/imagedetect.js")(message);
if (image === undefined) return `${message.author.mention}, you need to provide an image to mirror!`;
const { buffer, type } = await magick.run({
cmd: "mirror",
path: image.path,
first: true,
type: image.type
});
return {
file: buffer,
name: `haah.${type}`
};
};
exports.aliases = ["magik4", "mirror2"];
exports.category = 5;
exports.help = "Mirrors the left side of an image onto the right";

View File

@ -1,124 +0,0 @@
const database = require("../utils/database.js");
const collections = require("../utils/collections.js");
const client = require("../utils/client.js");
const misc = require("../utils/misc.js");
const paginator = require("../utils/pagination/pagination.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"];
exports.run = async (message, args) => {
const { prefix } = message.channel.guild ? await database.getGuild(message.channel.guild.id) : "N/A";
const commands = collections.commands;
const aliases = collections.aliases;
if (args.length !== 0 && (commands.has(args[0].toLowerCase()) || aliases.has(args[0].toLowerCase()))) {
const command = aliases.has(args[0].toLowerCase()) ? collections.aliases.get(args[0].toLowerCase()) : 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": client.user.avatarURL
},
"title": `${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 ? info.params : "None",
"inline": true
}]
}
};
return embed;
} else {
const categories = {
general: [],
moderation: [],
tags: ["**Every command in this category is a subcommand of the tag command.**\n"],
fun: [],
images: ["**These commands support the PNG, JPEG, WEBP (static), and GIF (animated or static) formats.**\n"],
soundboard: [],
music: []
};
for (const [command] of commands) {
const category = collections.info.get(command).category;
const description = collections.info.get(command).description;
const params = collections.info.get(command).params;
if (category === 1) {
categories.general.push(`**${command}**${params ? ` ${params}` : ""} - ${description}`);
} else if (category === 2) {
categories.moderation.push(`**${command}**${params ? ` ${params}` : ""} - ${description}`);
} else if (category === 3) {
const subCommands = [...Object.keys(description)];
for (const subCommand of subCommands) {
categories.tags.push(`**tags${subCommand !== "default" ? ` ${subCommand}` : ""}**${params[subCommand] ? ` ${params[subCommand]}` : ""} - ${description[subCommand]}`);
}
} else if (category === 4) {
categories.fun.push(`**${command}**${params ? ` ${params}` : ""} - ${description}`);
} else if (category === 5) {
categories.images.push(`**${command}**${params ? ` ${params}` : ""} - ${description}`);
} else if (category === 6) {
categories.soundboard.push(`**${command}**${params ? ` ${params}` : ""} - ${description}`);
} else if (category === 7) {
categories.music.push(`**${command}**${params ? ` ${params}` : ""} - ${description}`);
}
}
const pages = [];
for (const category of Object.keys(categories)) {
const splitPages = categories[category].map((item, index) => {
return index % 15 === 0 ? categories[category].slice(index, index + 15) : null;
}).filter((item) => {
return item;
});
for (const page of splitPages) {
pages.push({
title: category.charAt(0).toUpperCase() + category.slice(1),
page: page
});
}
}
const embeds = [];
for (const [i, value] of pages.entries()) {
embeds.push({
"embed": {
"author": {
"name": "esmBot Help",
"icon_url": 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": message.channel.guild ? prefix : "N/A"
}, {
"name": "Tip",
"value": misc.random(tips)
}]
}
});
}
return paginator(message, embeds);
}
};
exports.aliases = ["commands"];
exports.category = 1;
exports.help = "Gets a list of commands";
exports.params = "{command}";

View File

@ -1,19 +0,0 @@
const magick = require("../utils/image.js");
exports.run = async (message, args) => {
if (args.length === 0) return `${message.author.mention}, you need to provide some text to make a Homebrew Channel edit!`;
message.channel.sendTyping();
const { buffer } = await magick.run({
cmd: "homebrew",
caption: args.join(" ").toLowerCase().replaceAll("\n", " ")
});
return {
file: buffer,
name: "homebrew.png"
};
};
exports.aliases = ["hbc", "brew", "wiibrew"];
exports.category = 4;
exports.help = "Creates a Homebrew Channel edit";
exports.params = "[text]";

View File

@ -1,21 +0,0 @@
const magick = require("../utils/image.js");
exports.run = async (message) => {
message.channel.sendTyping();
const image = await require("../utils/imagedetect.js")(message);
if (image === undefined) return `${message.author.mention}, you need to provide an image to mirror!`;
const { buffer, type } = await magick.run({
cmd: "mirror",
path: image.path,
vertical: true,
type: image.type
});
return {
file: buffer,
name: `hooh.${type}`
};
};
exports.aliases = ["magik6", "mirror4"];
exports.category = 5;
exports.help = "Mirrors the bottom of an image onto the top";

View File

@ -1,23 +0,0 @@
const magick = require("../utils/image.js");
exports.run = async (message) => {
message.channel.sendTyping();
const image = await require("../utils/imagedetect.js")(message);
if (image === undefined) return `${message.author.mention}, you need to provide an image to add a Hypercam watermark!`;
const { buffer, type } = await magick.run({
cmd: "watermark",
path: image.path,
water: "./assets/images/hypercam.png",
gravity: 1,
resize: true,
type: image.type
});
return {
file: buffer,
name: `hypercam.${type}`
};
};
exports.aliases = ["hcam"];
exports.category = 5;
exports.help = "Adds the Hypercam watermark to an image";

View File

@ -1,23 +0,0 @@
const magick = require("../utils/image.js");
exports.run = async (message) => {
message.channel.sendTyping();
const image = await require("../utils/imagedetect.js")(message);
if (image === undefined) return `${message.author.mention}, you need to provide an image to add a iFunny watermark!`;
const { buffer, type } = await magick.run({
cmd: "watermark",
path: image.path,
water: "./assets/images/ifunny.png",
gravity: 8,
resize: true,
append: true,
type: image.type
});
return {
file: buffer,
name: `ifunny.${type}`
};
};
exports.category = 5;
exports.help = "Adds the iFunny watermark to an image";

View File

@ -0,0 +1,16 @@
const ImageCommand = require("../../classes/imageCommand.js");
class NineGagCommand extends ImageCommand {
params = {
water: "./assets/images/9gag.png",
gravity: 6
};
static description = "Adds the 9GAG watermark to an image";
static aliases = ["ninegag", "gag"];
static noImage = "you need to provide an image to add a 9GAG watermark!";
static command = "watermark";
}
module.exports = NineGagCommand;

View File

@ -0,0 +1,17 @@
const ImageCommand = require("../../classes/imageCommand.js");
class BandicamCommand extends ImageCommand {
params = {
water: "./assets/images/bandicam.png",
gravity: 2,
resize: true
};
static description = "Adds the Bandicam watermark to an image";
static aliases = ["bandi"];
static noImage = "you need to provide an image to add a Bandicam watermark!";
static command = "watermark";
}
module.exports = BandicamCommand;

View File

@ -0,0 +1,14 @@
const ImageCommand = require("../../classes/imageCommand.js");
class BlurCommand extends ImageCommand {
params = {
sharp: false
};
static description = "Blurs an image";
static noImage = "you need to provide an image to blur!";
static command = "blur";
}
module.exports = BlurCommand;

Some files were not shown because too many files have changed in this diff Show More