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:
parent
ff8a24d0e8
commit
40223ec8b5
291 changed files with 5296 additions and 5171 deletions
|
@ -1,24 +0,0 @@
|
|||
// separate the client from app.js so we can call it later
|
||||
const { Client } = require("eris");
|
||||
const client = new Client(process.env.TOKEN, {
|
||||
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
|
||||
},
|
||||
maxShards: "auto",
|
||||
allowedMentions: {
|
||||
everyone: false,
|
||||
roles: false,
|
||||
users: true,
|
||||
repliedUser: true
|
||||
}
|
||||
});
|
||||
module.exports = client;
|
|
@ -1,7 +1,19 @@
|
|||
exports.commands = new Map();
|
||||
exports.paths = new Map();
|
||||
exports.aliases = new Map();
|
||||
exports.info = new Map();
|
||||
|
||||
class TimedMap extends Map {
|
||||
set(key, value) {
|
||||
super.set(key, value);
|
||||
setTimeout(() => {
|
||||
if (super.has(key)) super.delete(key);
|
||||
}, 5000);
|
||||
}
|
||||
}
|
||||
|
||||
exports.runningCommands = new TimedMap();
|
||||
|
||||
class Cache extends Map {
|
||||
constructor(values) {
|
||||
super(values);
|
||||
|
|
|
@ -2,6 +2,8 @@ const collections = require("../collections.js");
|
|||
const logger = require("../logger.js");
|
||||
const misc = require("../misc.js");
|
||||
|
||||
logger.warn("\x1b[1m\x1b[31m\x1b[40m" + "The MongoDB database driver has been deprecated and will be removed in a future release. Please migrate your database to PostgreSQL as soon as possible." + "\x1b[0m");
|
||||
|
||||
const mongoose = require("mongoose");
|
||||
mongoose.connect(process.env.DB, {
|
||||
poolSize: 10,
|
||||
|
|
|
@ -68,6 +68,8 @@ exports.addCount = async (command) => {
|
|||
};
|
||||
|
||||
exports.addGuild = async (guild) => {
|
||||
const query = await this.getGuild(guild);
|
||||
if (query) return query;
|
||||
await connection.query("INSERT INTO guilds (guild_id, tags, prefix, disabled, tags_disabled) VALUES ($1, $2, $3, $4, $5)", [guild.id, misc.tagDefaults, process.env.PREFIX, [], false]);
|
||||
return await this.getGuild(guild.id);
|
||||
};
|
||||
|
|
12
utils/dbl.js
12
utils/dbl.js
|
@ -1,12 +0,0 @@
|
|||
// dbl api client
|
||||
const poster = require("topgg-autoposter");
|
||||
const logger = require("./logger.js");
|
||||
const client = require("./client.js");
|
||||
const dbl = poster(process.env.DBL, client);
|
||||
dbl.on("posted", () => {
|
||||
logger.log("Posted stats to top.gg");
|
||||
});
|
||||
dbl.on("error", e => {
|
||||
logger.error(e);
|
||||
});
|
||||
module.exports = dbl;
|
|
@ -3,21 +3,28 @@ const logger = require("./logger.js");
|
|||
|
||||
// load command into memory
|
||||
exports.load = async (command, soundStatus) => {
|
||||
const props = require(`../commands/${command}`);
|
||||
if (props.requires === "google" && process.env.GOOGLE === "") return logger.log("info", `Google info not provided in config, skipped loading command ${command}...`);
|
||||
if (props.requires === "cat" && process.env.CAT === "") return logger.log("info", `Cat API info not provided in config, skipped loading command ${command}...`);
|
||||
if (props.requires === "mashape" && process.env.MASHAPE === "") return logger.log("info", `Mashape/RapidAPI info not provided in config, skipped loading command ${command}...`);
|
||||
if (props.requires === "sound" && soundStatus) return logger.log("info", `Failed to connect to some Lavalink nodes, skipped loading command ${command}...`);
|
||||
collections.commands.set(command.split(".")[0], props.run);
|
||||
collections.info.set(command.split(".")[0], {
|
||||
category: props.category,
|
||||
description: props.help,
|
||||
const props = require(`../${command}`);
|
||||
if (props.requires.includes("google") && process.env.GOOGLE === "") return logger.log("warn", `Google info not provided in config, skipped loading command ${command}...`);
|
||||
if (props.requires.includes("cat") && process.env.CAT === "") return logger.log("warn", `Cat API info not provided in config, skipped loading command ${command}...`);
|
||||
if (props.requires.includes("mashape") && process.env.MASHAPE === "") return logger.log("warn", `Mashape/RapidAPI info not provided in config, skipped loading command ${command}...`);
|
||||
if (props.requires.includes("sound") && soundStatus) return logger.log("warn", `Failed to connect to some Lavalink nodes, skipped loading command ${command}...`);
|
||||
const commandArray = command.split("/");
|
||||
const commandName = commandArray[commandArray.length - 1].split(".")[0];
|
||||
|
||||
collections.paths.set(commandName, command);
|
||||
collections.commands.set(commandName, props);
|
||||
|
||||
collections.info.set(commandName, {
|
||||
category: commandArray[2],
|
||||
description: props.description,
|
||||
aliases: props.aliases,
|
||||
params: props.params
|
||||
params: props.arguments
|
||||
});
|
||||
|
||||
if (props.aliases) {
|
||||
for (const alias of props.aliases) {
|
||||
collections.aliases.set(alias, command.split(".")[0]);
|
||||
collections.aliases.set(alias, commandName);
|
||||
collections.paths.set(alias, command);
|
||||
}
|
||||
}
|
||||
return false;
|
||||
|
@ -32,11 +39,12 @@ exports.unload = async (command) => {
|
|||
cmd = collections.commands.get(collections.aliases.get(command));
|
||||
}
|
||||
if (!cmd) return `The command \`${command}\` doesn't seem to exist, nor is it an alias.`;
|
||||
const mod = require.cache[require.resolve(`../commands/${command}`)];
|
||||
delete require.cache[require.resolve(`../commands/${command}.js`)];
|
||||
for (let i = 0; i < mod.parent.children.length; i++) {
|
||||
if (mod.parent.children[i] === mod) {
|
||||
mod.parent.children.splice(i, 1);
|
||||
const path = collections.paths.get(command);
|
||||
const mod = require.cache[require.resolve(`../${path}`)];
|
||||
delete require.cache[require.resolve(`../${path}`)];
|
||||
for (let i = 0; i < module.children.length; i++) {
|
||||
if (module.children[i] === mod) {
|
||||
module.children.splice(i, 1);
|
||||
break;
|
||||
}
|
||||
}
|
||||
|
|
|
@ -1,10 +1,34 @@
|
|||
const collections = require("./collections.js");
|
||||
const logger = require("./logger.js");
|
||||
const fs = require("fs");
|
||||
|
||||
module.exports = async (output) => {
|
||||
const template = `# <img src="https://raw.githubusercontent.com/esmBot/esmBot/master/esmbot.png" width="64"> esmBot${process.env.NODE_ENV === "development" ? " Dev" : ""} Command List
|
||||
${process.env.NODE_ENV === "development" ? "\n**You are currently using esmBot Dev! Things may change at any time without warning and there will be bugs. Many bugs. If you find one, [report it here](https://github.com/esmBot/esmBot/issues) or in the esmBot Support server.**\n" : ""}
|
||||
const categoryTemplate = {
|
||||
general: [],
|
||||
tags: ["> **Every command in this category is a subcommand of the tag command.**\n"],
|
||||
"image-editing": ["> **These commands support the PNG, JPEG, WEBP (static), and GIF (animated or static) formats.**\n"]
|
||||
};
|
||||
exports.categories = categoryTemplate;
|
||||
|
||||
exports.generateList = async () => {
|
||||
this.categories = categoryTemplate;
|
||||
for (const [command] of collections.commands) {
|
||||
const category = collections.info.get(command).category;
|
||||
const description = collections.info.get(command).description;
|
||||
const params = collections.info.get(command).params;
|
||||
if (category === "tags") {
|
||||
const subCommands = [...Object.keys(description)];
|
||||
for (const subCommand of subCommands) {
|
||||
this.categories.tags.push(`**tags${subCommand !== "default" ? ` ${subCommand}` : ""}**${params[subCommand] ? ` ${params[subCommand]}` : ""} - ${description[subCommand]}`);
|
||||
}
|
||||
} else {
|
||||
if (!this.categories[category]) this.categories[category] = [];
|
||||
this.categories[category].push(`**${command}**${params ? ` ${params}` : ""} - ${description}`);
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
exports.createPage = async (output) => {
|
||||
let template = `# <img src="https://raw.githubusercontent.com/esmBot/esmBot/master/esmbot.png" width="64"> esmBot${process.env.NODE_ENV === "development" ? " Dev" : ""} Command List
|
||||
|
||||
This page was last generated on \`${new Date().toString()}\`.
|
||||
|
||||
\`[]\` means an argument is required, \`{}\` means an argument is optional.
|
||||
|
@ -13,47 +37,33 @@ Default prefix is \`&\`.
|
|||
|
||||
**Want to help support esmBot's development? Consider donating on Patreon!** https://patreon.com/TheEssem
|
||||
|
||||
> Tip: You can get more info about a command by using \`help [command]\`.
|
||||
|
||||
## Table of Contents
|
||||
+ [**General**](#💻-general)
|
||||
+ [**Tags**](#🏷️-tags)
|
||||
+ [**Fun**](#👌-fun)
|
||||
+ [**Image Editing**](#🖼️-image-editing)
|
||||
+ [**Soundboard**](#🔊-soundboard)
|
||||
+ [**Music**](#🎤-music)
|
||||
> Tip: You can get more info about a command by using \`help [command]\` in the bot itself.
|
||||
`;
|
||||
const commands = collections.commands;
|
||||
const categories = {
|
||||
general: ["## 💻 General"],
|
||||
tags: ["## 🏷️ Tags"],
|
||||
fun: ["## 👌 Fun"],
|
||||
images: ["## 🖼️ Image Editing", "> These commands support the PNG, JPEG, WEBP, and GIF formats.\n"],
|
||||
soundboard: ["## 🔊 Soundboard"],
|
||||
music: ["## 🎤 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 === 3) {
|
||||
const subCommands = [...Object.keys(description)];
|
||||
for (const subCommand of subCommands) {
|
||||
categories.tags.push(`+ **tags${subCommand !== "default" ? ` ${subCommand}` : ""}**${params[subCommand] ? ` ${params[subCommand]}` : ""} - ${description[subCommand]}`);
|
||||
|
||||
template += "\n## Table of Contents\n";
|
||||
for (const category of Object.keys(this.categories)) {
|
||||
const categoryStringArray = category.split("-");
|
||||
for (const index of categoryStringArray.keys()) {
|
||||
categoryStringArray[index] = categoryStringArray[index].charAt(0).toUpperCase() + categoryStringArray[index].slice(1);
|
||||
}
|
||||
template += `+ [**${categoryStringArray.join(" ")}**](#${category})\n`;
|
||||
}
|
||||
|
||||
// hell
|
||||
for (const category of Object.keys(this.categories)) {
|
||||
const categoryStringArray = category.split("-");
|
||||
for (const index of categoryStringArray.keys()) {
|
||||
categoryStringArray[index] = categoryStringArray[index].charAt(0).toUpperCase() + categoryStringArray[index].slice(1);
|
||||
}
|
||||
template += `\n## ${categoryStringArray.join(" ")}\n`;
|
||||
for (const command of this.categories[category]) {
|
||||
if (command.startsWith(">")) {
|
||||
template += `${command}\n`;
|
||||
} else {
|
||||
template += `+ ${command}\n`;
|
||||
}
|
||||
} 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}`);
|
||||
}
|
||||
}
|
||||
fs.writeFile(output, `${template}\n${categories.general.join("\n")}\n\n${categories.tags.join("\n")}\n\n${categories.fun.join("\n")}\n\n${categories.images.join("\n")}\n\n${categories.soundboard.join("\n")}\n\n${categories.music.join("\n")}`, () => {
|
||||
logger.log("The help docs have been generated.");
|
||||
});
|
||||
|
||||
await fs.promises.writeFile(output, template);
|
||||
};
|
|
@ -1,9 +1,8 @@
|
|||
const magick = require("../build/Release/image.node");
|
||||
const { promisify } = require("util");
|
||||
const { isMainThread, parentPort, workerData } = require("worker_threads");
|
||||
|
||||
exports.run = async object => {
|
||||
return new Promise(async (resolve, reject) => {
|
||||
exports.run = object => {
|
||||
return new Promise((resolve, reject) => {
|
||||
// If the image has a path, it must also have a type
|
||||
if (object.path) {
|
||||
if (object.type !== "image/gif" && object.onlyGIF) resolve({
|
||||
|
@ -16,25 +15,23 @@ exports.run = async object => {
|
|||
// If no image type is given (say, the command generates its own image), make it a PNG.
|
||||
const fileExtension = object.type ? object.type.split("/")[1] : "png";
|
||||
const objectWithFixedType = Object.assign({}, object, {type: fileExtension});
|
||||
try {
|
||||
const data = await promisify(magick[object.cmd])(objectWithFixedType);
|
||||
magick[object.cmd](objectWithFixedType, (error, data, type) => {
|
||||
if (error) reject(error);
|
||||
const returnObject = {
|
||||
buffer: data,
|
||||
fileExtension
|
||||
fileExtension: type
|
||||
};
|
||||
resolve(returnObject);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
};
|
||||
|
||||
if (!isMainThread) {
|
||||
this.run(workerData)
|
||||
// eslint-disable-next-line promise/always-return
|
||||
.then(returnObject => {
|
||||
parentPort.postMessage(returnObject);
|
||||
process.exit();
|
||||
return;
|
||||
})
|
||||
.catch(err => {
|
||||
// turn promise rejection into normal error
|
||||
|
|
|
@ -1,10 +1,9 @@
|
|||
const magick = require("../build/Release/image.node");
|
||||
const { Worker } = require("worker_threads");
|
||||
const fetch = require("node-fetch");
|
||||
const AbortController = require("abort-controller");
|
||||
const fs = require("fs");
|
||||
const net = require("net");
|
||||
const fileType = require("file-type");
|
||||
exports.servers = require("../servers.json").image;
|
||||
const path = require("path");
|
||||
const { EventEmitter } = require("events");
|
||||
const logger = require("./logger.js");
|
||||
|
@ -13,10 +12,12 @@ const formats = ["image/jpeg", "image/png", "image/webp", "image/gif"];
|
|||
|
||||
const jobs = {};
|
||||
|
||||
const connections = [];
|
||||
exports.connections = [];
|
||||
|
||||
const statuses = {};
|
||||
|
||||
exports.servers = JSON.parse(fs.readFileSync("./servers.json", { encoding: "utf8" })).image;
|
||||
|
||||
const chooseServer = async (ideal) => {
|
||||
if (ideal.length === 0) throw "No available servers";
|
||||
const sorted = ideal.sort((a, b) => {
|
||||
|
@ -25,12 +26,40 @@ const chooseServer = async (ideal) => {
|
|||
return sorted[0];
|
||||
};
|
||||
|
||||
exports.repopulate = async () => {
|
||||
const data = await fs.promises.readFile("./servers.json", { encoding: "utf8" });
|
||||
this.servers = JSON.parse(data).image;
|
||||
return;
|
||||
};
|
||||
|
||||
exports.getStatus = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let serversLeft = this.connections.length;
|
||||
const statuses = [];
|
||||
const timeout = setTimeout(() => {
|
||||
resolve(statuses);
|
||||
}, 5000);
|
||||
for (const connection of this.connections) {
|
||||
if (!connection.remoteAddress) continue;
|
||||
fetch(`http://${connection.remoteAddress}:8081/running`).then(statusRequest => statusRequest.json()).then((status) => {
|
||||
serversLeft--;
|
||||
statuses.push(status);
|
||||
if (!serversLeft) {
|
||||
clearTimeout(timeout);
|
||||
resolve(statuses);
|
||||
}
|
||||
return;
|
||||
}).catch(e => reject(e));
|
||||
}
|
||||
});
|
||||
};
|
||||
|
||||
exports.connect = (server) => {
|
||||
return new Promise((resolve, reject) => {
|
||||
const connection = net.createConnection(8080, server);
|
||||
const timeout = setTimeout(() => {
|
||||
const connectionIndex = connections.indexOf(connection);
|
||||
if (connectionIndex < 0) delete connections[connectionIndex];
|
||||
const connectionIndex = this.connections.indexOf(connection);
|
||||
if (connectionIndex < 0) delete this.connections[connectionIndex];
|
||||
reject(`Failed to connect to ${server}`);
|
||||
}, 5000);
|
||||
connection.once("connect", () => {
|
||||
|
@ -66,24 +95,35 @@ exports.connect = (server) => {
|
|||
connection.on("error", (e) => {
|
||||
console.error(e);
|
||||
});
|
||||
connections.push(connection);
|
||||
this.connections.push(connection);
|
||||
resolve();
|
||||
});
|
||||
};
|
||||
|
||||
exports.disconnect = async () => {
|
||||
for (const connection of this.connections) {
|
||||
connection.destroy();
|
||||
}
|
||||
for (const uuid of Object.keys(jobs)) {
|
||||
jobs[uuid].emit("error", new Error("Job ended prematurely (not really an error; just run your image job again)"));
|
||||
}
|
||||
this.connections = [];
|
||||
return;
|
||||
};
|
||||
|
||||
const getIdeal = () => {
|
||||
return new Promise((resolve, reject) => {
|
||||
let serversLeft = connections.length;
|
||||
let serversLeft = this.connections.length;
|
||||
const idealServers = [];
|
||||
const timeout = setTimeout(async () => {
|
||||
try {
|
||||
const server = await chooseServer(idealServers);
|
||||
resolve(connections.find(val => val.remoteAddress === server.addr));
|
||||
resolve(this.connections.find(val => val.remoteAddress === server.addr));
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
}, 5000);
|
||||
for (const connection of connections) {
|
||||
for (const connection of this.connections) {
|
||||
if (!connection.remoteAddress) continue;
|
||||
fetch(`http://${connection.remoteAddress}:8081/status`).then(statusRequest => statusRequest.text()).then(async (status) => {
|
||||
serversLeft--;
|
||||
|
@ -94,7 +134,7 @@ const getIdeal = () => {
|
|||
if (!serversLeft) {
|
||||
clearTimeout(timeout);
|
||||
const server = await chooseServer(idealServers);
|
||||
resolve(connections.find(val => val.remoteAddress === server.addr));
|
||||
resolve(this.connections.find(val => val.remoteAddress === server.addr));
|
||||
}
|
||||
return;
|
||||
}).catch(e => reject(e));
|
||||
|
@ -150,7 +190,7 @@ exports.getType = async (image) => {
|
|||
return undefined;
|
||||
}
|
||||
let type;
|
||||
const controller = new AbortController();
|
||||
const controller = new AbortController(); // eslint-disable-line no-undef
|
||||
const timeout = setTimeout(() => {
|
||||
controller.abort();
|
||||
}, 25000);
|
||||
|
@ -183,13 +223,15 @@ exports.run = object => {
|
|||
const num = Math.floor(Math.random() * 100000).toString().slice(0, 5);
|
||||
const timeout = setTimeout(() => {
|
||||
if (jobs[num]) delete jobs[num];
|
||||
reject("Request timed out");
|
||||
reject("the image request timed out after 25 seconds. Try uploading your image elsewhere.");
|
||||
}, 25000);
|
||||
start(object, num).catch(err => { // incredibly hacky code incoming
|
||||
clearTimeout(timeout);
|
||||
if (err instanceof Error) return reject(err);
|
||||
return err;
|
||||
}).then((data) => {
|
||||
clearTimeout(timeout);
|
||||
if (!data.event) reject("Not connected to image server");
|
||||
data.event.once("image", (image, type) => {
|
||||
delete jobs[data.uuid];
|
||||
const payload = {
|
||||
|
|
|
@ -1,4 +1,3 @@
|
|||
const client = require("./client.js");
|
||||
const fetch = require("node-fetch");
|
||||
const url = require("url");
|
||||
const { getType } = require("./image.js");
|
||||
|
@ -93,7 +92,7 @@ const checkImages = async (message) => {
|
|||
};
|
||||
|
||||
// this checks for the latest message containing an image and returns the url of the image
|
||||
module.exports = async (cmdMessage) => {
|
||||
module.exports = async (client, cmdMessage) => {
|
||||
// we start by checking the current message for images
|
||||
const result = await checkImages(cmdMessage);
|
||||
if (result !== false) return result;
|
||||
|
|
|
@ -1,15 +1,4 @@
|
|||
const moment = require("moment");
|
||||
const winston = require("winston");
|
||||
const logger = winston.createLogger({
|
||||
transports: [
|
||||
new winston.transports.Console(),
|
||||
new winston.transports.File({ filename: "logs/error.log", level: "error" }),
|
||||
new winston.transports.File({ filename: "logs/main.log" }),
|
||||
],
|
||||
format: winston.format.printf(log => `[${moment().format("YYYY-MM-DD HH:mm:ss")}]: [${log.level.toUpperCase()}] - ${log.message}`)
|
||||
});
|
||||
|
||||
exports.log = (type, content) => content ? logger.log(type, content) : logger.log("info", type);
|
||||
exports.log = (type, content) => content ? process.send({ name: type, msg: content }) : process.send({ name: "info", msg: type });
|
||||
|
||||
exports.error = (...args) => this.log("error", ...args);
|
||||
|
||||
|
|
|
@ -1,5 +1,4 @@
|
|||
const util = require("util");
|
||||
const client = require("./client.js");
|
||||
|
||||
// random(array) to select a random entry in array
|
||||
exports.random = (array) => {
|
||||
|
@ -34,14 +33,6 @@ exports.clean = async (text) => {
|
|||
return text;
|
||||
};
|
||||
|
||||
exports.getRandomMessage = async () => {
|
||||
const messages = await client.guilds.get("631290275456745502").channels.get("631290275888627713").getMessages(50);
|
||||
const randomMessage = this.random(messages);
|
||||
if (randomMessage.content.length > 144) return await this.getRandomMessage();
|
||||
if (randomMessage.content.match(/<@!?\d+>/g)) return await this.getRandomMessage();
|
||||
return randomMessage.content;
|
||||
};
|
||||
|
||||
// regexEscape(string) to escape characters in a string for use in a regex
|
||||
exports.regexEscape = (string) => {
|
||||
return string.replace(/[.*+?^${}()|[\]\\]/g, "\\$&"); // $& means the whole matched string
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
// eris doesn't come with an awaitMessages method by default, so we make our own
|
||||
const EventEmitter = require("events").EventEmitter;
|
||||
const client = require("../client.js");
|
||||
|
||||
class MessageCollector extends EventEmitter {
|
||||
constructor(channel, filter, options = {}) {
|
||||
constructor(client, channel, filter, options = {}) {
|
||||
super();
|
||||
this.filter = filter;
|
||||
this.channel = channel;
|
||||
|
|
|
@ -1,9 +1,8 @@
|
|||
// eris doesn't come with an awaitReactions method by default, so we make our own
|
||||
const EventEmitter = require("events").EventEmitter;
|
||||
const client = require("../client.js");
|
||||
|
||||
class ReactionCollector extends EventEmitter {
|
||||
constructor(message, filter, options = {}) {
|
||||
constructor(client, message, filter, options = {}) {
|
||||
super();
|
||||
this.filter = filter;
|
||||
this.message = message;
|
||||
|
@ -20,7 +19,7 @@ class ReactionCollector extends EventEmitter {
|
|||
if (this.message.id !== message.id) return false;
|
||||
if (this.filter(message, emoji, member)) {
|
||||
this.collected.push({ message: message, emoji: emoji, member: member });
|
||||
this.emit("reaction", await client.getMessage(message.channel.id, message.id), emoji, member);
|
||||
this.emit("reaction", await this.bot.getMessage(message.channel.id, message.id), emoji, member);
|
||||
if (this.collected.length >= this.options.maxMatches) this.stop("maxMatches");
|
||||
return true;
|
||||
}
|
||||
|
|
|
@ -1,8 +1,7 @@
|
|||
const ReactionCollector = require("./awaitreactions.js");
|
||||
const MessageCollector = require("./awaitmessages.js");
|
||||
const client = require("../client.js");
|
||||
|
||||
module.exports = async (message, pages, timeout = 120000) => {
|
||||
module.exports = async (client, message, pages, timeout = 120000) => {
|
||||
const manageMessages = message.channel.guild && message.channel.permissionsOf(client.user.id).has("manageMessages") ? true : false;
|
||||
let page = 0;
|
||||
let currentPage = await message.channel.createMessage(pages[page]);
|
||||
|
@ -10,18 +9,18 @@ module.exports = async (message, pages, timeout = 120000) => {
|
|||
for (const emoji of emojiList) {
|
||||
await currentPage.addReaction(emoji);
|
||||
}
|
||||
const reactionCollector = new ReactionCollector(currentPage, (message, reaction, member) => emojiList.includes(reaction.name) && !member.bot, { time: timeout });
|
||||
const reactionCollector = new ReactionCollector(client, currentPage, (message, reaction, member) => emojiList.includes(reaction.name) && !member.bot, { time: timeout });
|
||||
reactionCollector.on("reaction", async (msg, reaction, member) => {
|
||||
if (member.id === message.author.id) {
|
||||
if (member === message.author.id) {
|
||||
switch (reaction.name) {
|
||||
case "◀":
|
||||
page = page > 0 ? --page : pages.length - 1;
|
||||
currentPage = await currentPage.edit(pages[page]);
|
||||
if (manageMessages) msg.removeReaction("◀", member.id);
|
||||
if (manageMessages) msg.removeReaction("◀", member);
|
||||
break;
|
||||
case "🔢":
|
||||
message.channel.createMessage(`${message.author.mention}, what page do you want to jump to?`).then(askMessage => {
|
||||
const messageCollector = new MessageCollector(askMessage.channel, (response) => response.author.id === message.author.id && !isNaN(response.content) && Number(response.content) <= pages.length && Number(response.content) > 0, {
|
||||
const messageCollector = new MessageCollector(client, askMessage.channel, (response) => response.author.id === message.author.id && !isNaN(response.content) && Number(response.content) <= pages.length && Number(response.content) > 0, {
|
||||
time: timeout,
|
||||
maxMatches: 1
|
||||
});
|
||||
|
@ -30,7 +29,7 @@ module.exports = async (message, pages, timeout = 120000) => {
|
|||
if (manageMessages) await response.delete();
|
||||
page = Number(response.content) - 1;
|
||||
currentPage = await currentPage.edit(pages[page]);
|
||||
if (manageMessages) msg.removeReaction("🔢", member.id);
|
||||
if (manageMessages) msg.removeReaction("🔢", member);
|
||||
});
|
||||
}).catch(error => {
|
||||
throw error;
|
||||
|
@ -39,7 +38,7 @@ module.exports = async (message, pages, timeout = 120000) => {
|
|||
case "▶":
|
||||
page = page + 1 < pages.length ? ++page : 0;
|
||||
currentPage = await currentPage.edit(pages[page]);
|
||||
if (manageMessages) msg.removeReaction("▶", member.id);
|
||||
if (manageMessages) msg.removeReaction("▶", member);
|
||||
break;
|
||||
case "🗑":
|
||||
reactionCollector.emit("end");
|
||||
|
|
|
@ -1,23 +0,0 @@
|
|||
# if your install supports docker and you don't want to run chromium directly then you can use this dockerfile instead
|
||||
# this also comes with a squid proxy to automatically handle errors and some privacy stuff
|
||||
# adapted from https://github.com/westy92/headless-chrome-alpine/blob/master/Dockerfile
|
||||
|
||||
FROM alpine:edge
|
||||
|
||||
RUN apk --no-cache upgrade && apk add --no-cache chromium squid sudo libstdc++ harfbuzz nss freetype ttf-freefont zlib-dev wait4ports dbus chromium-chromedriver grep
|
||||
|
||||
ENV ALL_PROXY "http://localhost:3128"
|
||||
RUN echo -e "\nvisible_hostname esmBot\nforwarded_for delete\nvia off\nfollow_x_forwarded_for deny all \
|
||||
\nrequest_header_access X-Forwarded-For deny all\nerror_default_language en\n" >> /etc/squid/squid.conf
|
||||
COPY ./ERR_DNS_FAIL /usr/share/squid/errors/en/
|
||||
|
||||
RUN adduser esmBot -s /bin/sh -D
|
||||
WORKDIR /home/esmBot/.internal
|
||||
COPY ./start.sh .
|
||||
RUN chmod +x start.sh
|
||||
RUN echo -e "\nesmBot ALL=(ALL) NOPASSWD:ALL\n" >> /etc/sudoers
|
||||
USER esmBot
|
||||
|
||||
EXPOSE 9222
|
||||
|
||||
ENTRYPOINT ["sh", "./start.sh"]
|
|
@ -1,63 +0,0 @@
|
|||
<html>
|
||||
|
||||
<head>
|
||||
<meta name="viewport" content="width=device-width, initial-scale=1">
|
||||
<meta charset="utf8">
|
||||
<style>
|
||||
body {
|
||||
font-family: -apple-system, BlinkMacSystemFont, Segoe UI, Helvetica, Arial, sans-serif, Apple Color Emoji, Segoe UI Emoji;
|
||||
}
|
||||
|
||||
.page {
|
||||
height: 100%;
|
||||
width: 100%;
|
||||
align-items: center;
|
||||
justify-content: center;
|
||||
flex-flow: row wrap;
|
||||
align-content: center;
|
||||
display: flex;
|
||||
}
|
||||
|
||||
.text {
|
||||
text-align: center;
|
||||
font-weight: 400;
|
||||
width: 100%;
|
||||
margin: 5px;
|
||||
}
|
||||
|
||||
.img {
|
||||
text-align: center;
|
||||
margin: 10px auto 20px;
|
||||
display: block;
|
||||
width: 256px;
|
||||
}
|
||||
|
||||
.container {
|
||||
padding: 2em;
|
||||
}
|
||||
|
||||
* {
|
||||
box-sizing: border-box;
|
||||
}
|
||||
|
||||
.large {
|
||||
font-size: 5em;
|
||||
}
|
||||
|
||||
.med {
|
||||
font-size: 3em;
|
||||
}
|
||||
</style>
|
||||
</head>
|
||||
|
||||
<body>
|
||||
<div class="page">
|
||||
<div class="container">
|
||||
<img src="https://projectlounge.pw/pictures/esmbot.png" width="256" class="text img"></img>
|
||||
<p class="text large" id="page">An error was encountered while trying to load %H:</p>
|
||||
<p class="text med" id="error">%z</p>
|
||||
</div>
|
||||
</div>
|
||||
</body>
|
||||
|
||||
</html>
|
|
@ -1,16 +0,0 @@
|
|||
sudo squid -N & chromium-browser \
|
||||
--disable-background-networking \
|
||||
--disable-default-apps \
|
||||
--disable-extensions \
|
||||
--disable-gpu \
|
||||
--disable-sync \
|
||||
--disable-translate \
|
||||
--headless \
|
||||
--hide-scrollbars \
|
||||
--metrics-recording-only \
|
||||
--mute-audio \
|
||||
--no-first-run \
|
||||
--no-sandbox \
|
||||
--remote-debugging-address=0.0.0.0 \
|
||||
--remote-debugging-port=9222 \
|
||||
--safebrowsing-disable-auto-update
|
|
@ -1,23 +1,19 @@
|
|||
const client = require("./client.js");
|
||||
const logger = require("./logger.js");
|
||||
const paginator = require("./pagination/pagination.js");
|
||||
const fetch = require("node-fetch");
|
||||
const fs = require("fs");
|
||||
const moment = require("moment");
|
||||
require("moment-duration-format");
|
||||
const day = require("dayjs");
|
||||
const duration = require("dayjs/plugin/duration");
|
||||
day.extend(duration);
|
||||
const { Manager } = require("lavacord");
|
||||
|
||||
let nodes;
|
||||
|
||||
exports.players = new Map();
|
||||
|
||||
exports.queues = new Map();
|
||||
const skipVotes = new Map();
|
||||
exports.skipVotes = new Map();
|
||||
|
||||
exports.manager;
|
||||
|
||||
exports.status = false;
|
||||
|
||||
exports.connected = false;
|
||||
|
||||
exports.checkStatus = async () => {
|
||||
|
@ -29,7 +25,7 @@ exports.checkStatus = async () => {
|
|||
const response = await fetch(`http://${node.host}:${node.port}/version`, { headers: { Authorization: node.password } }).then(res => res.text());
|
||||
if (response) newNodes.push(node);
|
||||
} catch {
|
||||
logger.log(`Failed to get status of Lavalink node ${node.host}.`);
|
||||
logger.error(`Failed to get status of Lavalink node ${node.host}.`);
|
||||
}
|
||||
}
|
||||
nodes = newNodes;
|
||||
|
@ -37,7 +33,7 @@ exports.checkStatus = async () => {
|
|||
return this.status;
|
||||
};
|
||||
|
||||
exports.connect = async () => {
|
||||
exports.connect = async (client) => {
|
||||
this.manager = new Manager(nodes, {
|
||||
user: client.user.id,
|
||||
shards: client.shards.size || 1,
|
||||
|
@ -56,11 +52,11 @@ exports.connect = async () => {
|
|||
return length;
|
||||
};
|
||||
|
||||
exports.play = async (sound, message, music = false) => {
|
||||
exports.play = async (client, sound, message, music = false) => {
|
||||
if (!this.manager) return `${message.author.mention}, the sound commands are still starting up!`;
|
||||
if (!message.channel.guild) return `${message.author.mention}, this command only works in servers!`;
|
||||
if (!message.member.voiceState.channelID) return `${message.author.mention}, you need to be in a voice channel first!`;
|
||||
if (!message.channel.guild.members.get(client.user.id).permissions.has("voiceConnect") || !message.channel.permissionsOf(client.user.id).has("voiceConnect")) return `${message.author.mention}, I can't join this voice channel!`;
|
||||
if (!message.channel.permissionsOf(client.user.id).has("voiceConnect")) return `${message.author.mention}, I can't join this voice channel!`;
|
||||
const voiceChannel = message.channel.guild.channels.get(message.member.voiceState.channelID);
|
||||
if (!voiceChannel.permissionsOf(client.user.id).has("voiceConnect")) return `${message.author.mention}, I don't have permission to join this voice channel!`;
|
||||
const player = this.players.get(message.channel.guild.id);
|
||||
|
@ -87,40 +83,45 @@ exports.play = async (sound, message, music = false) => {
|
|||
}
|
||||
|
||||
if (oldQueue && music) {
|
||||
return `${message.author.mention}, your tune has been added to the queue!`;
|
||||
return `${message.author.mention}, your tune \`${tracks[0].info.title}\` has been added to the queue!`;
|
||||
} else {
|
||||
this.nextSong(message, connection, tracks[0].track, tracks[0].info, music, voiceChannel, player ? player.loop : false);
|
||||
this.nextSong(client, message, connection, tracks[0].track, tracks[0].info, music, voiceChannel, player ? player.loop : false);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
exports.nextSong = async (message, connection, track, info, music, voiceChannel, loop = false, inQueue = false) => {
|
||||
exports.nextSong = async (client, message, connection, track, info, music, voiceChannel, loop = false, inQueue = false, lastTrack = null, oldPlaying = null) => {
|
||||
const parts = Math.floor((0 / info.length) * 10);
|
||||
const playingMessage = await client.createMessage(message.channel.id, !music ? "🔊 Playing sound..." : {
|
||||
"embed": {
|
||||
"color": 16711680,
|
||||
"author": {
|
||||
"name": "Now Playing",
|
||||
"icon_url": client.user.avatarURL
|
||||
},
|
||||
"fields": [{
|
||||
"name": "ℹ️ Title:",
|
||||
"value": info.title
|
||||
},
|
||||
{
|
||||
"name": "🎤 Artist:",
|
||||
"value": info.author
|
||||
},
|
||||
{
|
||||
"name": "💬 Channel:",
|
||||
"value": voiceChannel.name
|
||||
},
|
||||
{
|
||||
"name": `${"▬".repeat(parts)}🔘${"▬".repeat(10 - parts)}`,
|
||||
"value": `${moment.duration(0).format("m:ss", { trim: false })}/${info.isStream ? "∞" : moment.duration(info.length).format("m:ss", { trim: false })}`
|
||||
}]
|
||||
}
|
||||
});
|
||||
let playingMessage;
|
||||
if (lastTrack === track) {
|
||||
playingMessage = oldPlaying;
|
||||
} else {
|
||||
playingMessage = await client.createMessage(message.channel.id, !music ? "🔊 Playing sound..." : {
|
||||
"embed": {
|
||||
"color": 16711680,
|
||||
"author": {
|
||||
"name": "Now Playing",
|
||||
"icon_url": client.user.avatarURL
|
||||
},
|
||||
"fields": [{
|
||||
"name": "ℹ️ Title:",
|
||||
"value": info.title
|
||||
},
|
||||
{
|
||||
"name": "🎤 Artist:",
|
||||
"value": info.author
|
||||
},
|
||||
{
|
||||
"name": "💬 Channel:",
|
||||
"value": voiceChannel.name
|
||||
},
|
||||
{
|
||||
"name": `${"▬".repeat(parts)}🔘${"▬".repeat(10 - parts)}`,
|
||||
"value": `${day.duration(0).format("m:ss", { trim: false })}/${info.isStream ? "∞" : day.duration(info.length).format("m:ss", { trim: false })}`
|
||||
}]
|
||||
}
|
||||
});
|
||||
}
|
||||
await connection.play(track);
|
||||
this.players.set(voiceChannel.guild.id, { player: connection, type: music ? "music" : "sound", host: message.author.id, voiceChannel: voiceChannel, originalChannel: message.channel, loop: loop });
|
||||
if (inQueue && connection.listeners("error").length === 0) {
|
||||
|
@ -154,152 +155,10 @@ exports.nextSong = async (message, connection, track, info, music, voiceChannel,
|
|||
if (music) await client.createMessage(message.channel.id, "🔊 The current voice channel session has ended.");
|
||||
if (playingMessage.channel.messages.get(playingMessage.id)) await playingMessage.delete();
|
||||
} else {
|
||||
const track = await fetch(`http://${connection.node.host}:${connection.node.port}/decodetrack?track=${encodeURIComponent(newQueue[0])}`, { headers: { Authorization: connection.node.password } }).then(res => res.json());
|
||||
this.nextSong(message, connection, newQueue[0], track, music, voiceChannel, isLooping, true);
|
||||
if (playingMessage.channel.messages.get(playingMessage.id)) await playingMessage.delete();
|
||||
const newTrack = await fetch(`http://${connection.node.host}:${connection.node.port}/decodetrack?track=${encodeURIComponent(newQueue[0])}`, { headers: { Authorization: connection.node.password } }).then(res => res.json());
|
||||
this.nextSong(client, message, connection, newQueue[0], newTrack, music, voiceChannel, isLooping, true, track, playingMessage);
|
||||
if (newQueue[0] !== track && playingMessage.channel.messages.get(playingMessage.id)) await playingMessage.delete();
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
exports.stop = async (message) => {
|
||||
if (!message.channel.guild) return `${message.author.mention}, this command only works in servers!`;
|
||||
if (!message.member.voiceState.channelID) return `${message.author.mention}, you need to be in a voice channel first!`;
|
||||
if (!message.channel.guild.members.get(client.user.id).voiceState.channelID) return `${message.author.mention}, I'm not in a voice channel!`;
|
||||
if (this.players.get(message.channel.guild.id).host !== message.author.id) return `${message.author.mention}, only the current voice session host can stop the music!`;
|
||||
this.manager.leave(message.channel.guild.id);
|
||||
const connection = this.players.get(message.channel.guild.id).player;
|
||||
connection.destroy();
|
||||
this.players.delete(message.channel.guild.id);
|
||||
this.queues.delete(message.channel.guild.id);
|
||||
return "🔊 The current voice channel session has ended.";
|
||||
};
|
||||
|
||||
exports.skip = async (message) => {
|
||||
if (!message.channel.guild) return `${message.author.mention}, this command only works in servers!`;
|
||||
if (!message.member.voiceState.channelID) return `${message.author.mention}, you need to be in a voice channel first!`;
|
||||
if (!message.channel.guild.members.get(client.user.id).voiceState.channelID) return `${message.author.mention}, I'm not in a voice channel!`;
|
||||
const player = this.players.get(message.channel.guild.id);
|
||||
if (player.host !== message.author.id) {
|
||||
const votes = skipVotes.has(message.channel.guild.id) ? skipVotes.get(message.channel.guild.id) : { count: 0, ids: [] };
|
||||
if (votes.ids.includes(message.author.id)) return `${message.author.mention}, you've already voted to skip!`;
|
||||
const newObject = {
|
||||
count: votes.count + 1,
|
||||
ids: [...votes.ids, message.author.id].filter(item => !!item)
|
||||
};
|
||||
if (votes.count + 1 === 3) {
|
||||
player.player.stop(message.channel.guild.id);
|
||||
skipVotes.set(message.channel.guild.id, { count: 0, ids: [] });
|
||||
} else {
|
||||
skipVotes.set(message.channel.guild.id, newObject);
|
||||
return `🔊 Voted to skip song (${votes.count + 1}/3 people have voted).`;
|
||||
}
|
||||
} else {
|
||||
player.player.stop(message.channel.guild.id);
|
||||
return;
|
||||
}
|
||||
};
|
||||
|
||||
exports.pause = async (message) => {
|
||||
if (!message.channel.guild) return `${message.author.mention}, this command only works in servers!`;
|
||||
if (!message.member.voiceState.channelID) return `${message.author.mention}, you need to be in a voice channel first!`;
|
||||
if (!message.channel.guild.members.get(client.user.id).voiceState.channelID) return `${message.author.mention}, I'm not in a voice channel!`;
|
||||
if (this.players.get(message.channel.guild.id).host !== message.author.id) return `${message.author.mention}, only the current voice session host can pause/resume the music!`;
|
||||
const player = this.players.get(message.channel.guild.id).player;
|
||||
player.pause(!player.paused ? true : false);
|
||||
return `🔊 The player has been ${!player.paused ? "paused" : "resumed"}.`;
|
||||
};
|
||||
|
||||
exports.playing = async (message) => {
|
||||
if (!message.channel.guild) return `${message.author.mention}, this command only works in servers!`;
|
||||
if (!message.member.voiceState.channelID) return `${message.author.mention}, you need to be in a voice channel first!`;
|
||||
if (!message.channel.guild.members.get(client.user.id).voiceState.channelID) return `${message.author.mention}, I'm not in a voice channel!`;
|
||||
const player = this.players.get(message.channel.guild.id).player;
|
||||
if (!player) return `${message.author.mention}, I'm not playing anything!`;
|
||||
const track = await fetch(`http://${player.node.host}:${player.node.port}/decodetrack?track=${encodeURIComponent(player.track)}`, { headers: { Authorization: player.node.password } }).then(res => res.json());
|
||||
const parts = Math.floor((player.state.position / track.length) * 10);
|
||||
return {
|
||||
"embed": {
|
||||
"color": 16711680,
|
||||
"author": {
|
||||
"name": "Now Playing",
|
||||
"icon_url": client.user.avatarURL
|
||||
},
|
||||
"fields": [{
|
||||
"name": "ℹ️ Title:",
|
||||
"value": track.title ? track.title : "Unknown"
|
||||
},
|
||||
{
|
||||
"name": "🎤 Artist:",
|
||||
"value": track.author ? track.author : "Unknown"
|
||||
},
|
||||
{
|
||||
"name": "💬 Channel:",
|
||||
"value": message.channel.guild.channels.get(message.member.voiceState.channelID).name
|
||||
},
|
||||
{
|
||||
"name": `${"▬".repeat(parts)}🔘${"▬".repeat(10 - parts)}`,
|
||||
"value": `${moment.duration(player.state.position).format("m:ss", { trim: false })}/${track.isStream ? "∞" : moment.duration(track.length).format("m:ss", { trim: false })}`
|
||||
}]
|
||||
}
|
||||
};
|
||||
};
|
||||
|
||||
exports.queue = async (message) => {
|
||||
if (!message.channel.guild) return `${message.author.mention}, this command only works in servers!`;
|
||||
if (!message.member.voiceState.channelID) return `${message.author.mention}, you need to be in a voice channel first!`;
|
||||
if (!message.channel.guild.members.get(client.user.id).voiceState.channelID) return `${message.author.mention}, I'm not in a voice channel!`;
|
||||
if (!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.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 queue = this.queues.get(message.channel.guild.id);
|
||||
const player = this.players.get(message.channel.guild.id);
|
||||
const tracks = await fetch(`http://${player.player.node.host}:${player.player.node.port}/decodetracks`, { method: "POST", body: JSON.stringify(queue), headers: { Authorization: player.player.node.password, "Content-Type": "application/json" } }).then(res => res.json());
|
||||
const trackList = [];
|
||||
const firstTrack = tracks.shift();
|
||||
for (const [i, track] of tracks.entries()) {
|
||||
trackList.push(`${i + 1}. ${track.info.author} - **${track.info.title}** (${track.info.isStream ? "∞" : moment.duration(track.info.length).format("m:ss", { trim: false })})`);
|
||||
}
|
||||
const pageSize = 5;
|
||||
const embeds = [];
|
||||
const groups = trackList.map((item, index) => {
|
||||
return index % pageSize === 0 ? trackList.slice(index, index + pageSize) : null;
|
||||
}).filter(Boolean);
|
||||
if (groups.length === 0) groups.push("del");
|
||||
for (const [i, value] of groups.entries()) {
|
||||
embeds.push({
|
||||
"embed": {
|
||||
"author": {
|
||||
"name": "Queue",
|
||||
"icon_url": client.user.avatarURL
|
||||
},
|
||||
"color": 16711680,
|
||||
"footer": {
|
||||
"text": `Page ${i + 1} of ${groups.length}`
|
||||
},
|
||||
"fields": [{
|
||||
"name": "🎶 Now Playing",
|
||||
"value": `${firstTrack.info.author} - **${firstTrack.info.title}** (${firstTrack.info.isStream ? "∞" : moment.duration(firstTrack.info.length).format("m:ss", { trim: false })})`
|
||||
}, {
|
||||
"name": "🔁 Looping?",
|
||||
"value": player.loop ? "Yes" : "No"
|
||||
}, {
|
||||
"name": "🗒️ Queue",
|
||||
"value": value !== "del" ? value.join("\n") : "There's nothing in the queue!"
|
||||
}]
|
||||
}
|
||||
});
|
||||
}
|
||||
if (embeds.length === 0) return `${message.author.mention}, there's nothing in the queue!`;
|
||||
return paginator(message, embeds);
|
||||
};
|
||||
|
||||
exports.loop = async (message) => {
|
||||
if (!message.channel.guild) return `${message.author.mention}, this command only works in servers!`;
|
||||
if (!message.member.voiceState.channelID) return `${message.author.mention}, you need to be in a voice channel first!`;
|
||||
if (!message.channel.guild.members.get(client.user.id).voiceState.channelID) return `${message.author.mention}, I'm not in a voice channel!`;
|
||||
if (this.players.get(message.channel.guild.id).host !== message.author.id) return `${message.author.mention}, only the current voice session host can loop the music!`;
|
||||
const object = this.players.get(message.channel.guild.id);
|
||||
object.loop = !object.loop;
|
||||
this.players.set(message.channel.guild.id, object);
|
||||
return object.loop ? "🔊 The player is now looping." : "🔊 The player is no longer looping.";
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue