Merge branch 'pm2'
This commit is contained in:
commit
273e5b94d7
46 changed files with 1818 additions and 923 deletions
|
@ -48,12 +48,7 @@ METRICS=
|
||||||
# The image API type to be used
|
# The image API type to be used
|
||||||
# Set this to `none` to process all images locally
|
# Set this to `none` to process all images locally
|
||||||
# Set this to `ws` if you want to use the external image API script, located in api/index.js
|
# Set this to `ws` if you want to use the external image API script, located in api/index.js
|
||||||
# Set this to `azure` to use the Azure Functions API
|
|
||||||
API_TYPE=none
|
API_TYPE=none
|
||||||
# If API_TYPE is `azure`, set this to your Azure webhook URL
|
|
||||||
AZURE_URL=
|
|
||||||
# If API_TYPE is `azure`, set an optional password for webhook responses
|
|
||||||
AZURE_PASS=
|
|
||||||
|
|
||||||
# Put ID of server to limit owner-only commands to
|
# Put ID of server to limit owner-only commands to
|
||||||
ADMIN_SERVER=
|
ADMIN_SERVER=
|
11
.gitignore
vendored
11
.gitignore
vendored
|
@ -118,14 +118,3 @@ libvips/
|
||||||
# Databases
|
# Databases
|
||||||
data/
|
data/
|
||||||
*.sqlite
|
*.sqlite
|
||||||
|
|
||||||
# Azure Functions artifacts
|
|
||||||
bin
|
|
||||||
obj
|
|
||||||
appsettings.json
|
|
||||||
local.settings.json
|
|
||||||
|
|
||||||
# Azurite artifacts
|
|
||||||
__blobstorage__
|
|
||||||
__queuestorage__
|
|
||||||
__azurite_db*__.json
|
|
||||||
|
|
|
@ -6,6 +6,9 @@ The esmBot image API is a combined HTTP and WebSocket API. The default port to a
|
||||||
### GET `/image/?id=<job id>`
|
### GET `/image/?id=<job id>`
|
||||||
Get image data after job is finished running. The Content-Type header is properly set.
|
Get image data after job is finished running. The Content-Type header is properly set.
|
||||||
|
|
||||||
|
### GET `/count`
|
||||||
|
Get the current amount of running jobs. Response is a plaintext number value.
|
||||||
|
|
||||||
## WebSockets
|
## WebSockets
|
||||||
A client sends *requests* (T-messages) to a server, which subsequently *replies* (R-messages) to the client.
|
A client sends *requests* (T-messages) to a server, which subsequently *replies* (R-messages) to the client.
|
||||||
### Message IDs
|
### Message IDs
|
||||||
|
@ -24,11 +27,11 @@ A client sends *requests* (T-messages) to a server, which subsequently *replies*
|
||||||
[j] means JSON data that goes until the end of the message.
|
[j] means JSON data that goes until the end of the message.
|
||||||
`tag` is used to identify a request/response pair, like `lock` in the original API. `jid` is used to identify a job. `job` is a job object.
|
`tag` is used to identify a request/response pair, like `lock` in the original API. `jid` is used to identify a job. `job` is a job object.
|
||||||
- Rerror tag[2] error[s]
|
- Rerror tag[2] error[s]
|
||||||
- Tqueue tag[2] jid[4] job[j]
|
- Tqueue tag[2] jid[8] job[j]
|
||||||
- Rqueue tag[2]
|
- Rqueue tag[2]
|
||||||
- Tcancel tag[2] jid[4]
|
- Tcancel tag[2] jid[8]
|
||||||
- Rcancel tag[2]
|
- Rcancel tag[2]
|
||||||
- Twait tag[2] jid[4]
|
- Twait tag[2] jid[8]
|
||||||
- Rwait tag[2]
|
- Rwait tag[2]
|
||||||
- Rinit tag[2] max_jobs[2] running_jobs[2] formats[j]
|
- Rinit tag[2] max_jobs[2] running_jobs[2] formats[j]
|
||||||
|
|
||||||
|
@ -42,6 +45,7 @@ The job object is formatted like this:
|
||||||
"params": { // content varies depending on the command, some common parameters are listed here
|
"params": { // content varies depending on the command, some common parameters are listed here
|
||||||
"type": string, // mime type of output, should usually be the same as input
|
"type": string, // mime type of output, should usually be the same as input
|
||||||
...
|
...
|
||||||
}
|
},
|
||||||
|
"name": string // filename of the image, without extension
|
||||||
}
|
}
|
||||||
```
|
```
|
||||||
|
|
13
api/index.js
13
api/index.js
|
@ -107,8 +107,8 @@ wss.on("connection", (ws, request) => {
|
||||||
const tag = msg.slice(1, 3);
|
const tag = msg.slice(1, 3);
|
||||||
const req = msg.toString().slice(3);
|
const req = msg.toString().slice(3);
|
||||||
if (opcode == Tqueue) {
|
if (opcode == Tqueue) {
|
||||||
const id = msg.readUInt32LE(3);
|
const id = msg.readBigInt64LE(3);
|
||||||
const obj = msg.slice(7);
|
const obj = msg.slice(11);
|
||||||
const job = { msg: obj, num: jobAmount, verifyEvent: new EventEmitter() };
|
const job = { msg: obj, num: jobAmount, verifyEvent: new EventEmitter() };
|
||||||
jobs.set(id, job);
|
jobs.set(id, job);
|
||||||
queue.push(id);
|
queue.push(id);
|
||||||
|
@ -128,7 +128,7 @@ wss.on("connection", (ws, request) => {
|
||||||
const cancelResponse = Buffer.concat([Buffer.from([Rcancel]), tag]);
|
const cancelResponse = Buffer.concat([Buffer.from([Rcancel]), tag]);
|
||||||
ws.send(cancelResponse);
|
ws.send(cancelResponse);
|
||||||
} else if (opcode == Twait) {
|
} else if (opcode == Twait) {
|
||||||
const id = msg.readUInt32LE(3);
|
const id = msg.readBigUInt64LE(3);
|
||||||
const job = jobs.get(id);
|
const job = jobs.get(id);
|
||||||
if (!job) {
|
if (!job) {
|
||||||
const errorResponse = Buffer.concat([Buffer.from([Rerror]), tag, Buffer.from("Invalid job ID")]);
|
const errorResponse = Buffer.concat([Buffer.from([Rerror]), tag, Buffer.from("Invalid job ID")]);
|
||||||
|
@ -178,7 +178,7 @@ httpServer.on("request", async (req, res) => {
|
||||||
res.statusCode = 400;
|
res.statusCode = 400;
|
||||||
return res.end("400 Bad Request");
|
return res.end("400 Bad Request");
|
||||||
}
|
}
|
||||||
const id = parseInt(reqUrl.searchParams.get("id"));
|
const id = BigInt(reqUrl.searchParams.get("id"));
|
||||||
if (!jobs.has(id)) {
|
if (!jobs.has(id)) {
|
||||||
res.statusCode = 410;
|
res.statusCode = 410;
|
||||||
return res.end("410 Gone");
|
return res.end("410 Gone");
|
||||||
|
@ -208,6 +208,11 @@ httpServer.on("request", async (req, res) => {
|
||||||
return res.end(data, (err) => {
|
return res.end(data, (err) => {
|
||||||
if (err) error(err);
|
if (err) error(err);
|
||||||
});
|
});
|
||||||
|
} else if (reqUrl.pathname === "/count" && req.method === "GET") {
|
||||||
|
log(`Sending job count to ${req.socket.remoteAddress}:${req.socket.remotePort} via HTTP`);
|
||||||
|
return res.end(jobAmount.toString(), (err) => {
|
||||||
|
if (err) error(err);
|
||||||
|
});
|
||||||
} else {
|
} else {
|
||||||
res.statusCode = 404;
|
res.statusCode = 404;
|
||||||
return res.end("404 Not Found");
|
return res.end("404 Not Found");
|
||||||
|
|
293
app.js
293
app.js
|
@ -1,12 +1,15 @@
|
||||||
if (process.platform === "win32") console.error("\x1b[1m\x1b[31m\x1b[40m" + `WIN32 IS NOT OFFICIALLY SUPPORTED!
|
if (process.versions.node.split(".")[0] < 16) {
|
||||||
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");
|
|
||||||
if (process.versions.node.split(".")[0] < 15) {
|
|
||||||
console.error(`You are currently running Node.js version ${process.version}.
|
console.error(`You are currently running Node.js version ${process.version}.
|
||||||
esmBot requires Node.js version 15 or above.
|
esmBot requires Node.js version 16 or above.
|
||||||
Please refer to step 3 of the setup guide.`);
|
Please refer to step 3 of the setup guide.`);
|
||||||
process.exit(1);
|
process.exit(1);
|
||||||
}
|
}
|
||||||
|
if (process.platform === "win32") {
|
||||||
|
console.error("\x1b[1m\x1b[31m\x1b[40m" + `WINDOWS 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 in 5 seconds, 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");
|
||||||
|
Atomics.wait(new Int32Array(new SharedArrayBuffer(4)), 0, 0, 5000);
|
||||||
|
}
|
||||||
|
|
||||||
// load config from .env file
|
// load config from .env file
|
||||||
import { resolve, dirname } from "path";
|
import { resolve, dirname } from "path";
|
||||||
|
@ -14,61 +17,58 @@ import { fileURLToPath } from "url";
|
||||||
import { config } from "dotenv";
|
import { config } from "dotenv";
|
||||||
config({ path: resolve(dirname(fileURLToPath(import.meta.url)), ".env") });
|
config({ path: resolve(dirname(fileURLToPath(import.meta.url)), ".env") });
|
||||||
|
|
||||||
// main sharding manager
|
import { generateList, createPage } from "./utils/help.js";
|
||||||
import { Fleet } from "eris-fleet";
|
import { reloadImageConnections } from "./utils/image.js";
|
||||||
import { isMaster } from "cluster";
|
|
||||||
// main services
|
// main services
|
||||||
import Shard from "./shard.js";
|
import Eris from "eris";
|
||||||
import ImageWorker from "./utils/services/image.js";
|
import pm2 from "pm2";
|
||||||
import PrometheusWorker from "./utils/services/prometheus.js";
|
|
||||||
// some utils
|
// some utils
|
||||||
import { promises, readFileSync } from "fs";
|
import { promises, readFileSync } from "fs";
|
||||||
import winston from "winston";
|
import { logger } from "./utils/logger.js";
|
||||||
import "winston-daily-rotate-file";
|
|
||||||
import { exec as baseExec } from "child_process";
|
import { exec as baseExec } from "child_process";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
|
||||||
const exec = promisify(baseExec);
|
const exec = promisify(baseExec);
|
||||||
|
// initialize command loader
|
||||||
|
import { load, send } from "./utils/handler.js";
|
||||||
|
// command collections
|
||||||
|
import { paths } from "./utils/collections.js";
|
||||||
// database stuff
|
// database stuff
|
||||||
import database from "./utils/database.js";
|
import database from "./utils/database.js";
|
||||||
// dbl posting
|
// lavalink stuff
|
||||||
import { Api } from "@top-gg/sdk";
|
import { checkStatus, connect, reload, status, connected } from "./utils/soundplayer.js";
|
||||||
const dbl = process.env.NODE_ENV === "production" && process.env.DBL ? new Api(process.env.DBL) : null;
|
// events
|
||||||
|
import { endBroadcast, startBroadcast, activityChanger, checkBroadcast } from "./utils/misc.js";
|
||||||
|
import { parseThreshold } from "./utils/tempimages.js";
|
||||||
|
|
||||||
const { types } = JSON.parse(readFileSync(new URL("./config/commands.json", import.meta.url)));
|
const { types } = JSON.parse(readFileSync(new URL("./config/commands.json", import.meta.url)));
|
||||||
|
|
||||||
if (isMaster) {
|
const esmBotVersion = JSON.parse(readFileSync(new URL("./package.json", import.meta.url))).version;
|
||||||
const esmBotVersion = JSON.parse(readFileSync(new URL("./package.json", import.meta.url))).version;
|
exec("git rev-parse HEAD").then(output => output.stdout.substring(0, 7), () => "unknown commit").then(o => process.env.GIT_REV = o).then(() => {
|
||||||
const erisFleetVersion = JSON.parse(readFileSync(new URL("./node_modules/eris-fleet/package.json", import.meta.url))).version; // a bit of a hacky way to get the eris-fleet version
|
|
||||||
console.log(`
|
console.log(`
|
||||||
,*\`$ z\`"v
|
,*\`$ z\`"v
|
||||||
F zBw\`% A ,W "W
|
F zBw\`% A ,W "W
|
||||||
,\` ,EBBBWp"%. ,-=~~==-,+* 4BBE T
|
,\` ,EBBBWp"%. ,-=~~==-,+* 4BBE T
|
||||||
M BBBBBBBB* ,w=####Wpw 4BBBBB# 1
|
M BBBBBBBB* ,w=####Wpw 4BBBBB# 1
|
||||||
F BBBBBBBMwBBBBBBBBBBBBB#wXBBBBBH E
|
F BBBBBBBMwBBBBBBBBBBBBB#wXBBBBBH E
|
||||||
F BBBBBBkBBBBBBBBBBBBBBBBBBBBE4BL k
|
F BBBBBBkBBBBBBBBBBBBBBBBBBBBE4BL k
|
||||||
# BFBBBBBBBBBBBBF" "RBBBW F
|
# BFBBBBBBBBBBBBF" "RBBBW F
|
||||||
V ' 4BBBBBBBBBBM TBBL F
|
V ' 4BBBBBBBBBBM TBBL F
|
||||||
F BBBBBBBBBBF JBB L
|
F BBBBBBBBBBF JBB L
|
||||||
F FBBBBBBBEB BBL 4
|
F FBBBBBBBEB BBL 4
|
||||||
E [BB4BBBBEBL BBL 4
|
E [BB4BBBBEBL BBL 4
|
||||||
I #BBBBBBBEB 4BBH *w
|
I #BBBBBBBEB 4BBH *w
|
||||||
A 4BBBBBBBBBEW, ,BBBB W [
|
A 4BBBBBBBBBEW, ,BBBB W [
|
||||||
.A ,k 4BBBBBBBBBBBEBW####BBBBBBM BF F
|
.A ,k 4BBBBBBBBBBBEBW####BBBBBBM BF F
|
||||||
k <BBBw BBBBEBBBBBBBBBBBBBBBBBQ4BM #
|
k <BBBw BBBBEBBBBBBBBBBBBBBBBBQ4BM #
|
||||||
5, REBBB4BBBBB#BBBBBBBBBBBBP5BFF ,F
|
5, REBBB4BBBBB#BBBBBBBBBBBBP5BFF ,F
|
||||||
*w \`*4BBW\`"FF#F##FFFF"\` , * +"
|
*w \`*4BBW\`"FF#F##FFFF"\` , * +"
|
||||||
*+, " F'"'*^~~~^"^\` V+*^
|
*+, " F'"'*^~~~^"^\` V+*^
|
||||||
\`"""
|
\`"""
|
||||||
|
|
||||||
esmBot ${esmBotVersion} (${(await exec("git rev-parse HEAD").then(output => output.stdout.substring(0, 7), () => "unknown commit"))}), powered by eris-fleet ${erisFleetVersion}
|
esmBot ${esmBotVersion} (${process.env.GIT_REV})
|
||||||
`);
|
`);
|
||||||
}
|
});
|
||||||
|
|
||||||
const services = [
|
|
||||||
{ name: "image", ServiceWorker: ImageWorker }
|
|
||||||
];
|
|
||||||
if (process.env.METRICS && process.env.METRICS !== "") services.push({ name: "prometheus", ServiceWorker: PrometheusWorker });
|
|
||||||
|
|
||||||
const intents = [
|
const intents = [
|
||||||
"guildVoiceStates",
|
"guildVoiceStates",
|
||||||
|
@ -80,21 +80,69 @@ if (types.classic) {
|
||||||
intents.push("messageContent");
|
intents.push("messageContent");
|
||||||
}
|
}
|
||||||
|
|
||||||
const Admiral = new Fleet({
|
// PM2-specific handling
|
||||||
BotWorker: Shard,
|
if (process.env.PM2_USAGE) {
|
||||||
token: `Bot ${process.env.TOKEN}`,
|
pm2.launchBus((err, pm2Bus) => {
|
||||||
fetchTimeout: 900000,
|
if (err) {
|
||||||
maxConcurrencyOverride: 1,
|
logger.error(err);
|
||||||
startingStatus: {
|
return;
|
||||||
status: "idle",
|
|
||||||
game: {
|
|
||||||
name: "Starting esmBot..."
|
|
||||||
}
|
}
|
||||||
|
|
||||||
|
pm2Bus.on("process:msg", async (packet) => {
|
||||||
|
switch (packet.data?.type) {
|
||||||
|
case "reload":
|
||||||
|
var path = paths.get(packet.data.message);
|
||||||
|
await load(bot, path, await checkStatus(), true);
|
||||||
|
break;
|
||||||
|
case "soundreload":
|
||||||
|
var soundStatus = await checkStatus();
|
||||||
|
if (!soundStatus) {
|
||||||
|
reload();
|
||||||
|
}
|
||||||
|
break;
|
||||||
|
case "imagereload":
|
||||||
|
await reloadImageConnections();
|
||||||
|
break;
|
||||||
|
case "broadcastStart":
|
||||||
|
startBroadcast(bot, packet.data.message);
|
||||||
|
break;
|
||||||
|
case "broadcastEnd":
|
||||||
|
endBroadcast(bot);
|
||||||
|
break;
|
||||||
|
case "serverCounts":
|
||||||
|
pm2.sendDataToProcessId(0, {
|
||||||
|
id: 0,
|
||||||
|
type: "process:msg",
|
||||||
|
data: {
|
||||||
|
type: "serverCounts",
|
||||||
|
guilds: bot.guilds.size,
|
||||||
|
shards: bot.shards.size
|
||||||
},
|
},
|
||||||
whatToLog: {
|
topic: true
|
||||||
blacklist: ["stats_update"]
|
}, (err) => {
|
||||||
},
|
if (err) logger.error(err);
|
||||||
clientOptions: {
|
});
|
||||||
|
break;
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
database.upgrade(logger).then(result => {
|
||||||
|
if (result === 1) return process.exit(1);
|
||||||
|
});
|
||||||
|
|
||||||
|
// process the threshold into bytes early
|
||||||
|
if (process.env.TEMPDIR && process.env.THRESHOLD) {
|
||||||
|
parseThreshold();
|
||||||
|
}
|
||||||
|
|
||||||
|
if (!types.classic && !types.application) {
|
||||||
|
logger.error("Both classic and application commands are disabled! Please enable at least one command type in config/commands.json.");
|
||||||
|
process.exit(1);
|
||||||
|
}
|
||||||
|
|
||||||
|
const bot = new Eris(`Bot ${process.env.TOKEN}`, {
|
||||||
allowedMentions: {
|
allowedMentions: {
|
||||||
everyone: false,
|
everyone: false,
|
||||||
roles: false,
|
roles: false,
|
||||||
|
@ -102,93 +150,78 @@ const Admiral = new Fleet({
|
||||||
repliedUser: true
|
repliedUser: true
|
||||||
},
|
},
|
||||||
restMode: true,
|
restMode: true,
|
||||||
|
maxShards: "auto",
|
||||||
messageLimit: 50,
|
messageLimit: 50,
|
||||||
intents,
|
intents,
|
||||||
stats: {
|
|
||||||
requestTimeout: 30000
|
|
||||||
},
|
|
||||||
connectionTimeout: 30000
|
connectionTimeout: 30000
|
||||||
},
|
|
||||||
useCentralRequestHandler: process.env.DEBUG_LOG ? false : true, // workaround for eris-fleet weirdness
|
|
||||||
services
|
|
||||||
});
|
});
|
||||||
|
|
||||||
if (isMaster) {
|
bot.once("ready", async () => {
|
||||||
const logger = winston.createLogger({
|
// register commands and their info
|
||||||
levels: {
|
const soundStatus = await checkStatus();
|
||||||
error: 0,
|
logger.log("info", "Attempting to load commands...");
|
||||||
warn: 1,
|
for await (const commandFile of getFiles(resolve(dirname(fileURLToPath(import.meta.url)), "./commands/"))) {
|
||||||
info: 2,
|
logger.log("main", `Loading command from ${commandFile}...`);
|
||||||
main: 3,
|
try {
|
||||||
debug: 4
|
await load(bot, commandFile, soundStatus);
|
||||||
},
|
} catch (e) {
|
||||||
transports: [
|
logger.error(`Failed to register command from ${commandFile}: ${e}`);
|
||||||
new winston.transports.Console({ format: winston.format.colorize({ all: true }), stderrLevels: ["error", "warn"] }),
|
}
|
||||||
new winston.transports.DailyRotateFile({ filename: "logs/error-%DATE%.log", level: "error", zippedArchive: true, maxSize: 4194304, maxFiles: 8 }),
|
}
|
||||||
new winston.transports.DailyRotateFile({ filename: "logs/main-%DATE%.log", zippedArchive: true, maxSize: 4194304, maxFiles: 8 })
|
if (types.application) {
|
||||||
],
|
try {
|
||||||
level: process.env.DEBUG_LOG ? "debug" : "main",
|
await send(bot);
|
||||||
format: winston.format.combine(
|
} catch (e) {
|
||||||
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
logger.log("error", e);
|
||||||
winston.format.printf((info) => {
|
logger.log("error", "Failed to send command data to Discord, slash/message commands may be unavailable.");
|
||||||
const {
|
}
|
||||||
timestamp, level, message, ...args
|
}
|
||||||
} = info;
|
logger.log("info", "Finished loading commands.");
|
||||||
|
|
||||||
return `[${timestamp}]: [${level.toUpperCase()}] - ${message} ${Object.keys(args).length ? JSON.stringify(args, null, 2) : ""}`;
|
if (process.env.API_TYPE === "ws") await reloadImageConnections();
|
||||||
}),
|
await database.setup();
|
||||||
)
|
|
||||||
});
|
|
||||||
|
|
||||||
winston.addColors({
|
// register events
|
||||||
info: "green",
|
logger.log("info", "Attempting to load events...");
|
||||||
main: "gray",
|
for await (const file of getFiles(resolve(dirname(fileURLToPath(import.meta.url)), "./events/"))) {
|
||||||
debug: "magenta",
|
logger.log("main", `Loading event from ${file}...`);
|
||||||
warn: "yellow",
|
const eventArray = file.split("/");
|
||||||
error: "red"
|
const eventName = eventArray[eventArray.length - 1].split(".")[0];
|
||||||
});
|
if (eventName === "interactionCreate" && !types.application) {
|
||||||
|
logger.log("warn", `Skipped loading event from ${file} because application commands are disabled`);
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
const { default: event } = await import(file);
|
||||||
|
bot.on(eventName, event.bind(null, bot));
|
||||||
|
}
|
||||||
|
logger.log("info", "Finished loading events.");
|
||||||
|
|
||||||
database.upgrade(logger).then(result => {
|
// generate docs
|
||||||
if (result === 1) return process.exit(1);
|
if (process.env.OUTPUT && process.env.OUTPUT !== "") {
|
||||||
});
|
generateList();
|
||||||
|
await createPage(process.env.OUTPUT);
|
||||||
Admiral.on("log", (m) => logger.main(m));
|
logger.log("info", "The help docs have been generated.");
|
||||||
Admiral.on("info", (m) => logger.info(m));
|
|
||||||
Admiral.on("debug", (m) => logger.debug(m));
|
|
||||||
Admiral.on("warn", (m) => logger.warn(m));
|
|
||||||
Admiral.on("error", (m) => logger.error(m));
|
|
||||||
|
|
||||||
if (dbl) {
|
|
||||||
Admiral.on("stats", async (m) => {
|
|
||||||
await dbl.postStats({
|
|
||||||
serverCount: m.guilds,
|
|
||||||
shardCount: m.shardCount
|
|
||||||
});
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
|
|
||||||
// process the threshold into bytes early
|
// connect to lavalink
|
||||||
if (process.env.TEMPDIR && process.env.THRESHOLD) {
|
if (!status && !connected) connect(bot);
|
||||||
const matched = process.env.THRESHOLD.match(/(\d+)([KMGT])/);
|
|
||||||
const sizes = {
|
checkBroadcast(bot);
|
||||||
K: 1024,
|
activityChanger(bot);
|
||||||
M: 1048576,
|
|
||||||
G: 1073741824,
|
logger.log("info", "Started esmBot.");
|
||||||
T: 1099511627776
|
});
|
||||||
};
|
|
||||||
if (matched && matched[1] && matched[2]) {
|
async function* getFiles(dir) {
|
||||||
process.env.THRESHOLD = matched[1] * sizes[matched[2]];
|
const dirents = await promises.readdir(dir, { withFileTypes: true });
|
||||||
} else {
|
for (const dirent of dirents) {
|
||||||
logger.error("Invalid THRESHOLD config.");
|
const name = dir + (dir.charAt(dir.length - 1) !== "/" ? "/" : "") + dirent.name;
|
||||||
process.env.THRESHOLD = undefined;
|
if (dirent.isDirectory()) {
|
||||||
|
yield* getFiles(name);
|
||||||
|
} else if (dirent.name.endsWith(".js")) {
|
||||||
|
yield name;
|
||||||
}
|
}
|
||||||
const dirstat = (await promises.readdir(process.env.TEMPDIR)).map((file) => {
|
|
||||||
return promises.stat(`${process.env.TEMPDIR}/${file}`).then((stats) => stats.size);
|
|
||||||
});
|
|
||||||
const size = await Promise.all(dirstat);
|
|
||||||
const reduced = size.reduce((a, b) => {
|
|
||||||
return a + b;
|
|
||||||
}, 0);
|
|
||||||
Admiral.centralStore.set("dirSizeCache", reduced);
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
bot.connect();
|
|
@ -1,10 +1,7 @@
|
||||||
class Command {
|
class Command {
|
||||||
success = true;
|
success = true;
|
||||||
constructor(client, cluster, worker, ipc, options) {
|
constructor(client, options) {
|
||||||
this.client = client;
|
this.client = client;
|
||||||
this.cluster = cluster;
|
|
||||||
this.worker = worker;
|
|
||||||
this.ipc = ipc;
|
|
||||||
this.origOptions = options;
|
this.origOptions = options;
|
||||||
this.type = options.type;
|
this.type = options.type;
|
||||||
this.args = options.args;
|
this.args = options.args;
|
||||||
|
@ -50,7 +47,7 @@ class Command {
|
||||||
async acknowledge() {
|
async acknowledge() {
|
||||||
if (this.type === "classic") {
|
if (this.type === "classic") {
|
||||||
await this.client.sendChannelTyping(this.channel.id);
|
await this.client.sendChannelTyping(this.channel.id);
|
||||||
} else {
|
} else if (!this.interaction.acknowledged) {
|
||||||
await this.interaction.acknowledge();
|
await this.interaction.acknowledge();
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,5 +1,6 @@
|
||||||
import Command from "./command.js";
|
import Command from "./command.js";
|
||||||
import imageDetect from "../utils/imagedetect.js";
|
import imageDetect from "../utils/imagedetect.js";
|
||||||
|
import { runImageJob } from "../utils/image.js";
|
||||||
import { runningCommands } from "../utils/collections.js";
|
import { runningCommands } from "../utils/collections.js";
|
||||||
import { readFileSync } from "fs";
|
import { readFileSync } from "fs";
|
||||||
const { emotes } = JSON.parse(readFileSync(new URL("../config/messages.json", import.meta.url)));
|
const { emotes } = JSON.parse(readFileSync(new URL("../config/messages.json", import.meta.url)));
|
||||||
|
@ -22,7 +23,7 @@ class ImageCommand extends Command {
|
||||||
// before awaiting the command result, add this command to the set of running commands
|
// before awaiting the command result, add this command to the set of running commands
|
||||||
runningCommands.set(this.author.id, timestamp);
|
runningCommands.set(this.author.id, timestamp);
|
||||||
|
|
||||||
const magickParams = {
|
const imageParams = {
|
||||||
cmd: this.constructor.command,
|
cmd: this.constructor.command,
|
||||||
params: {}
|
params: {}
|
||||||
};
|
};
|
||||||
|
@ -36,7 +37,7 @@ class ImageCommand extends Command {
|
||||||
if (selection) selectedImages.delete(this.author.id);
|
if (selection) selectedImages.delete(this.author.id);
|
||||||
if (image === undefined) {
|
if (image === undefined) {
|
||||||
runningCommands.delete(this.author.id);
|
runningCommands.delete(this.author.id);
|
||||||
return this.constructor.noImage;
|
return `${this.constructor.noImage} (Tip: try right-clicking/holding on a message and press Apps -> Select Image, then try again.)`;
|
||||||
} else if (image.type === "large") {
|
} else if (image.type === "large") {
|
||||||
runningCommands.delete(this.author.id);
|
runningCommands.delete(this.author.id);
|
||||||
return "That image is too large (>= 25MB)! Try using a smaller image.";
|
return "That image is too large (>= 25MB)! Try using a smaller image.";
|
||||||
|
@ -44,11 +45,12 @@ class ImageCommand extends Command {
|
||||||
runningCommands.delete(this.author.id);
|
runningCommands.delete(this.author.id);
|
||||||
return "I've been rate-limited by Tenor. Please try uploading your GIF elsewhere.";
|
return "I've been rate-limited by Tenor. Please try uploading your GIF elsewhere.";
|
||||||
}
|
}
|
||||||
magickParams.path = image.path;
|
imageParams.path = image.path;
|
||||||
magickParams.params.type = image.type;
|
imageParams.params.type = image.type;
|
||||||
magickParams.url = image.url; // technically not required but can be useful for text filtering
|
imageParams.url = image.url; // technically not required but can be useful for text filtering
|
||||||
magickParams.name = image.name;
|
imageParams.name = image.name;
|
||||||
if (this.constructor.requiresGIF) magickParams.onlyGIF = true;
|
imageParams.id = (this.interaction ?? this.message).id;
|
||||||
|
if (this.constructor.requiresGIF) imageParams.onlyGIF = true;
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
runningCommands.delete(this.author.id);
|
runningCommands.delete(this.author.id);
|
||||||
throw e;
|
throw e;
|
||||||
|
@ -57,31 +59,31 @@ class ImageCommand extends Command {
|
||||||
|
|
||||||
if (this.constructor.requiresText) {
|
if (this.constructor.requiresText) {
|
||||||
const text = this.options.text ?? this.args.join(" ").trim();
|
const text = this.options.text ?? this.args.join(" ").trim();
|
||||||
if (text.length === 0 || !await this.criteria(text, magickParams.url)) {
|
if (text.length === 0 || !await this.criteria(text, imageParams.url)) {
|
||||||
runningCommands.delete(this.author.id);
|
runningCommands.delete(this.author.id);
|
||||||
return this.constructor.noText;
|
return this.constructor.noText;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
if (typeof this.params === "function") {
|
if (typeof this.params === "function") {
|
||||||
Object.assign(magickParams.params, this.params(magickParams.url, magickParams.name));
|
Object.assign(imageParams.params, this.params(imageParams.url, imageParams.name));
|
||||||
} else if (typeof this.params === "object") {
|
} else if (typeof this.params === "object") {
|
||||||
Object.assign(magickParams.params, this.params);
|
Object.assign(imageParams.params, this.params);
|
||||||
}
|
}
|
||||||
|
|
||||||
let status;
|
let status;
|
||||||
if (magickParams.params.type === "image/gif" && this.type === "classic") {
|
if (imageParams.params.type === "image/gif" && this.type === "classic") {
|
||||||
status = await this.processMessage(this.message);
|
status = await this.processMessage(this.message);
|
||||||
}
|
}
|
||||||
|
|
||||||
try {
|
try {
|
||||||
const { buffer, type } = await this.ipc.serviceCommand("image", { type: "run", obj: magickParams }, true, 9000000);
|
const { arrayBuffer, type } = await runImageJob(imageParams);
|
||||||
if (type === "nogif" && this.constructor.requiresGIF) {
|
if (type === "nogif" && this.constructor.requiresGIF) {
|
||||||
return "That isn't a GIF!";
|
return "That isn't a GIF!";
|
||||||
}
|
}
|
||||||
this.success = true;
|
this.success = true;
|
||||||
return {
|
return {
|
||||||
file: Buffer.from(buffer.data),
|
file: Buffer.from(arrayBuffer),
|
||||||
name: `${this.constructor.command}.${type}`
|
name: `${this.constructor.command}.${type}`
|
||||||
};
|
};
|
||||||
} catch (e) {
|
} catch (e) {
|
||||||
|
|
|
@ -2,8 +2,8 @@ import Command from "./command.js";
|
||||||
import { players, queues } from "../utils/soundplayer.js";
|
import { players, queues } from "../utils/soundplayer.js";
|
||||||
|
|
||||||
class MusicCommand extends Command {
|
class MusicCommand extends Command {
|
||||||
constructor(client, cluster, worker, ipc, options) {
|
constructor(client, options) {
|
||||||
super(client, cluster, worker, ipc, options);
|
super(client, options);
|
||||||
if (this.channel.guild) {
|
if (this.channel.guild) {
|
||||||
this.connection = players.get(this.channel.guild.id);
|
this.connection = players.get(this.channel.guild.id);
|
||||||
this.queue = queues.get(this.channel.guild.id);
|
this.queue = queues.get(this.channel.guild.id);
|
||||||
|
|
|
@ -7,11 +7,11 @@ class AvatarCommand extends Command {
|
||||||
const self = await this.client.getRESTUser(this.author.id);
|
const self = await this.client.getRESTUser(this.author.id);
|
||||||
if (this.type === "classic" && this.message.mentions[0]) {
|
if (this.type === "classic" && this.message.mentions[0]) {
|
||||||
return this.message.mentions[0].dynamicAvatarURL(null, 512);
|
return this.message.mentions[0].dynamicAvatarURL(null, 512);
|
||||||
} else if (await this.ipc.fetchUser(member)) {
|
} else if (member) {
|
||||||
let user = await this.ipc.fetchUser(member);
|
const user = await this.client.getRESTUser(member);
|
||||||
if (!user) user = await this.client.getRESTUser(member);
|
if (user) {
|
||||||
return user?.avatar ? this.client._formatImage(`/avatars/${user.id}/${user.avatar}`, null, 512) : `https://cdn.discordapp.com/embed/avatars/${user.discriminator % 5}.png`; // hacky "solution"
|
return user?.avatar ? this.client._formatImage(`/avatars/${user.id}/${user.avatar}`, null, 512) : `https://cdn.discordapp.com/embed/avatars/${user.discriminator % 5}.png`; // hacky "solution"
|
||||||
} else if (mentionRegex.test(member)) {
|
} else if (mentionRegex.text(member)) {
|
||||||
const id = member.match(mentionRegex)[1];
|
const id = member.match(mentionRegex)[1];
|
||||||
if (id < 21154535154122752n) {
|
if (id < 21154535154122752n) {
|
||||||
this.success = false;
|
this.success = false;
|
||||||
|
@ -23,6 +23,9 @@ class AvatarCommand extends Command {
|
||||||
} catch {
|
} catch {
|
||||||
return self.dynamicAvatarURL(null, 512);
|
return self.dynamicAvatarURL(null, 512);
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return self.dynamicAvatarURL(null, 512);
|
||||||
|
}
|
||||||
} else if (this.args.join(" ") !== "" && this.channel.guild) {
|
} else if (this.args.join(" ") !== "" && this.channel.guild) {
|
||||||
const searched = await this.channel.guild.searchMembers(this.args.join(" "));
|
const searched = await this.channel.guild.searchMembers(this.args.join(" "));
|
||||||
if (searched.length === 0) return self.dynamicAvatarURL(null, 512);
|
if (searched.length === 0) return self.dynamicAvatarURL(null, 512);
|
||||||
|
|
|
@ -7,10 +7,11 @@ class BannerCommand extends Command {
|
||||||
const self = await this.client.getRESTUser(this.author.id);
|
const self = await this.client.getRESTUser(this.author.id);
|
||||||
if (this.type === "classic" && this.message.mentions[0]) {
|
if (this.type === "classic" && this.message.mentions[0]) {
|
||||||
return this.message.mentions[0].dynamicBannerURL(null, 512) ?? "This user doesn't have a banner!";
|
return this.message.mentions[0].dynamicBannerURL(null, 512) ?? "This user doesn't have a banner!";
|
||||||
} else if (await this.ipc.fetchUser(member)) {
|
} else if (member) {
|
||||||
const user = await this.client.getRESTUser(member);
|
const user = await this.client.getRESTUser(member);
|
||||||
|
if (user) {
|
||||||
return user.dynamicBannerURL(null, 512) ?? "This user doesn't have a banner!";
|
return user.dynamicBannerURL(null, 512) ?? "This user doesn't have a banner!";
|
||||||
} else if (mentionRegex.test(member)) {
|
} else if (mentionRegex.text(member)) {
|
||||||
const id = member.match(mentionRegex)[1];
|
const id = member.match(mentionRegex)[1];
|
||||||
if (id < 21154535154122752n) {
|
if (id < 21154535154122752n) {
|
||||||
this.success = false;
|
this.success = false;
|
||||||
|
@ -22,6 +23,9 @@ class BannerCommand extends Command {
|
||||||
} catch {
|
} catch {
|
||||||
return self.dynamicBannerURL(null, 512) ?? "You don't have a banner!";
|
return self.dynamicBannerURL(null, 512) ?? "You don't have a banner!";
|
||||||
}
|
}
|
||||||
|
} else {
|
||||||
|
return "This user doesn't have a banner!";
|
||||||
|
}
|
||||||
} else if (this.args.join(" ") !== "" && this.channel.guild) {
|
} else if (this.args.join(" ") !== "" && this.channel.guild) {
|
||||||
const searched = await this.channel.guild.searchMembers(this.args.join(" "));
|
const searched = await this.channel.guild.searchMembers(this.args.join(" "));
|
||||||
if (searched.length === 0) return self.dynamicBannerURL(null, 512) ?? "This user doesn't have a banner!";
|
if (searched.length === 0) return self.dynamicBannerURL(null, 512) ?? "This user doesn't have a banner!";
|
||||||
|
|
|
@ -1,33 +1,39 @@
|
||||||
import Command from "../../classes/command.js";
|
import Command from "../../classes/command.js";
|
||||||
|
import { endBroadcast, startBroadcast } from "../../utils/misc.js";
|
||||||
|
|
||||||
class BroadcastCommand extends Command {
|
class BroadcastCommand extends Command {
|
||||||
// yet another very hacky command
|
async run() {
|
||||||
run() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const owners = process.env.OWNER.split(",");
|
const owners = process.env.OWNER.split(",");
|
||||||
if (!owners.includes(this.author.id)) {
|
if (!owners.includes(this.author.id)) {
|
||||||
this.success = false;
|
this.success = false;
|
||||||
resolve("Only the bot owner can broadcast messages!");
|
return "Only the bot owner can broadcast messages!";
|
||||||
return;
|
|
||||||
}
|
}
|
||||||
const message = this.options.message ?? this.args.join(" ");
|
const message = this.options.message ?? this.args.join(" ");
|
||||||
if (message?.trim()) {
|
if (message?.trim()) {
|
||||||
this.ipc.centralStore.set("broadcast", message);
|
startBroadcast(this.client, message);
|
||||||
this.ipc.broadcast("playbroadcast", message);
|
if (process.env.PM2_USAGE) {
|
||||||
this.ipc.register("broadcastSuccess", () => {
|
process.send({
|
||||||
this.ipc.unregister("broadcastSuccess");
|
type: "process:msg",
|
||||||
resolve("Successfully broadcasted message.");
|
data: {
|
||||||
});
|
type: "broadcastStart",
|
||||||
} else {
|
message
|
||||||
this.ipc.centralStore.delete("broadcast");
|
|
||||||
this.ipc.broadcast("broadcastend");
|
|
||||||
this.ipc.register("broadcastEnd", () => {
|
|
||||||
this.ipc.unregister("broadcastEnd");
|
|
||||||
resolve("Successfully ended broadcast.");
|
|
||||||
});
|
|
||||||
}
|
}
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return "Started broadcast.";
|
||||||
|
} else {
|
||||||
|
endBroadcast(this.client);
|
||||||
|
if (process.env.PM2_USAGE) {
|
||||||
|
process.send({
|
||||||
|
type: "process:msg",
|
||||||
|
data: {
|
||||||
|
type: "broadcastEnd"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return "Ended broadcast.";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static flags = [{
|
static flags = [{
|
||||||
name: "message",
|
name: "message",
|
||||||
|
|
|
@ -1,4 +1,5 @@
|
||||||
import Command from "../../classes/command.js";
|
import Command from "../../classes/command.js";
|
||||||
|
import { reloadImageConnections } from "../../utils/image.js";
|
||||||
|
|
||||||
class ImageReloadCommand extends Command {
|
class ImageReloadCommand extends Command {
|
||||||
async run() {
|
async run() {
|
||||||
|
@ -7,9 +8,18 @@ class ImageReloadCommand extends Command {
|
||||||
this.success = false;
|
this.success = false;
|
||||||
return "Only the bot owner can reload the image servers!";
|
return "Only the bot owner can reload the image servers!";
|
||||||
}
|
}
|
||||||
const amount = await this.ipc.serviceCommand("image", { type: "reload" }, true);
|
await this.acknowledge();
|
||||||
if (amount > 0) {
|
const length = await reloadImageConnections();
|
||||||
return `Successfully connected to ${amount} image servers.`;
|
if (!length) {
|
||||||
|
if (process.env.PM2_USAGE) {
|
||||||
|
process.send({
|
||||||
|
type: "process:msg",
|
||||||
|
data: {
|
||||||
|
type: "imagereload"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
return `Successfully connected to ${length} image server(s).`;
|
||||||
} else {
|
} else {
|
||||||
return "I couldn't connect to any image servers!";
|
return "I couldn't connect to any image servers!";
|
||||||
}
|
}
|
||||||
|
|
|
@ -1,9 +1,9 @@
|
||||||
import Command from "../../classes/command.js";
|
import Command from "../../classes/command.js";
|
||||||
|
import { connections } from "../../utils/image.js";
|
||||||
|
|
||||||
class ImageStatsCommand extends Command {
|
class ImageStatsCommand extends Command {
|
||||||
async run() {
|
async run() {
|
||||||
await this.acknowledge();
|
await this.acknowledge();
|
||||||
const servers = await this.ipc.serviceCommand("image", { type: "stats" }, true);
|
|
||||||
const embed = {
|
const embed = {
|
||||||
embeds: [{
|
embeds: [{
|
||||||
"author": {
|
"author": {
|
||||||
|
@ -11,14 +11,17 @@ class ImageStatsCommand extends Command {
|
||||||
"icon_url": this.client.user.avatarURL
|
"icon_url": this.client.user.avatarURL
|
||||||
},
|
},
|
||||||
"color": 16711680,
|
"color": 16711680,
|
||||||
"description": `The bot is currently connected to ${servers.length} image server(s).`,
|
"description": `The bot is currently connected to ${connections.size} image server(s).`,
|
||||||
"fields": []
|
"fields": []
|
||||||
}]
|
}]
|
||||||
};
|
};
|
||||||
for (let i = 0; i < servers.length; i++) {
|
let i = 0;
|
||||||
|
for (const connection of connections.values()) {
|
||||||
|
const count = await connection.getCount();
|
||||||
|
if (!count) continue;
|
||||||
embed.embeds[0].fields.push({
|
embed.embeds[0].fields.push({
|
||||||
name: `Server ${i + 1}`,
|
name: `Server ${i++}`,
|
||||||
value: `Running Jobs: ${Math.min(servers[i].runningJobs, servers[i].max)}\nQueued: ${Math.max(0, servers[i].runningJobs - servers[i].max)}\nMax Jobs: ${servers[i].max}`
|
value: `Running Jobs: ${count}`
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
return embed;
|
return embed;
|
||||||
|
|
|
@ -5,13 +5,14 @@ const { version } = JSON.parse(readFileSync(new URL("../../package.json", import
|
||||||
import Command from "../../classes/command.js";
|
import Command from "../../classes/command.js";
|
||||||
import { exec as baseExec } from "child_process";
|
import { exec as baseExec } from "child_process";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
import { getServers } from "../../utils/misc.js";
|
||||||
const exec = promisify(baseExec);
|
const exec = promisify(baseExec);
|
||||||
|
|
||||||
class InfoCommand extends Command {
|
class InfoCommand extends Command {
|
||||||
async run() {
|
async run() {
|
||||||
let owner = await this.ipc.fetchUser(process.env.OWNER.split(",")[0]);
|
const owner = await this.client.getRESTUser(process.env.OWNER.split(",")[0]);
|
||||||
if (!owner) owner = await this.client.getRESTUser(process.env.OWNER.split(",")[0]);
|
const servers = await getServers();
|
||||||
const stats = await this.ipc.getStats();
|
await this.acknowledge();
|
||||||
return {
|
return {
|
||||||
embeds: [{
|
embeds: [{
|
||||||
color: 16711680,
|
color: 16711680,
|
||||||
|
@ -30,7 +31,7 @@ class InfoCommand extends Command {
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "💬 Total Servers:",
|
name: "💬 Total Servers:",
|
||||||
value: stats?.guilds ? stats.guilds : `${this.client.guilds.size} (for this cluster only)`
|
value: servers ? servers : `${this.client.guilds.size} (for this process only)`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
name: "✅ Official Server:",
|
name: "✅ Official Server:",
|
||||||
|
|
|
@ -1,28 +1,30 @@
|
||||||
import Command from "../../classes/command.js";
|
import Command from "../../classes/command.js";
|
||||||
|
import { load } from "../../utils/handler.js";
|
||||||
|
import { checkStatus } from "../../utils/soundplayer.js";
|
||||||
|
import { paths } from "../../utils/collections.js";
|
||||||
|
|
||||||
class ReloadCommand extends Command {
|
class ReloadCommand extends Command {
|
||||||
// quite possibly one of the hackiest commands in the bot
|
async run() {
|
||||||
run() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const owners = process.env.OWNER.split(",");
|
const owners = process.env.OWNER.split(",");
|
||||||
if (!owners.includes(this.author.id)) return resolve("Only the bot owner can reload commands!");
|
if (!owners.includes(this.author.id)) return "Only the bot owner can reload commands!";
|
||||||
const commandName = this.options.cmd ?? this.args.join(" ");
|
const commandName = this.options.cmd ?? this.args.join(" ");
|
||||||
if (!commandName || !commandName.trim()) return resolve("You need to provide a command to reload!");
|
if (!commandName || !commandName.trim()) return "You need to provide a command to reload!";
|
||||||
this.acknowledge().then(() => {
|
await this.acknowledge();
|
||||||
this.ipc.broadcast("reload", commandName);
|
const path = paths.get(commandName);
|
||||||
this.ipc.register("reloadSuccess", () => {
|
if (!path) return "I couldn't find that command!";
|
||||||
this.ipc.unregister("reloadSuccess");
|
const result = await load(this.client, path, await checkStatus(), true);
|
||||||
this.ipc.unregister("reloadFail");
|
if (result !== commandName) return "I couldn't reload that command!";
|
||||||
resolve(`The command \`${commandName}\` has been reloaded.`);
|
if (process.env.PM2_USAGE) {
|
||||||
});
|
process.send({
|
||||||
this.ipc.register("reloadFail", (message) => {
|
type: "process:msg",
|
||||||
this.ipc.unregister("reloadSuccess");
|
data: {
|
||||||
this.ipc.unregister("reloadFail");
|
type: "reload",
|
||||||
resolve(message.result);
|
message: commandName
|
||||||
});
|
}
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return `The command \`${commandName}\` has been reloaded.`;
|
||||||
|
}
|
||||||
|
|
||||||
static flags = [{
|
static flags = [{
|
||||||
name: "cmd",
|
name: "cmd",
|
||||||
|
|
|
@ -10,8 +10,7 @@ class RestartCommand extends Command {
|
||||||
await this.client.createMessage(this.channel.id, Object.assign({
|
await this.client.createMessage(this.channel.id, Object.assign({
|
||||||
content: "esmBot is restarting."
|
content: "esmBot is restarting."
|
||||||
}, this.reference));
|
}, this.reference));
|
||||||
this.ipc.restartAllClusters(true);
|
process.exit(1);
|
||||||
//this.ipc.broadcast("restart");
|
|
||||||
}
|
}
|
||||||
|
|
||||||
static description = "Restarts me";
|
static description = "Restarts me";
|
||||||
|
|
|
@ -1,30 +1,30 @@
|
||||||
import Command from "../../classes/command.js";
|
import Command from "../../classes/command.js";
|
||||||
|
import { checkStatus, reload } from "../../utils/soundplayer.js";
|
||||||
|
|
||||||
class SoundReloadCommand extends Command {
|
class SoundReloadCommand extends Command {
|
||||||
// another very hacky command
|
async run() {
|
||||||
run() {
|
|
||||||
return new Promise((resolve) => {
|
|
||||||
const owners = process.env.OWNER.split(",");
|
const owners = process.env.OWNER.split(",");
|
||||||
if (!owners.includes(this.author.id)) {
|
if (!owners.includes(this.author.id)) {
|
||||||
this.success = false;
|
this.success = false;
|
||||||
return "Only the bot owner can reload Lavalink!";
|
return "Only the bot owner can reload Lavalink!";
|
||||||
}
|
}
|
||||||
this.acknowledge().then(() => {
|
await this.acknowledge();
|
||||||
this.ipc.broadcast("soundreload");
|
const soundStatus = await checkStatus();
|
||||||
this.ipc.register("soundReloadSuccess", (msg) => {
|
if (!soundStatus) {
|
||||||
this.ipc.unregister("soundReloadSuccess");
|
const length = reload();
|
||||||
this.ipc.unregister("soundReloadFail");
|
if (process.env.PM2_USAGE) {
|
||||||
resolve(`Successfully connected to ${msg.length} Lavalink node(s).`);
|
process.send({
|
||||||
});
|
type: "process:msg",
|
||||||
this.ipc.register("soundReloadFail", () => {
|
data: {
|
||||||
this.ipc.unregister("soundReloadSuccess");
|
type: "soundreload"
|
||||||
this.ipc.unregister("soundReloadFail");
|
}
|
||||||
resolve("I couldn't connect to any Lavalink nodes!");
|
|
||||||
});
|
|
||||||
});
|
|
||||||
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
return `Successfully connected to ${length} Lavalink node(s).`;
|
||||||
|
} else {
|
||||||
|
return "I couldn't connect to any Lavalink nodes!";
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
static description = "Attempts to reconnect to all available Lavalink nodes";
|
static description = "Attempts to reconnect to all available Lavalink nodes";
|
||||||
static aliases = ["lava", "lavalink", "lavaconnect", "soundconnect"];
|
static aliases = ["lava", "lavalink", "lavaconnect", "soundconnect"];
|
||||||
|
|
|
@ -7,6 +7,8 @@ import Command from "../../classes/command.js";
|
||||||
import { VERSION } from "eris";
|
import { VERSION } from "eris";
|
||||||
import { exec as baseExec } from "child_process";
|
import { exec as baseExec } from "child_process";
|
||||||
import { promisify } from "util";
|
import { promisify } from "util";
|
||||||
|
import pm2 from "pm2";
|
||||||
|
import { getServers } from "../../utils/misc.js";
|
||||||
const exec = promisify(baseExec);
|
const exec = promisify(baseExec);
|
||||||
|
|
||||||
class StatsCommand extends Command {
|
class StatsCommand extends Command {
|
||||||
|
@ -14,7 +16,7 @@ class StatsCommand extends Command {
|
||||||
const uptime = process.uptime() * 1000;
|
const uptime = process.uptime() * 1000;
|
||||||
const connUptime = this.client.uptime;
|
const connUptime = this.client.uptime;
|
||||||
const owner = await this.client.getRESTUser(process.env.OWNER.split(",")[0]);
|
const owner = await this.client.getRESTUser(process.env.OWNER.split(",")[0]);
|
||||||
const stats = await this.ipc.getStats();
|
const servers = await getServers();
|
||||||
return {
|
return {
|
||||||
embeds: [{
|
embeds: [{
|
||||||
"author": {
|
"author": {
|
||||||
|
@ -28,13 +30,13 @@ class StatsCommand extends Command {
|
||||||
"value": `v${version}${process.env.NODE_ENV === "development" ? `-dev (${(await exec("git rev-parse HEAD", { cwd: dirname(fileURLToPath(import.meta.url)) })).stdout.substring(0, 7)})` : ""}`
|
"value": `v${version}${process.env.NODE_ENV === "development" ? `-dev (${(await exec("git rev-parse HEAD", { cwd: dirname(fileURLToPath(import.meta.url)) })).stdout.substring(0, 7)})` : ""}`
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Cluster Memory Usage",
|
"name": "Process Memory Usage",
|
||||||
"value": stats?.clusters[this.cluster] ? `${stats.clusters[this.cluster].ram.toFixed(2)} MB` : `${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)} MB`,
|
"value": `${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)} MB`,
|
||||||
"inline": true
|
"inline": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
"name": "Total Memory Usage",
|
"name": "Total Memory Usage",
|
||||||
"value": stats?.totalRam ? `${stats.totalRam.toFixed(2)} MB` : "Unknown",
|
"value": process.env.PM2_USAGE ? `${((await this.list()).reduce((prev, cur) => prev + cur.monit.memory, 0) / 1024 / 1024).toFixed(2)} MB` : "Unknown",
|
||||||
"inline": true
|
"inline": true
|
||||||
},
|
},
|
||||||
{
|
{
|
||||||
|
@ -65,14 +67,9 @@ class StatsCommand extends Command {
|
||||||
"value": this.channel.guild ? this.client.guildShardMap[this.channel.guild.id] : "N/A",
|
"value": this.channel.guild ? this.client.guildShardMap[this.channel.guild.id] : "N/A",
|
||||||
"inline": true
|
"inline": true
|
||||||
},
|
},
|
||||||
{
|
|
||||||
"name": "Cluster",
|
|
||||||
"value": this.cluster,
|
|
||||||
"inline": true
|
|
||||||
},
|
|
||||||
{
|
{
|
||||||
"name": "Servers",
|
"name": "Servers",
|
||||||
"value": stats?.guilds ? stats.guilds : `${this.client.guilds.size} (for this cluster only)`,
|
"value": servers ? servers : `${this.client.guilds.size} (for this process only)`,
|
||||||
"inline": true
|
"inline": true
|
||||||
}
|
}
|
||||||
]
|
]
|
||||||
|
@ -80,6 +77,15 @@ class StatsCommand extends Command {
|
||||||
};
|
};
|
||||||
}
|
}
|
||||||
|
|
||||||
|
list() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
pm2.list((err, list) => {
|
||||||
|
if (err) return reject(err);
|
||||||
|
resolve(list.filter((v) => v.name === "esmBot"));
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
static description = "Gets some statistics about me";
|
static description = "Gets some statistics about me";
|
||||||
static aliases = ["status", "stat"];
|
static aliases = ["status", "stat"];
|
||||||
}
|
}
|
||||||
|
|
|
@ -2,7 +2,7 @@ import Command from "../../classes/command.js";
|
||||||
|
|
||||||
class UserInfoCommand extends Command {
|
class UserInfoCommand extends Command {
|
||||||
async run() {
|
async run() {
|
||||||
const getUser = this.message.mentions.length >= 1 ? this.message.mentions[0] : (this.args.length !== 0 ? await this.ipc.fetchUser(this.args[0]) : this.author);
|
const getUser = this.message.mentions.length >= 1 ? this.message.mentions[0] : (this.args.length !== 0 ? this.client.users.get(this.args[0]) : this.author);
|
||||||
let user;
|
let user;
|
||||||
if (getUser) {
|
if (getUser) {
|
||||||
user = getUser;
|
user = getUser;
|
||||||
|
|
|
@ -12,7 +12,7 @@ class HostCommand extends MusicCommand {
|
||||||
if (input?.trim()) {
|
if (input?.trim()) {
|
||||||
let user;
|
let user;
|
||||||
if (this.type === "classic") {
|
if (this.type === "classic") {
|
||||||
const getUser = this.message.mentions.length >= 1 ? this.message.mentions[0] : (await this.ipc.fetchUser(input));
|
const getUser = this.message.mentions.length >= 1 ? this.message.mentions[0] : this.client.users.get(input);
|
||||||
if (getUser) {
|
if (getUser) {
|
||||||
user = getUser;
|
user = getUser;
|
||||||
} else if (input.match(/^<?[@#]?[&!]?\d+>?$/) && input >= 21154535154122752n) {
|
} else if (input.match(/^<?[@#]?[&!]?\d+>?$/) && input >= 21154535154122752n) {
|
||||||
|
|
|
@ -15,7 +15,7 @@ class MusicAIOCommand extends Command {
|
||||||
if (aliases.has(cmd)) cmd = aliases.get(cmd);
|
if (aliases.has(cmd)) cmd = aliases.get(cmd);
|
||||||
if (commands.has(cmd) && info.get(cmd).category === "music") {
|
if (commands.has(cmd) && info.get(cmd).category === "music") {
|
||||||
const command = commands.get(cmd);
|
const command = commands.get(cmd);
|
||||||
const inst = new command(this.client, this.cluster, this.worker, this.ipc, this.origOptions);
|
const inst = new command(this.client, this.origOptions);
|
||||||
const result = await inst.run();
|
const result = await inst.run();
|
||||||
this.success = inst.success;
|
this.success = inst.success;
|
||||||
return result;
|
return result;
|
||||||
|
|
|
@ -44,7 +44,7 @@ class TagsCommand extends Command {
|
||||||
if (!tagName || !tagName.trim()) return "You need to provide the name of the tag you want to check the owner of!";
|
if (!tagName || !tagName.trim()) return "You need to provide the name of the tag you want to check the owner of!";
|
||||||
const getResult = await database.getTag(this.channel.guild.id, tagName);
|
const getResult = await database.getTag(this.channel.guild.id, tagName);
|
||||||
if (!getResult) return "This tag doesn't exist!";
|
if (!getResult) return "This tag doesn't exist!";
|
||||||
const user = await this.ipc.fetchUser(getResult.author);
|
const user = this.client.users.get(getResult.author);
|
||||||
this.success = true;
|
this.success = true;
|
||||||
if (!user) {
|
if (!user) {
|
||||||
try {
|
try {
|
||||||
|
|
|
@ -22,9 +22,7 @@ These variables that are not necessarily required for the bot to run, but can gr
|
||||||
- `TMP_DOMAIN`: The root domain/directory that the images larger than 8MB are stored at. Example: `https://projectlounge.pw/tmp`
|
- `TMP_DOMAIN`: The root domain/directory that the images larger than 8MB are stored at. Example: `https://projectlounge.pw/tmp`
|
||||||
- `THRESHOLD`: A filesize threshold that the bot will start deleting old files in `TEMPDIR` at.
|
- `THRESHOLD`: A filesize threshold that the bot will start deleting old files in `TEMPDIR` at.
|
||||||
- `METRICS`: The HTTP port to serve [Prometheus](https://prometheus.io/)-compatible metrics on.
|
- `METRICS`: The HTTP port to serve [Prometheus](https://prometheus.io/)-compatible metrics on.
|
||||||
- `API_TYPE`: Set this to "none" if you want to process all images locally. Alternatively, set it to "ws" to use an image API server specified in the `image` block of `config/servers.json`, or "azure" to use the Azure Functions-based API.
|
- `API_TYPE`: Set this to "none" if you want to process all images locally. Alternatively, set it to "ws" to use an image API server specified in the `image` block of `config/servers.json`.
|
||||||
- `AZURE_URL`: Your Azure webhook URL. Only applies if `API` is set to "azure".
|
|
||||||
- `AZURE_PASS`: An optional password used for Azure requests. Only applies if `API` is set to "azure".
|
|
||||||
- `ADMIN_SERVER`: A Discord server/guild ID to limit owner-only commands such as eval to.
|
- `ADMIN_SERVER`: A Discord server/guild ID to limit owner-only commands such as eval to.
|
||||||
|
|
||||||
## JSON
|
## JSON
|
||||||
|
|
|
@ -45,9 +45,6 @@ The default command name is the same as the filename that you save it as, exclud
|
||||||
The parameters available to your command consist of the following:
|
The parameters available to your command consist of the following:
|
||||||
|
|
||||||
- `this.client`: An instance of an Eris [`Client`](https://abal.moe/Eris/docs/Client), useful for getting info or performing lower-level communication with the Discord API.
|
- `this.client`: An instance of an Eris [`Client`](https://abal.moe/Eris/docs/Client), useful for getting info or performing lower-level communication with the Discord API.
|
||||||
- `this.cluster`: The ID of the eris-fleet cluster that the command is being run from. This should be a number greater than or equal to 0.
|
|
||||||
- `this.worker`: The ID of the current eris-fleet worker. This should be a number greater than or equal to 0.
|
|
||||||
- `this.ipc`: An eris-fleet [`IPC`](https://danclay.github.io/eris-fleet/classes/IPC.html) instance, useful for communication between worker processes.
|
|
||||||
- `this.origOptions`: The raw options object provided to the command by the command handler.
|
- `this.origOptions`: The raw options object provided to the command by the command handler.
|
||||||
- `this.type`: The type of message that activated the command. Can be "classic" (a regular message) or "application" (slash/context menu commands).
|
- `this.type`: The type of message that activated the command. Can be "classic" (a regular message) or "application" (slash/context menu commands).
|
||||||
- `this.channel`: An Eris [`TextChannel`](https://abal.moe/Eris/docs/TextChannel) object of the channel that the command was run in, useful for getting info about a server and how to respond to a message.
|
- `this.channel`: An Eris [`TextChannel`](https://abal.moe/Eris/docs/TextChannel) object of the channel that the command was run in, useful for getting info about a server and how to respond to a message.
|
||||||
|
|
17
ecosystem.config.cjs
Normal file
17
ecosystem.config.cjs
Normal file
|
@ -0,0 +1,17 @@
|
||||||
|
module.exports = {
|
||||||
|
apps: [{
|
||||||
|
name: "esmBot-manager",
|
||||||
|
script: "utils/pm2/ext.js",
|
||||||
|
autorestart: true,
|
||||||
|
exp_backoff_restart_delay: 1000,
|
||||||
|
watch: false,
|
||||||
|
exec_mode: "fork"
|
||||||
|
}, {
|
||||||
|
name: "esmBot",
|
||||||
|
script: "app.js",
|
||||||
|
autorestart: true,
|
||||||
|
exp_backoff_restart_delay: 1000,
|
||||||
|
watch: false,
|
||||||
|
exec_mode: "cluster"
|
||||||
|
}]
|
||||||
|
};
|
|
@ -1,5 +1,5 @@
|
||||||
import { debug } from "../utils/logger.js";
|
import { debug } from "../utils/logger.js";
|
||||||
|
|
||||||
export default async (client, cluster, worker, ipc, message) => {
|
export default async (client, message) => {
|
||||||
debug(message);
|
debug(message);
|
||||||
};
|
};
|
5
events/error.js
Normal file
5
events/error.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { error } from "../utils/logger.js";
|
||||||
|
|
||||||
|
export default async (client, message) => {
|
||||||
|
error(message);
|
||||||
|
};
|
|
@ -2,7 +2,7 @@ import db from "../utils/database.js";
|
||||||
import { log } from "../utils/logger.js";
|
import { log } from "../utils/logger.js";
|
||||||
|
|
||||||
// run when the bot is added to a guild
|
// run when the bot is added to a guild
|
||||||
export default async (client, cluster, worker, ipc, guild) => {
|
export default async (client, guild) => {
|
||||||
log(`[GUILD JOIN] ${guild.name} (${guild.id}) added the bot.`);
|
log(`[GUILD JOIN] ${guild.name} (${guild.id}) added the bot.`);
|
||||||
const guildDB = await db.getGuild(guild.id);
|
const guildDB = await db.getGuild(guild.id);
|
||||||
if (!guildDB) await db.addGuild(guild);
|
if (!guildDB) await db.addGuild(guild);
|
||||||
|
|
|
@ -1,6 +1,6 @@
|
||||||
import { log } from "../utils/logger.js";
|
import { log } from "../utils/logger.js";
|
||||||
|
|
||||||
// run when the bot is removed from a guild
|
// run when the bot is removed from a guild
|
||||||
export default async (client, cluster, worker, ipc, guild) => {
|
export default async (client, guild) => {
|
||||||
log(`[GUILD LEAVE] ${guild.name} (${guild.id}) removed the bot.`);
|
log(`[GUILD LEAVE] ${guild.name} (${guild.id}) removed the bot.`);
|
||||||
};
|
};
|
||||||
|
|
|
@ -5,7 +5,7 @@ import { clean } from "../utils/misc.js";
|
||||||
import { upload } from "../utils/tempimages.js";
|
import { upload } from "../utils/tempimages.js";
|
||||||
|
|
||||||
// run when a slash command is executed
|
// run when a slash command is executed
|
||||||
export default async (client, cluster, worker, ipc, interaction) => {
|
export default async (client, interaction) => {
|
||||||
if (interaction?.type !== 2) return;
|
if (interaction?.type !== 2) return;
|
||||||
|
|
||||||
// check if command exists and if it's enabled
|
// check if command exists and if it's enabled
|
||||||
|
@ -23,7 +23,7 @@ export default async (client, cluster, worker, ipc, interaction) => {
|
||||||
try {
|
try {
|
||||||
await database.addCount(command);
|
await database.addCount(command);
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const commandClass = new cmd(client, cluster, worker, ipc, { type: "application", interaction });
|
const commandClass = new cmd(client, { type: "application", interaction });
|
||||||
const result = await commandClass.run();
|
const result = await commandClass.run();
|
||||||
const replyMethod = interaction.acknowledged ? "editOriginalMessage" : "createMessage";
|
const replyMethod = interaction.acknowledged ? "editOriginalMessage" : "createMessage";
|
||||||
if (typeof result === "string") {
|
if (typeof result === "string") {
|
||||||
|
@ -39,7 +39,7 @@ export default async (client, cluster, worker, ipc, interaction) => {
|
||||||
const fileSize = 8388119;
|
const fileSize = 8388119;
|
||||||
if (result.file.length > fileSize) {
|
if (result.file.length > fileSize) {
|
||||||
if (process.env.TEMPDIR && process.env.TEMPDIR !== "") {
|
if (process.env.TEMPDIR && process.env.TEMPDIR !== "") {
|
||||||
await upload(client, ipc, result, interaction, true);
|
await upload(client, result, interaction, true);
|
||||||
} else {
|
} else {
|
||||||
await interaction[replyMethod]({
|
await interaction[replyMethod]({
|
||||||
content: "The resulting image was more than 8MB in size, so I can't upload it.",
|
content: "The resulting image was more than 8MB in size, so I can't upload it.",
|
||||||
|
|
|
@ -6,7 +6,7 @@ import { clean } from "../utils/misc.js";
|
||||||
import { upload } from "../utils/tempimages.js";
|
import { upload } from "../utils/tempimages.js";
|
||||||
|
|
||||||
// run when someone sends a message
|
// run when someone sends a message
|
||||||
export default async (client, cluster, worker, ipc, message) => {
|
export default async (client, message) => {
|
||||||
// ignore other bots
|
// ignore other bots
|
||||||
if (message.author.bot) return;
|
if (message.author.bot) return;
|
||||||
|
|
||||||
|
@ -104,7 +104,7 @@ export default async (client, cluster, worker, ipc, message) => {
|
||||||
await database.addCount(aliases.get(command) ?? command);
|
await database.addCount(aliases.get(command) ?? command);
|
||||||
const startTime = new Date();
|
const startTime = new Date();
|
||||||
// eslint-disable-next-line no-unused-vars
|
// eslint-disable-next-line no-unused-vars
|
||||||
const commandClass = new cmd(client, cluster, worker, ipc, { type: "classic", message, args: parsed._, content: message.content.substring(prefix.length).trim().replace(command, "").trim(), specialArgs: (({ _, ...o }) => o)(parsed) }); // we also provide the message content as a parameter for cases where we need more accuracy
|
const commandClass = new cmd(client, { type: "classic", message, args: parsed._, content: message.content.substring(prefix.length).trim().replace(command, "").trim(), specialArgs: (({ _, ...o }) => o)(parsed) }); // we also provide the message content as a parameter for cases where we need more accuracy
|
||||||
const result = await commandClass.run();
|
const result = await commandClass.run();
|
||||||
const endTime = new Date();
|
const endTime = new Date();
|
||||||
if ((endTime - startTime) >= 180000) reference.allowedMentions.repliedUser = true;
|
if ((endTime - startTime) >= 180000) reference.allowedMentions.repliedUser = true;
|
||||||
|
@ -129,7 +129,7 @@ export default async (client, cluster, worker, ipc, message) => {
|
||||||
}
|
}
|
||||||
if (result.file.length > fileSize) {
|
if (result.file.length > fileSize) {
|
||||||
if (process.env.TEMPDIR && process.env.TEMPDIR !== "") {
|
if (process.env.TEMPDIR && process.env.TEMPDIR !== "") {
|
||||||
await upload(client, ipc, result, message);
|
await upload(client, result, message);
|
||||||
} else {
|
} else {
|
||||||
await client.createMessage(message.channel.id, "The resulting image was more than 8MB in size, so I can't upload it.");
|
await client.createMessage(message.channel.id, "The resulting image was more than 8MB in size, so I can't upload it.");
|
||||||
}
|
}
|
||||||
|
|
|
@ -4,7 +4,7 @@ import { random } from "../utils/misc.js";
|
||||||
|
|
||||||
const isWaiting = new Map();
|
const isWaiting = new Map();
|
||||||
|
|
||||||
export default async (client, cluster, worker, ipc, member, oldChannel) => {
|
export default async (client, member, oldChannel) => {
|
||||||
if (!oldChannel) return;
|
if (!oldChannel) return;
|
||||||
const connection = players.get(oldChannel.guild.id);
|
const connection = players.get(oldChannel.guild.id);
|
||||||
if (oldChannel.id === connection?.voiceChannel.id) {
|
if (oldChannel.id === connection?.voiceChannel.id) {
|
||||||
|
|
|
@ -1,5 +1,5 @@
|
||||||
import leaveHandler from "./voiceChannelLeave.js";
|
import leaveHandler from "./voiceChannelLeave.js";
|
||||||
|
|
||||||
export default async (client, cluster, worker, ipc, member, newChannel, oldChannel) => {
|
export default async (client, member, newChannel, oldChannel) => {
|
||||||
await leaveHandler(client, cluster, worker, ipc, member, oldChannel);
|
await leaveHandler(client, member, oldChannel);
|
||||||
};
|
};
|
5
events/warn.js
Normal file
5
events/warn.js
Normal file
|
@ -0,0 +1,5 @@
|
||||||
|
import { warn } from "../utils/logger.js";
|
||||||
|
|
||||||
|
export default async (client, message) => {
|
||||||
|
warn(message);
|
||||||
|
};
|
|
@ -32,7 +32,6 @@
|
||||||
"dotenv": "^16.0.2",
|
"dotenv": "^16.0.2",
|
||||||
"emoji-regex": "^10.1.0",
|
"emoji-regex": "^10.1.0",
|
||||||
"eris": "github:esmBot/eris#dev",
|
"eris": "github:esmBot/eris#dev",
|
||||||
"eris-fleet": "github:esmBot/eris-fleet#a19920f",
|
|
||||||
"file-type": "^17.1.6",
|
"file-type": "^17.1.6",
|
||||||
"format-duration": "^2.0.0",
|
"format-duration": "^2.0.0",
|
||||||
"jsqr": "^1.4.0",
|
"jsqr": "^1.4.0",
|
||||||
|
@ -57,6 +56,7 @@
|
||||||
"better-sqlite3": "^7.6.2",
|
"better-sqlite3": "^7.6.2",
|
||||||
"bufferutil": "^4.0.6",
|
"bufferutil": "^4.0.6",
|
||||||
"erlpack": "github:abalabahaha/erlpack",
|
"erlpack": "github:abalabahaha/erlpack",
|
||||||
|
"pm2": "^5.2.0",
|
||||||
"postgres": "^3.2.4",
|
"postgres": "^3.2.4",
|
||||||
"uuid": "^8.3.2",
|
"uuid": "^8.3.2",
|
||||||
"ws": "^8.8.1",
|
"ws": "^8.8.1",
|
||||||
|
|
1017
pnpm-lock.yaml
1017
pnpm-lock.yaml
File diff suppressed because it is too large
Load diff
178
shard.js
178
shard.js
|
@ -1,178 +0,0 @@
|
||||||
// shard base
|
|
||||||
import { BaseClusterWorker } from "eris-fleet";
|
|
||||||
// path stuff
|
|
||||||
import { readdir } from "fs/promises";
|
|
||||||
import { readFileSync } from "fs";
|
|
||||||
import { resolve, dirname } from "path";
|
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
// fancy loggings
|
|
||||||
import { log, error } from "./utils/logger.js";
|
|
||||||
// initialize command loader
|
|
||||||
import { load, send } from "./utils/handler.js";
|
|
||||||
// lavalink stuff
|
|
||||||
import { checkStatus, connect, reload, status, connected } from "./utils/soundplayer.js";
|
|
||||||
// database stuff
|
|
||||||
import database from "./utils/database.js";
|
|
||||||
// command collections
|
|
||||||
import { paths } from "./utils/collections.js";
|
|
||||||
// playing messages
|
|
||||||
const { messages } = JSON.parse(readFileSync(new URL("./config/messages.json", import.meta.url)));
|
|
||||||
// command config
|
|
||||||
const { types } = JSON.parse(readFileSync(new URL("./config/commands.json", import.meta.url)));
|
|
||||||
// other stuff
|
|
||||||
import { random } from "./utils/misc.js";
|
|
||||||
// generate help page
|
|
||||||
import { generateList, createPage } from "./utils/help.js";
|
|
||||||
// whether a broadcast is currently in effect
|
|
||||||
let broadcast = false;
|
|
||||||
|
|
||||||
class Shard extends BaseClusterWorker {
|
|
||||||
constructor(bot) {
|
|
||||||
super(bot);
|
|
||||||
|
|
||||||
console.info = (str) => this.ipc.sendToAdmiral("info", str);
|
|
||||||
this.playingSuffix = types.classic ? ` | @${this.bot.user.username} help` : "";
|
|
||||||
this.init();
|
|
||||||
}
|
|
||||||
|
|
||||||
async init() {
|
|
||||||
if (!types.classic && !types.application) {
|
|
||||||
error("Both classic and application commands are disabled! Please enable at least one command type in config/commands.json.");
|
|
||||||
this.ipc.totalShutdown(true);
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
// register commands and their info
|
|
||||||
const soundStatus = await checkStatus();
|
|
||||||
log("info", "Attempting to load commands...");
|
|
||||||
for await (const commandFile of this.getFiles(resolve(dirname(fileURLToPath(import.meta.url)), "./commands/"))) {
|
|
||||||
log("log", `Loading command from ${commandFile}...`);
|
|
||||||
try {
|
|
||||||
await load(this.bot, commandFile, soundStatus);
|
|
||||||
} catch (e) {
|
|
||||||
error(`Failed to register command from ${commandFile}: ${e}`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
if (types.application) {
|
|
||||||
try {
|
|
||||||
await send(this.bot);
|
|
||||||
} catch (e) {
|
|
||||||
log("error", e);
|
|
||||||
log("error", "Failed to send command data to Discord, slash/message commands may be unavailable.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
log("info", "Finished loading commands.");
|
|
||||||
|
|
||||||
await database.setup(this.ipc);
|
|
||||||
|
|
||||||
// register events
|
|
||||||
log("info", "Attempting to load events...");
|
|
||||||
for await (const file of this.getFiles(resolve(dirname(fileURLToPath(import.meta.url)), "./events/"))) {
|
|
||||||
log("log", `Loading event from ${file}...`);
|
|
||||||
const eventArray = file.split("/");
|
|
||||||
const eventName = eventArray[eventArray.length - 1].split(".")[0];
|
|
||||||
if (eventName === "interactionCreate" && !types.application) {
|
|
||||||
log("warn", `Skipped loading event from ${file} because application commands are disabled`);
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
const { default: event } = await import(file);
|
|
||||||
this.bot.on(eventName, event.bind(null, this.bot, this.clusterID, this.workerID, this.ipc));
|
|
||||||
}
|
|
||||||
log("info", "Finished loading events.");
|
|
||||||
|
|
||||||
// generate docs
|
|
||||||
if (process.env.OUTPUT && process.env.OUTPUT !== "") {
|
|
||||||
generateList();
|
|
||||||
if (this.clusterID === 0) {
|
|
||||||
await createPage(process.env.OUTPUT);
|
|
||||||
log("info", "The help docs have been generated.");
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
this.ipc.register("reload", async (message) => {
|
|
||||||
const path = paths.get(message);
|
|
||||||
if (!path) return this.ipc.broadcast("reloadFail", { result: "I couldn't find that command!" });
|
|
||||||
try {
|
|
||||||
const result = await load(this.bot, path, await checkStatus(), true);
|
|
||||||
if (result !== message) return this.ipc.broadcast("reloadFail", { result });
|
|
||||||
return this.ipc.broadcast("reloadSuccess");
|
|
||||||
} catch (result) {
|
|
||||||
return this.ipc.broadcast("reloadFail", { result });
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.ipc.register("soundreload", async () => {
|
|
||||||
const soundStatus = await checkStatus();
|
|
||||||
if (!soundStatus) {
|
|
||||||
const length = reload();
|
|
||||||
return this.ipc.broadcast("soundReloadSuccess", { length });
|
|
||||||
} else {
|
|
||||||
return this.ipc.broadcast("soundReloadFail");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
|
|
||||||
this.ipc.register("playbroadcast", (message) => {
|
|
||||||
this.bot.editStatus("dnd", {
|
|
||||||
name: message + this.playingSuffix,
|
|
||||||
});
|
|
||||||
broadcast = true;
|
|
||||||
return this.ipc.broadcast("broadcastSuccess");
|
|
||||||
});
|
|
||||||
|
|
||||||
this.ipc.register("broadcastend", () => {
|
|
||||||
this.bot.editStatus("dnd", {
|
|
||||||
name: random(messages) + this.playingSuffix,
|
|
||||||
});
|
|
||||||
broadcast = false;
|
|
||||||
return this.ipc.broadcast("broadcastEnd");
|
|
||||||
});
|
|
||||||
|
|
||||||
// connect to lavalink
|
|
||||||
if (!status && !connected) connect(this.bot);
|
|
||||||
|
|
||||||
const broadcastMessage = await this.ipc.centralStore.get("broadcast");
|
|
||||||
if (broadcastMessage) {
|
|
||||||
broadcast = true;
|
|
||||||
this.bot.editStatus("dnd", {
|
|
||||||
name: broadcastMessage + this.playingSuffix,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.activityChanger();
|
|
||||||
|
|
||||||
log("info", `Started worker ${this.workerID}.`);
|
|
||||||
}
|
|
||||||
|
|
||||||
// set activity (a.k.a. the gamer code)
|
|
||||||
activityChanger() {
|
|
||||||
if (!broadcast) {
|
|
||||||
this.bot.editStatus("dnd", {
|
|
||||||
name: random(messages) + this.playingSuffix,
|
|
||||||
});
|
|
||||||
}
|
|
||||||
setTimeout(this.activityChanger.bind(this), 900000);
|
|
||||||
}
|
|
||||||
|
|
||||||
async* getFiles(dir) {
|
|
||||||
const dirents = await readdir(dir, { withFileTypes: true });
|
|
||||||
for (const dirent of dirents) {
|
|
||||||
const name = dir + (dir.charAt(dir.length - 1) !== "/" ? "/" : "") + dirent.name;
|
|
||||||
if (dirent.isDirectory()) {
|
|
||||||
yield* this.getFiles(name);
|
|
||||||
} else if (dirent.name.endsWith(".js")) {
|
|
||||||
yield name;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shutdown(done) {
|
|
||||||
log("warn", "Shutting down...");
|
|
||||||
this.bot.editStatus("dnd", {
|
|
||||||
name: "Restarting/shutting down..."
|
|
||||||
});
|
|
||||||
database.stop();
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
|
|
||||||
}
|
|
||||||
|
|
||||||
export default Shard;
|
|
|
@ -37,11 +37,7 @@ This page was last generated on \`${new Date().toString()}\`.
|
||||||
|
|
||||||
\`[]\` means an argument is required, \`{}\` means an argument is optional.
|
\`[]\` means an argument is required, \`{}\` means an argument is optional.
|
||||||
|
|
||||||
Default prefix is \`&\`.
|
|
||||||
|
|
||||||
**Want to help support esmBot's development? Consider donating on Patreon!** https://patreon.com/TheEssem
|
**Want to help support esmBot's development? Consider donating on Patreon!** https://patreon.com/TheEssem
|
||||||
|
|
||||||
> Tip: You can get much more info about a command by using \`help [command]\` in the bot itself.
|
|
||||||
`;
|
`;
|
||||||
|
|
||||||
template += "\n## Table of Contents\n";
|
template += "\n## Table of Contents\n";
|
||||||
|
|
113
utils/image.js
113
utils/image.js
|
@ -1,14 +1,22 @@
|
||||||
import { request } from "undici";
|
import { request } from "undici";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
import path from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { Worker } from "worker_threads";
|
||||||
|
import { createRequire } from "module";
|
||||||
import { fileTypeFromBuffer, fileTypeFromFile } from "file-type";
|
import { fileTypeFromBuffer, fileTypeFromFile } from "file-type";
|
||||||
|
import * as logger from "./logger.js";
|
||||||
|
import ImageConnection from "./imageConnection.js";
|
||||||
|
|
||||||
|
// only requiring this to work around an issue regarding worker threads
|
||||||
|
const nodeRequire = createRequire(import.meta.url);
|
||||||
|
if (!process.env.API_TYPE || process.env.API_TYPE === "none") {
|
||||||
|
nodeRequire(`../build/${process.env.DEBUG && process.env.DEBUG === "true" ? "Debug" : "Release"}/image.node`);
|
||||||
|
}
|
||||||
|
|
||||||
const formats = ["image/jpeg", "image/png", "image/webp", "image/gif", "video/mp4", "video/webm", "video/quicktime"];
|
const formats = ["image/jpeg", "image/png", "image/webp", "image/gif", "video/mp4", "video/webm", "video/quicktime"];
|
||||||
|
|
||||||
export const jobs = {};
|
|
||||||
|
|
||||||
export const connections = new Map();
|
export const connections = new Map();
|
||||||
|
export let servers = process.env.API_TYPE === "ws" ? JSON.parse(fs.readFileSync(new URL("../config/servers.json", import.meta.url), { encoding: "utf8" })).image : null;
|
||||||
export const servers = JSON.parse(fs.readFileSync(new URL("../config/servers.json", import.meta.url), { encoding: "utf8" })).image;
|
|
||||||
|
|
||||||
export async function getType(image, extraReturnTypes) {
|
export async function getType(image, extraReturnTypes) {
|
||||||
if (!image.startsWith("http")) {
|
if (!image.startsWith("http")) {
|
||||||
|
@ -65,3 +73,98 @@ export async function getType(image, extraReturnTypes) {
|
||||||
}
|
}
|
||||||
return type;
|
return type;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
function connect(server, auth) {
|
||||||
|
const connection = new ImageConnection(server, auth);
|
||||||
|
connections.set(server, connection);
|
||||||
|
}
|
||||||
|
|
||||||
|
function disconnect() {
|
||||||
|
for (const connection of connections.values()) {
|
||||||
|
connection.close();
|
||||||
|
}
|
||||||
|
connections.clear();
|
||||||
|
}
|
||||||
|
|
||||||
|
async function repopulate() {
|
||||||
|
const data = await fs.promises.readFile(new URL("../config/servers.json", import.meta.url), { encoding: "utf8" });
|
||||||
|
servers = JSON.parse(data).image;
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function reloadImageConnections() {
|
||||||
|
disconnect();
|
||||||
|
await repopulate();
|
||||||
|
let amount = 0;
|
||||||
|
for (const server of servers) {
|
||||||
|
try {
|
||||||
|
connect(server.server, server.auth);
|
||||||
|
amount += 1;
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
return amount;
|
||||||
|
}
|
||||||
|
|
||||||
|
function chooseServer(ideal) {
|
||||||
|
if (ideal.length === 0) throw "No available servers";
|
||||||
|
const sorted = ideal.sort((a, b) => {
|
||||||
|
return a.load - b.load;
|
||||||
|
});
|
||||||
|
return sorted[0];
|
||||||
|
}
|
||||||
|
|
||||||
|
async function getIdeal(object) {
|
||||||
|
const idealServers = [];
|
||||||
|
for (const [address, connection] of connections) {
|
||||||
|
if (connection.conn.readyState !== 0 && connection.conn.readyState !== 1) {
|
||||||
|
continue;
|
||||||
|
}
|
||||||
|
if (object.params.type && !connection.formats[object.cmd]?.includes(object.params.type)) continue;
|
||||||
|
idealServers.push({
|
||||||
|
addr: address,
|
||||||
|
load: await connection.getCount()
|
||||||
|
});
|
||||||
|
}
|
||||||
|
const server = chooseServer(idealServers);
|
||||||
|
return connections.get(server.addr);
|
||||||
|
}
|
||||||
|
|
||||||
|
function waitForWorker(worker) {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
worker.once("message", (data) => {
|
||||||
|
resolve({
|
||||||
|
buffer: Buffer.from([...data.buffer]),
|
||||||
|
type: data.fileExtension
|
||||||
|
});
|
||||||
|
});
|
||||||
|
worker.once("error", reject);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
export async function runImageJob(params) {
|
||||||
|
if (process.env.API_TYPE === "ws") {
|
||||||
|
for (let i = 0; i < 3; i++) {
|
||||||
|
const currentServer = await getIdeal(params);
|
||||||
|
try {
|
||||||
|
await currentServer.queue(BigInt(params.id), params);
|
||||||
|
await currentServer.wait(BigInt(params.id));
|
||||||
|
const output = await currentServer.getOutput(params.id);
|
||||||
|
return output;
|
||||||
|
} catch (e) {
|
||||||
|
if (i < 2 && e === "Request ended prematurely due to a closed connection") {
|
||||||
|
continue;
|
||||||
|
} else {
|
||||||
|
if (e === "No available servers" && i >= 2) throw "Request ended prematurely due to a closed connection";
|
||||||
|
throw e;
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
} else {
|
||||||
|
// Called from command (not using image API)
|
||||||
|
const worker = new Worker(path.join(path.dirname(fileURLToPath(import.meta.url)), "./image-runner.js"), {
|
||||||
|
workerData: params
|
||||||
|
});
|
||||||
|
return await waitForWorker(worker);
|
||||||
|
}
|
||||||
|
}
|
|
@ -22,8 +22,6 @@ class ImageConnection {
|
||||||
this.auth = auth;
|
this.auth = auth;
|
||||||
this.tag = 0;
|
this.tag = 0;
|
||||||
this.disconnected = false;
|
this.disconnected = false;
|
||||||
this.njobs = 0;
|
|
||||||
this.max = 0;
|
|
||||||
this.formats = {};
|
this.formats = {};
|
||||||
this.wsproto = null;
|
this.wsproto = null;
|
||||||
if (tls) {
|
if (tls) {
|
||||||
|
@ -43,17 +41,15 @@ class ImageConnection {
|
||||||
} else {
|
} else {
|
||||||
httpproto = "http";
|
httpproto = "http";
|
||||||
}
|
}
|
||||||
this.httpurl = `${httpproto}://${host}/image`;
|
this.httpurl = `${httpproto}://${host}`;
|
||||||
this.conn.on("message", (msg) => this.onMessage(msg));
|
this.conn.on("message", (msg) => this.onMessage(msg));
|
||||||
this.conn.once("error", (err) => this.onError(err));
|
this.conn.once("error", (err) => this.onError(err));
|
||||||
this.conn.once("close", () => this.onClose());
|
this.conn.once("close", () => this.onClose());
|
||||||
}
|
}
|
||||||
|
|
||||||
onMessage(msg) {
|
async onMessage(msg) {
|
||||||
const op = msg.readUint8(0);
|
const op = msg.readUint8(0);
|
||||||
if (op === Rinit) {
|
if (op === Rinit) {
|
||||||
this.max = msg.readUint16LE(3);
|
|
||||||
this.njobs = msg.readUint16LE(5);
|
|
||||||
this.formats = JSON.parse(msg.toString("utf8", 7));
|
this.formats = JSON.parse(msg.toString("utf8", 7));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -64,10 +60,7 @@ class ImageConnection {
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
this.requests.delete(tag);
|
this.requests.delete(tag);
|
||||||
if (op === Rqueue) this.njobs++;
|
|
||||||
if (op === Rcancel || op === Rwait) this.njobs--;
|
|
||||||
if (op === Rerror) {
|
if (op === Rerror) {
|
||||||
this.njobs--;
|
|
||||||
promise.reject(new Error(msg.slice(3, msg.length).toString()));
|
promise.reject(new Error(msg.slice(3, msg.length).toString()));
|
||||||
return;
|
return;
|
||||||
}
|
}
|
||||||
|
@ -82,9 +75,7 @@ class ImageConnection {
|
||||||
for (const [tag, obj] of this.requests.entries()) {
|
for (const [tag, obj] of this.requests.entries()) {
|
||||||
obj.reject("Request ended prematurely due to a closed connection");
|
obj.reject("Request ended prematurely due to a closed connection");
|
||||||
this.requests.delete(tag);
|
this.requests.delete(tag);
|
||||||
if (obj.op === Twait || obj.op === Tcancel) this.njobs--;
|
|
||||||
}
|
}
|
||||||
//this.requests.clear();
|
|
||||||
if (!this.disconnected) {
|
if (!this.disconnected) {
|
||||||
logger.warn(`Lost connection to ${this.host}, attempting to reconnect in 5 seconds...`);
|
logger.warn(`Lost connection to ${this.host}, attempting to reconnect in 5 seconds...`);
|
||||||
await setTimeout(5000);
|
await setTimeout(5000);
|
||||||
|
@ -107,25 +98,25 @@ class ImageConnection {
|
||||||
|
|
||||||
queue(jobid, jobobj) {
|
queue(jobid, jobobj) {
|
||||||
const str = JSON.stringify(jobobj);
|
const str = JSON.stringify(jobobj);
|
||||||
const buf = Buffer.alloc(4);
|
const buf = Buffer.alloc(8);
|
||||||
buf.writeUint32LE(jobid);
|
buf.writeBigUint64LE(jobid);
|
||||||
return this.do(Tqueue, jobid, Buffer.concat([buf, Buffer.from(str)]));
|
return this.do(Tqueue, jobid, Buffer.concat([buf, Buffer.from(str)]));
|
||||||
}
|
}
|
||||||
|
|
||||||
wait(jobid) {
|
wait(jobid) {
|
||||||
const buf = Buffer.alloc(4);
|
const buf = Buffer.alloc(8);
|
||||||
buf.writeUint32LE(jobid);
|
buf.writeBigUint64LE(jobid);
|
||||||
return this.do(Twait, jobid, buf);
|
return this.do(Twait, jobid, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
cancel(jobid) {
|
cancel(jobid) {
|
||||||
const buf = Buffer.alloc(4);
|
const buf = Buffer.alloc(8);
|
||||||
buf.writeUint32LE(jobid);
|
buf.writeBigUint64LE(jobid);
|
||||||
return this.do(Tcancel, jobid, buf);
|
return this.do(Tcancel, jobid, buf);
|
||||||
}
|
}
|
||||||
|
|
||||||
async getOutput(jobid) {
|
async getOutput(jobid) {
|
||||||
const req = await request(`${this.httpurl}?id=${jobid}`, {
|
const req = await request(`${this.httpurl}/image?id=${jobid}`, {
|
||||||
headers: {
|
headers: {
|
||||||
authentication: this.auth || undefined
|
authentication: this.auth || undefined
|
||||||
}
|
}
|
||||||
|
@ -149,7 +140,18 @@ class ImageConnection {
|
||||||
type = contentType;
|
type = contentType;
|
||||||
break;
|
break;
|
||||||
}
|
}
|
||||||
return { buffer: Buffer.from(await req.body.arrayBuffer()), type };
|
return { arrayBuffer: await req.body.arrayBuffer(), type };
|
||||||
|
}
|
||||||
|
|
||||||
|
async getCount() {
|
||||||
|
const req = await request(`${this.httpurl}/count`, {
|
||||||
|
headers: {
|
||||||
|
authentication: this.auth || undefined
|
||||||
|
}
|
||||||
|
});
|
||||||
|
if (req.statusCode !== 200) return;
|
||||||
|
const res = parseInt(await req.body.text());
|
||||||
|
return res;
|
||||||
}
|
}
|
||||||
|
|
||||||
async do(op, id, data) {
|
async do(op, id, data) {
|
||||||
|
|
|
@ -1,4 +1,41 @@
|
||||||
export function log(type, content) { return content ? console[type](content) : console.info(type); }
|
import winston from "winston";
|
||||||
|
import "winston-daily-rotate-file";
|
||||||
|
|
||||||
|
export const logger = winston.createLogger({
|
||||||
|
levels: {
|
||||||
|
error: 0,
|
||||||
|
warn: 1,
|
||||||
|
info: 2,
|
||||||
|
main: 3,
|
||||||
|
debug: 4
|
||||||
|
},
|
||||||
|
transports: [
|
||||||
|
new winston.transports.Console({ format: winston.format.colorize({ all: true }), stderrLevels: ["error", "warn"] }),
|
||||||
|
new winston.transports.DailyRotateFile({ filename: "logs/error-%DATE%.log", level: "error", zippedArchive: true, maxSize: 4194304, maxFiles: 8 }),
|
||||||
|
new winston.transports.DailyRotateFile({ filename: "logs/main-%DATE%.log", zippedArchive: true, maxSize: 4194304, maxFiles: 8 })
|
||||||
|
],
|
||||||
|
level: process.env.DEBUG_LOG ? "debug" : "main",
|
||||||
|
format: winston.format.combine(
|
||||||
|
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||||
|
winston.format.printf((info) => {
|
||||||
|
const {
|
||||||
|
timestamp, level, message, ...args
|
||||||
|
} = info;
|
||||||
|
|
||||||
|
return `[${timestamp}]: [${level.toUpperCase()}] - ${message} ${Object.keys(args).length ? JSON.stringify(args, null, 2) : ""}`;
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
winston.addColors({
|
||||||
|
info: "green",
|
||||||
|
main: "gray",
|
||||||
|
debug: "magenta",
|
||||||
|
warn: "yellow",
|
||||||
|
error: "red"
|
||||||
|
});
|
||||||
|
|
||||||
|
export function log(type, content) { return content ? logger.log(type === "log" ? "main" : type, content) : logger.info(type); }
|
||||||
|
|
||||||
export function error(...args) { return log("error", ...args); }
|
export function error(...args) { return log("error", ...args); }
|
||||||
|
|
||||||
|
|
|
@ -1,7 +1,14 @@
|
||||||
import util from "util";
|
import util from "util";
|
||||||
import fs from "fs";
|
import fs from "fs";
|
||||||
|
import pm2 from "pm2";
|
||||||
import { config } from "dotenv";
|
import { config } from "dotenv";
|
||||||
|
|
||||||
|
// playing messages
|
||||||
|
const { messages } = JSON.parse(fs.readFileSync(new URL("../config/messages.json", import.meta.url)));
|
||||||
|
const { types } = JSON.parse(fs.readFileSync(new URL("../config/commands.json", import.meta.url)));
|
||||||
|
|
||||||
|
let broadcast = false;
|
||||||
|
|
||||||
// random(array) to select a random entry in array
|
// random(array) to select a random entry in array
|
||||||
export function random(array) {
|
export function random(array) {
|
||||||
if (!array || array.length < 1) return null;
|
if (!array || array.length < 1) return null;
|
||||||
|
@ -42,3 +49,61 @@ export function clean(text) {
|
||||||
export function textEncode(string) {
|
export function textEncode(string) {
|
||||||
return string.replaceAll("&", "&").replaceAll(">", ">").replaceAll("<", "<").replaceAll("\"", """).replaceAll("'", "'").replaceAll("\\n", "\n").replaceAll("\\:", ":");
|
return string.replaceAll("&", "&").replaceAll(">", ">").replaceAll("<", "<").replaceAll("\"", """).replaceAll("'", "'").replaceAll("\\n", "\n").replaceAll("\\:", ":");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
// set activity (a.k.a. the gamer code)
|
||||||
|
export function activityChanger(bot) {
|
||||||
|
if (!broadcast) {
|
||||||
|
bot.editStatus("dnd", {
|
||||||
|
name: random(messages) + (types.classic ? ` | @${bot.user.username} help` : ""),
|
||||||
|
});
|
||||||
|
}
|
||||||
|
setTimeout(() => activityChanger(bot), 900000);
|
||||||
|
}
|
||||||
|
|
||||||
|
export function checkBroadcast(bot) {
|
||||||
|
/*if () {
|
||||||
|
startBroadcast(bot, message);
|
||||||
|
}*/
|
||||||
|
}
|
||||||
|
|
||||||
|
export function startBroadcast(bot, message) {
|
||||||
|
bot.editStatus("dnd", {
|
||||||
|
name: message + (types.classic ? ` | @${bot.user.username} help` : ""),
|
||||||
|
});
|
||||||
|
broadcast = true;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function endBroadcast(bot) {
|
||||||
|
bot.editStatus("dnd", {
|
||||||
|
name: random(messages) + (types.classic ? ` | @${bot.user.username} help` : ""),
|
||||||
|
});
|
||||||
|
broadcast = false;
|
||||||
|
}
|
||||||
|
|
||||||
|
export function getServers() {
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
if (process.env.PM2_USAGE) {
|
||||||
|
pm2.launchBus((err, pm2Bus) => {
|
||||||
|
const listener = (packet) => {
|
||||||
|
if (packet.data?.type === "countResponse") {
|
||||||
|
resolve(packet.data.serverCount);
|
||||||
|
pm2Bus.off("process:msg");
|
||||||
|
}
|
||||||
|
};
|
||||||
|
pm2Bus.on("process:msg", listener);
|
||||||
|
});
|
||||||
|
pm2.sendDataToProcessId(0, {
|
||||||
|
id: 0,
|
||||||
|
type: "process:msg",
|
||||||
|
data: {
|
||||||
|
type: "getCount"
|
||||||
|
},
|
||||||
|
topic: true
|
||||||
|
}, (err) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
});
|
||||||
|
} else {
|
||||||
|
resolve(0);
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
166
utils/pm2/ext.js
Normal file
166
utils/pm2/ext.js
Normal file
|
@ -0,0 +1,166 @@
|
||||||
|
import pm2 from "pm2";
|
||||||
|
import { Api } from "@top-gg/sdk";
|
||||||
|
import winston from "winston";
|
||||||
|
|
||||||
|
// load config from .env file
|
||||||
|
import { resolve, dirname } from "path";
|
||||||
|
import { fileURLToPath } from "url";
|
||||||
|
import { readFileSync } from "fs";
|
||||||
|
import { createServer } from "http";
|
||||||
|
import { config } from "dotenv";
|
||||||
|
config({ path: resolve(dirname(fileURLToPath(import.meta.url)), "../../.env") });
|
||||||
|
|
||||||
|
import database from "../database.js";
|
||||||
|
|
||||||
|
const dbl = process.env.NODE_ENV === "production" && process.env.DBL ? new Api(process.env.DBL) : null;
|
||||||
|
|
||||||
|
const logger = winston.createLogger({
|
||||||
|
levels: {
|
||||||
|
error: 0,
|
||||||
|
warn: 1,
|
||||||
|
info: 2,
|
||||||
|
main: 3,
|
||||||
|
debug: 4
|
||||||
|
},
|
||||||
|
transports: [
|
||||||
|
new winston.transports.Console({ format: winston.format.colorize({ all: true }), stderrLevels: ["error", "warn"] })
|
||||||
|
],
|
||||||
|
level: process.env.DEBUG_LOG ? "debug" : "main",
|
||||||
|
format: winston.format.combine(
|
||||||
|
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||||
|
winston.format.printf((info) => {
|
||||||
|
const {
|
||||||
|
timestamp, level, message, ...args
|
||||||
|
} = info;
|
||||||
|
|
||||||
|
return `[${timestamp}]: [${level.toUpperCase()}] - ${message} ${Object.keys(args).length ? JSON.stringify(args, null, 2) : ""}`;
|
||||||
|
}),
|
||||||
|
)
|
||||||
|
});
|
||||||
|
|
||||||
|
winston.addColors({
|
||||||
|
info: "green",
|
||||||
|
main: "gray",
|
||||||
|
debug: "magenta",
|
||||||
|
warn: "yellow",
|
||||||
|
error: "red"
|
||||||
|
});
|
||||||
|
|
||||||
|
let serverCount = 0;
|
||||||
|
let shardCount = 0;
|
||||||
|
let clusterCount = 0;
|
||||||
|
let responseCount = 0;
|
||||||
|
|
||||||
|
let timeout;
|
||||||
|
|
||||||
|
process.on("message", (packet) => {
|
||||||
|
if (packet.data?.type === "getCount") {
|
||||||
|
process.send({
|
||||||
|
type: "process:msg",
|
||||||
|
data: {
|
||||||
|
type: "countResponse",
|
||||||
|
serverCount
|
||||||
|
}
|
||||||
|
});
|
||||||
|
}
|
||||||
|
});
|
||||||
|
|
||||||
|
function updateStats() {
|
||||||
|
serverCount = 0;
|
||||||
|
shardCount = 0;
|
||||||
|
clusterCount = 0;
|
||||||
|
responseCount = 0;
|
||||||
|
return new Promise((resolve, reject) => {
|
||||||
|
pm2.list((err, list) => {
|
||||||
|
if (err) reject(err);
|
||||||
|
const clusters = list.filter((v) => v.name === "esmBot");
|
||||||
|
clusterCount = clusters.length;
|
||||||
|
const listener = (packet) => {
|
||||||
|
if (packet.data?.type === "serverCounts") {
|
||||||
|
clearTimeout(timeout);
|
||||||
|
serverCount += packet.data.guilds;
|
||||||
|
shardCount += packet.data.shards;
|
||||||
|
responseCount += 1;
|
||||||
|
if (responseCount >= clusterCount) {
|
||||||
|
resolve();
|
||||||
|
process.removeListener("message", listener);
|
||||||
|
} else {
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
reject();
|
||||||
|
process.removeListener("message", listener);
|
||||||
|
}, 5000);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
};
|
||||||
|
timeout = setTimeout(() => {
|
||||||
|
reject();
|
||||||
|
process.removeListener("message", listener);
|
||||||
|
}, 5000);
|
||||||
|
process.on("message", listener);
|
||||||
|
process.send({
|
||||||
|
type: "process:msg",
|
||||||
|
data: {
|
||||||
|
type: "serverCounts"
|
||||||
|
}
|
||||||
|
});
|
||||||
|
});
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
async function dblPost() {
|
||||||
|
logger.main("Posting stats to Top.gg...");
|
||||||
|
try {
|
||||||
|
//await updateStats();
|
||||||
|
await dbl.postStats({
|
||||||
|
serverCount,
|
||||||
|
shardCount
|
||||||
|
});
|
||||||
|
logger.main("Stats posted.");
|
||||||
|
} catch (e) {
|
||||||
|
logger.error(e);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
|
if (process.env.METRICS && process.env.METRICS !== "") {
|
||||||
|
const servers = [];
|
||||||
|
if (process.env.API_TYPE === "ws") {
|
||||||
|
const imageHosts = JSON.parse(readFileSync(new URL("../../config/servers.json", import.meta.url), { encoding: "utf8" })).image;
|
||||||
|
for (let { server } of imageHosts) {
|
||||||
|
if (!server.includes(":")) {
|
||||||
|
server += ":3762";
|
||||||
|
}
|
||||||
|
servers.push(server);
|
||||||
|
}
|
||||||
|
}
|
||||||
|
const httpServer = createServer(async (req, res) => {
|
||||||
|
if (req.method !== "GET") {
|
||||||
|
res.statusCode = 405;
|
||||||
|
return res.end("GET only");
|
||||||
|
}
|
||||||
|
res.write(`# HELP esmbot_command_count Number of times a command has been run
|
||||||
|
# TYPE esmbot_command_count counter
|
||||||
|
# HELP esmbot_server_count Number of servers/guilds the bot is in
|
||||||
|
# TYPE esmbot_server_count gauge
|
||||||
|
# HELP esmbot_shard_count Number of shards the bot has
|
||||||
|
# TYPE esmbot_shard_count gauge
|
||||||
|
`);
|
||||||
|
const counts = await database.getCounts();
|
||||||
|
for (const [i, w] of Object.entries(counts)) {
|
||||||
|
res.write(`esmbot_command_count{command="${i}"} ${w}\n`);
|
||||||
|
}
|
||||||
|
|
||||||
|
res.write(`esmbot_server_count ${serverCount}\n`);
|
||||||
|
res.write(`esmbot_shard_count ${shardCount}\n`);
|
||||||
|
res.end();
|
||||||
|
});
|
||||||
|
httpServer.listen(process.env.METRICS, () => {
|
||||||
|
logger.log("info", `Serving metrics at ${process.env.METRICS}`);
|
||||||
|
});
|
||||||
|
}
|
||||||
|
|
||||||
|
setInterval(updateStats, 300000);
|
||||||
|
if (dbl) setInterval(dblPost, 1800000);
|
||||||
|
|
||||||
|
setTimeout(updateStats, 10000);
|
||||||
|
|
||||||
|
logger.info("Started esmBot management process.");
|
|
@ -1,268 +0,0 @@
|
||||||
import { BaseServiceWorker } from "eris-fleet";
|
|
||||||
import * as logger from "../logger.js";
|
|
||||||
import fs from "fs";
|
|
||||||
import path from "path";
|
|
||||||
import { fileURLToPath } from "url";
|
|
||||||
import { Worker } from "worker_threads";
|
|
||||||
import { createRequire } from "module";
|
|
||||||
import { createServer } from "http";
|
|
||||||
import { request } from "undici";
|
|
||||||
import EventEmitter from "events";
|
|
||||||
|
|
||||||
// only requiring this to work around an issue regarding worker threads
|
|
||||||
const nodeRequire = createRequire(import.meta.url);
|
|
||||||
if (!process.env.API_TYPE || process.env.API_TYPE === "none") {
|
|
||||||
nodeRequire(`../../build/${process.env.DEBUG && process.env.DEBUG === "true" ? "Debug" : "Release"}/image.node`);
|
|
||||||
}
|
|
||||||
|
|
||||||
import ImageConnection from "../imageConnection.js";
|
|
||||||
|
|
||||||
class ImageWorker extends BaseServiceWorker {
|
|
||||||
constructor(setup) {
|
|
||||||
super(setup);
|
|
||||||
|
|
||||||
console.info = (str) => this.ipc.sendToAdmiral("info", str);
|
|
||||||
|
|
||||||
if (process.env.API_TYPE === "ws") {
|
|
||||||
this.connections = new Map();
|
|
||||||
this.servers = JSON.parse(fs.readFileSync(new URL("../../config/servers.json", import.meta.url), { encoding: "utf8" })).image;
|
|
||||||
this.nextID = 0;
|
|
||||||
} else if (process.env.API_TYPE === "azure") {
|
|
||||||
this.jobs = new Map();
|
|
||||||
this.webhook = createServer();
|
|
||||||
this.port = parseInt(process.env.WEBHOOK_PORT) || 3763;
|
|
||||||
}
|
|
||||||
|
|
||||||
this.begin().then(() => this.serviceReady());
|
|
||||||
}
|
|
||||||
|
|
||||||
async begin() {
|
|
||||||
// connect to image api if enabled
|
|
||||||
if (process.env.API_TYPE === "ws") {
|
|
||||||
for (const server of this.servers) {
|
|
||||||
try {
|
|
||||||
await this.connect(server.server, server.auth);
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (process.env.API_TYPE === "azure") {
|
|
||||||
this.webhook.on("request", async (req, res) => {
|
|
||||||
if (req.method !== "POST") {
|
|
||||||
res.statusCode = 405;
|
|
||||||
return res.end("405 Method Not Allowed");
|
|
||||||
}
|
|
||||||
if (process.env.AZURE_PASS && req.headers.authorization !== process.env.AZURE_PASS) {
|
|
||||||
res.statusCode = 401;
|
|
||||||
return res.end("401 Unauthorized");
|
|
||||||
}
|
|
||||||
const reqUrl = new URL(req.url, `http://${req.headers.host}`);
|
|
||||||
if (reqUrl.pathname === "/callback") {
|
|
||||||
try {
|
|
||||||
const chunks = [];
|
|
||||||
req.on("data", (data) => {
|
|
||||||
chunks.push(data);
|
|
||||||
});
|
|
||||||
req.once("end", () => {
|
|
||||||
if (this.jobs.has(req.headers["x-azure-id"])) {
|
|
||||||
try {
|
|
||||||
const error = JSON.parse(Buffer.concat(chunks).toString());
|
|
||||||
if (error.error) this.jobs.get(req.headers["x-azure-id"]).emit("error", new Error(error.message));
|
|
||||||
} catch {
|
|
||||||
// no-op
|
|
||||||
}
|
|
||||||
const contentType = req.headers["content-type"];
|
|
||||||
let type;
|
|
||||||
switch (contentType) {
|
|
||||||
case "image/gif":
|
|
||||||
type = "gif";
|
|
||||||
break;
|
|
||||||
case "image/png":
|
|
||||||
type = "png";
|
|
||||||
break;
|
|
||||||
case "image/jpeg":
|
|
||||||
type = "jpg";
|
|
||||||
break;
|
|
||||||
case "image/webp":
|
|
||||||
type = "webp";
|
|
||||||
break;
|
|
||||||
default:
|
|
||||||
type = contentType;
|
|
||||||
break;
|
|
||||||
}
|
|
||||||
this.jobs.get(req.headers["x-azure-id"]).emit("image", { buffer: Buffer.concat(chunks), type });
|
|
||||||
return res.end("OK");
|
|
||||||
} else {
|
|
||||||
res.statusCode = 409;
|
|
||||||
return res.end("409 Conflict");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
} catch (e) {
|
|
||||||
logger.error("An error occurred while processing a webhook request: ", e);
|
|
||||||
res.statusCode = 500;
|
|
||||||
return res.end("500 Internal Server Error");
|
|
||||||
}
|
|
||||||
} else {
|
|
||||||
res.statusCode = 404;
|
|
||||||
return res.end("404 Not Found");
|
|
||||||
}
|
|
||||||
});
|
|
||||||
this.webhook.on("error", (e) => {
|
|
||||||
logger.error("An error occurred on the Azure webhook: ", e);
|
|
||||||
});
|
|
||||||
this.webhook.listen(this.port, () => {
|
|
||||||
logger.log(`Azure HTTP webhook listening on port ${this.port}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async repopulate() {
|
|
||||||
const data = await fs.promises.readFile(new URL("../../config/servers.json", import.meta.url), { encoding: "utf8" });
|
|
||||||
this.servers = JSON.parse(data).image;
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
async getRunning() {
|
|
||||||
const statuses = [];
|
|
||||||
if (process.env.API_TYPE === "ws") {
|
|
||||||
for (const [address, connection] of this.connections) {
|
|
||||||
if (connection.conn.readyState !== 0 && connection.conn.readyState !== 1) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
statuses.push({
|
|
||||||
address,
|
|
||||||
runningJobs: connection.njobs,
|
|
||||||
max: connection.max
|
|
||||||
});
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return statuses;
|
|
||||||
}
|
|
||||||
|
|
||||||
async chooseServer(ideal) {
|
|
||||||
if (ideal.length === 0) throw "No available servers";
|
|
||||||
const sorted = ideal.sort((a, b) => {
|
|
||||||
return a.load - b.load;
|
|
||||||
}).filter((e, i, array) => {
|
|
||||||
return !(e.load < array[0].load);
|
|
||||||
});
|
|
||||||
return sorted[0];
|
|
||||||
}
|
|
||||||
|
|
||||||
async getIdeal(object) {
|
|
||||||
const idealServers = [];
|
|
||||||
for (const [address, connection] of this.connections) {
|
|
||||||
if (connection.conn.readyState !== 0 && connection.conn.readyState !== 1) {
|
|
||||||
continue;
|
|
||||||
}
|
|
||||||
if (object.params.type && !connection.formats[object.cmd]?.includes(object.params.type)) continue;
|
|
||||||
idealServers.push({
|
|
||||||
addr: address,
|
|
||||||
load: connection.njobs / connection.max
|
|
||||||
});
|
|
||||||
}
|
|
||||||
const server = await this.chooseServer(idealServers);
|
|
||||||
return this.connections.get(server.addr);
|
|
||||||
}
|
|
||||||
|
|
||||||
async connect(server, auth) {
|
|
||||||
const connection = new ImageConnection(server, auth);
|
|
||||||
this.connections.set(server, connection);
|
|
||||||
}
|
|
||||||
|
|
||||||
async disconnect() {
|
|
||||||
for (const connection of this.connections.values()) {
|
|
||||||
connection.close();
|
|
||||||
}
|
|
||||||
this.connections.clear();
|
|
||||||
return;
|
|
||||||
}
|
|
||||||
|
|
||||||
waitForWorker(worker) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
worker.once("message", (data) => {
|
|
||||||
resolve({
|
|
||||||
buffer: Buffer.from([...data.buffer]),
|
|
||||||
type: data.fileExtension
|
|
||||||
});
|
|
||||||
});
|
|
||||||
worker.once("error", reject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
waitForAzure(event) {
|
|
||||||
return new Promise((resolve, reject) => {
|
|
||||||
event.once("image", (data) => {
|
|
||||||
resolve(data);
|
|
||||||
});
|
|
||||||
event.once("error", reject);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
async run(object) {
|
|
||||||
if (process.env.API_TYPE === "ws") {
|
|
||||||
let num = this.nextID++;
|
|
||||||
if (num > 4294967295) num = this.nextID = 0;
|
|
||||||
for (let i = 0; i < 3; i++) {
|
|
||||||
const currentServer = await this.getIdeal(object);
|
|
||||||
try {
|
|
||||||
await currentServer.queue(num, object);
|
|
||||||
await currentServer.wait(num);
|
|
||||||
const output = await currentServer.getOutput(num);
|
|
||||||
return output;
|
|
||||||
} catch (e) {
|
|
||||||
if (i < 2 && e === "Request ended prematurely due to a closed connection") {
|
|
||||||
continue;
|
|
||||||
} else {
|
|
||||||
if (e === "No available servers" && i >= 2) throw "Request ended prematurely due to a closed connection";
|
|
||||||
throw e;
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
} else if (process.env.API_TYPE === "azure") {
|
|
||||||
object.callback = `${process.env.AZURE_CALLBACK_URL}:${this.port}/callback`;
|
|
||||||
const response = await request(`${process.env.AZURE_URL}/api/orchestrators/ImageOrchestrator`, { method: "POST", body: JSON.stringify(object) }).then(r => r.body.json());
|
|
||||||
const event = new EventEmitter();
|
|
||||||
this.jobs.set(response.id, event);
|
|
||||||
return await this.waitForAzure(event);
|
|
||||||
} else {
|
|
||||||
// Called from command (not using image API)
|
|
||||||
const worker = new Worker(path.join(path.dirname(fileURLToPath(import.meta.url)), "../image-runner.js"), {
|
|
||||||
workerData: object
|
|
||||||
});
|
|
||||||
return await this.waitForWorker(worker);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
async handleCommand(data) {
|
|
||||||
try {
|
|
||||||
if (data.type === "run") {
|
|
||||||
const result = await this.run(data.obj);
|
|
||||||
return result;
|
|
||||||
} else if (data.type === "reload") {
|
|
||||||
await this.disconnect();
|
|
||||||
await this.repopulate();
|
|
||||||
let amount = 0;
|
|
||||||
for (const server of this.servers) {
|
|
||||||
try {
|
|
||||||
await this.connect(server.server, server.auth);
|
|
||||||
amount += 1;
|
|
||||||
} catch (e) {
|
|
||||||
logger.error(e);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
return amount;
|
|
||||||
} else if (data.type === "stats") {
|
|
||||||
return await this.getRunning();
|
|
||||||
}
|
|
||||||
} catch (err) {
|
|
||||||
return { err: typeof err === "string" ? err : err.message };
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
shutdown(done) {
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default ImageWorker;
|
|
|
@ -1,66 +0,0 @@
|
||||||
import { BaseServiceWorker } from "eris-fleet";
|
|
||||||
import { createServer } from "http";
|
|
||||||
import { log } from "../logger.js";
|
|
||||||
import database from "../database.js";
|
|
||||||
|
|
||||||
class PrometheusWorker extends BaseServiceWorker {
|
|
||||||
constructor(setup) {
|
|
||||||
super(setup);
|
|
||||||
|
|
||||||
console.info = (str) => this.ipc.sendToAdmiral("info", str);
|
|
||||||
|
|
||||||
if (process.env.METRICS && process.env.METRICS !== "") {
|
|
||||||
this.httpServer = createServer(async (req, res) => {
|
|
||||||
if (req.method !== "GET") {
|
|
||||||
res.statusCode = 405;
|
|
||||||
return res.end("GET only");
|
|
||||||
}
|
|
||||||
res.write(`# HELP esmbot_command_count Number of times a command has been run
|
|
||||||
# TYPE esmbot_command_count counter
|
|
||||||
# HELP esmbot_server_count Number of servers/guilds the bot is in
|
|
||||||
# TYPE esmbot_server_count gauge
|
|
||||||
# HELP esmbot_shard_count Number of shards the bot has
|
|
||||||
# TYPE esmbot_shard_count gauge
|
|
||||||
`);
|
|
||||||
if (process.env.API_TYPE === "ws") {
|
|
||||||
const servers = await this.ipc.serviceCommand("image", { type: "stats" }, true);
|
|
||||||
res.write(`# HELP esmbot_connected_workers Number of workers connected
|
|
||||||
# TYPE esmbot_connected_workers gauge
|
|
||||||
esmbot_connected_workers ${servers.length}
|
|
||||||
# HELP esmbot_running_jobs Number of running jobs on this worker
|
|
||||||
# TYPE esmbot_running_jobs gauge
|
|
||||||
# HELP esmbot_queued_jobs Number of queued jobs on this worker
|
|
||||||
# TYPE esmbot_queued_jobs gauge
|
|
||||||
# HELP esmbot_max_jobs Number of max allowed jobs on this worker
|
|
||||||
# TYPE esmbot_max_jobs gauge
|
|
||||||
`);
|
|
||||||
for (const [i, w] of servers.entries()) {
|
|
||||||
res.write(`esmbot_running_jobs{worker="${i}"} ${w.runningJobs}\n`);
|
|
||||||
res.write(`esmbot_max_jobs{worker="${i}"} ${w.max}\n`);
|
|
||||||
}
|
|
||||||
}
|
|
||||||
const counts = await database.getCounts();
|
|
||||||
for (const [i, w] of Object.entries(counts)) {
|
|
||||||
res.write(`esmbot_command_count{command="${i}"} ${w}\n`);
|
|
||||||
}
|
|
||||||
|
|
||||||
const stats = await this.ipc.getStats();
|
|
||||||
res.write(`esmbot_server_count ${stats.guilds}\n`);
|
|
||||||
res.write(`esmbot_shard_count ${stats.shardCount}\n`);
|
|
||||||
res.end();
|
|
||||||
});
|
|
||||||
this.httpServer.listen(process.env.METRICS, () => {
|
|
||||||
log("info", `Serving metrics at ${process.env.METRICS}`);
|
|
||||||
});
|
|
||||||
}
|
|
||||||
|
|
||||||
this.serviceReady();
|
|
||||||
}
|
|
||||||
|
|
||||||
shutdown(done) {
|
|
||||||
if (this.httpServer) this.httpServer.close();
|
|
||||||
done();
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
export default PrometheusWorker;
|
|
|
@ -1,7 +1,9 @@
|
||||||
import * as logger from "../utils/logger.js";
|
import * as logger from "../utils/logger.js";
|
||||||
import { readdir, lstat, rm, writeFile } from "fs/promises";
|
import { readdir, lstat, rm, writeFile, stat } from "fs/promises";
|
||||||
|
|
||||||
export async function upload(client, ipc, result, context, interaction = false) {
|
let dirSizeCache;
|
||||||
|
|
||||||
|
export async function upload(client, result, context, interaction = false) {
|
||||||
const filename = `${Math.random().toString(36).substring(2, 15)}.${result.name.split(".")[1]}`;
|
const filename = `${Math.random().toString(36).substring(2, 15)}.${result.name.split(".")[1]}`;
|
||||||
await writeFile(`${process.env.TEMPDIR}/${filename}`, result.file);
|
await writeFile(`${process.env.TEMPDIR}/${filename}`, result.file);
|
||||||
const imageURL = `${process.env.TMP_DOMAIN || "https://tmp.projectlounge.pw"}/${filename}`;
|
const imageURL = `${process.env.TMP_DOMAIN || "https://tmp.projectlounge.pw"}/${filename}`;
|
||||||
|
@ -34,13 +36,13 @@ export async function upload(client, ipc, result, context, interaction = false)
|
||||||
}));
|
}));
|
||||||
}
|
}
|
||||||
if (process.env.THRESHOLD) {
|
if (process.env.THRESHOLD) {
|
||||||
const size = await ipc.centralStore.get("dirSizeCache") + result.file.length;
|
const size = dirSizeCache + result.file.length;
|
||||||
await ipc.centralStore.set("dirSizeCache", size);
|
dirSizeCache = size;
|
||||||
await removeOldImages(ipc, size);
|
await removeOldImages(size);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
export async function removeOldImages(ipc, size) {
|
async function removeOldImages(size) {
|
||||||
if (size > process.env.THRESHOLD) {
|
if (size > process.env.THRESHOLD) {
|
||||||
const files = (await readdir(process.env.TEMPDIR)).map((file) => {
|
const files = (await readdir(process.env.TEMPDIR)).map((file) => {
|
||||||
return lstat(`${process.env.TEMPDIR}/${file}`).then((stats) => {
|
return lstat(`${process.env.TEMPDIR}/${file}`).then((stats) => {
|
||||||
|
@ -67,6 +69,30 @@ export async function removeOldImages(ipc, size) {
|
||||||
const newSize = oldestFiles.reduce((a, b) => {
|
const newSize = oldestFiles.reduce((a, b) => {
|
||||||
return a + b.size;
|
return a + b.size;
|
||||||
}, 0);
|
}, 0);
|
||||||
await ipc.centralStore.set("dirSizeCache", newSize);
|
dirSizeCache = newSize;
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
export async function parseThreshold() {
|
||||||
|
const matched = process.env.THRESHOLD.match(/(\d+)([KMGT])/);
|
||||||
|
const sizes = {
|
||||||
|
K: 1024,
|
||||||
|
M: 1048576,
|
||||||
|
G: 1073741824,
|
||||||
|
T: 1099511627776
|
||||||
|
};
|
||||||
|
if (matched && matched[1] && matched[2]) {
|
||||||
|
process.env.THRESHOLD = matched[1] * sizes[matched[2]];
|
||||||
|
} else {
|
||||||
|
logger.error("Invalid THRESHOLD config.");
|
||||||
|
process.env.THRESHOLD = undefined;
|
||||||
|
}
|
||||||
|
const dirstat = (await readdir(process.env.TEMPDIR)).map((file) => {
|
||||||
|
return stat(`${process.env.TEMPDIR}/${file}`).then((stats) => stats.size);
|
||||||
|
});
|
||||||
|
const size = await Promise.all(dirstat);
|
||||||
|
const reduced = size.reduce((a, b) => {
|
||||||
|
return a + b;
|
||||||
|
}, 0);
|
||||||
|
dirSizeCache = reduced;
|
||||||
|
}
|
Loading…
Reference in a new issue