Replace eris-fleet with a pm2-based cluster system, overhaul image handling, removed azure image api
This commit is contained in:
		
							parent
							
								
									5a3364736d
								
							
						
					
					
						commit
						db0decf71a
					
				
					 45 changed files with 1777 additions and 857 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: "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
									
										
									
										generated
									
									
									
								
							
							
						
						
									
										1017
									
								
								pnpm-lock.yaml
									
										
									
										generated
									
									
									
								
							
										
											
												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);
 | 
				
			||||||
 | 
					    }
 | 
				
			||||||
 | 
					  });
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
							
								
								
									
										125
									
								
								utils/pm2/ext.js
									
										
									
									
									
										Normal file
									
								
							
							
						
						
									
										125
									
								
								utils/pm2/ext.js
									
										
									
									
									
										Normal file
									
								
							| 
						 | 
					@ -0,0 +1,125 @@
 | 
				
			||||||
 | 
					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 { config } from "dotenv";
 | 
				
			||||||
 | 
					config({ path: resolve(dirname(fileURLToPath(import.meta.url)), "../../.env") });
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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() {
 | 
				
			||||||
 | 
					  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...");
 | 
				
			||||||
 | 
					  serverCount = 0;
 | 
				
			||||||
 | 
					  shardCount = 0;
 | 
				
			||||||
 | 
					  clusterCount = 0;
 | 
				
			||||||
 | 
					  responseCount = 0;
 | 
				
			||||||
 | 
					  try {
 | 
				
			||||||
 | 
					    //await updateStats();
 | 
				
			||||||
 | 
					    await dbl.postStats({
 | 
				
			||||||
 | 
					      serverCount,
 | 
				
			||||||
 | 
					      shardCount
 | 
				
			||||||
 | 
					    });
 | 
				
			||||||
 | 
					    logger.main("Stats posted.");
 | 
				
			||||||
 | 
					  } catch (e) {
 | 
				
			||||||
 | 
					    logger.error(e);
 | 
				
			||||||
 | 
					  }
 | 
				
			||||||
 | 
					}
 | 
				
			||||||
 | 
					
 | 
				
			||||||
 | 
					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,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…
	
	Add table
		Add a link
		
	
		Reference in a new issue