initial update
This commit is contained in:
parent
3272429cf6
commit
db9b70bf66
280 changed files with 11772 additions and 11966 deletions
|
@ -1,49 +1,49 @@
|
|||
// this is a method to wait for someone to rejoin a voice channel
|
||||
import { EventEmitter } from "events";
|
||||
import { random } from "./misc.js";
|
||||
|
||||
class AwaitRejoin extends EventEmitter {
|
||||
constructor(channel, anyone, memberID) {
|
||||
super();
|
||||
this.member = memberID;
|
||||
this.anyone = anyone;
|
||||
this.channel = channel;
|
||||
this.rejoined = false;
|
||||
this.ended = false;
|
||||
this.bot = channel.client;
|
||||
this.listener = (member, newChannel) => this.verify(member, newChannel);
|
||||
this.bot.on("voiceChannelJoin", this.listener);
|
||||
this.bot.on("voiceChannelSwitch", this.listener);
|
||||
this.stopTimeout = setTimeout(() => this.stop(), 10000);
|
||||
this.checkInterval = setInterval(() => this.verify({ id: memberID }, channel, true), 1000);
|
||||
}
|
||||
|
||||
verify(member, channel, checked) {
|
||||
if (this.channel.id === channel.id) {
|
||||
if ((this.member === member.id && this.channel.voiceMembers.has(member.id)) || (this.anyone && !checked)) {
|
||||
clearTimeout(this.stopTimeout);
|
||||
this.rejoined = true;
|
||||
this.stop(member);
|
||||
return true;
|
||||
} else if (this.anyone && (!checked || this.channel.voiceMembers.size > 1)) {
|
||||
clearTimeout(this.stopTimeout);
|
||||
this.rejoined = true;
|
||||
this.stop(random(this.channel.voiceMembers.filter((i) => i.id !== this.bot.user.id && !i.bot)));
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
stop(member) {
|
||||
if (this.ended) return;
|
||||
this.ended = true;
|
||||
clearInterval(this.checkInterval);
|
||||
this.bot.removeListener("voiceChannelJoin", this.listener);
|
||||
this.bot.removeListener("voiceChannelSwitch", this.listener);
|
||||
this.emit("end", this.rejoined, member);
|
||||
}
|
||||
}
|
||||
|
||||
// this is a method to wait for someone to rejoin a voice channel
|
||||
import { EventEmitter } from "events";
|
||||
import { random } from "./misc.js";
|
||||
|
||||
class AwaitRejoin extends EventEmitter {
|
||||
constructor(channel, anyone, memberID) {
|
||||
super();
|
||||
this.member = memberID;
|
||||
this.anyone = anyone;
|
||||
this.channel = channel;
|
||||
this.rejoined = false;
|
||||
this.ended = false;
|
||||
this.bot = channel.client;
|
||||
this.listener = (member, newChannel) => this.verify(member, newChannel);
|
||||
this.bot.on("voiceChannelJoin", this.listener);
|
||||
this.bot.on("voiceChannelSwitch", this.listener);
|
||||
this.stopTimeout = setTimeout(() => this.stop(), 10000);
|
||||
this.checkInterval = setInterval(() => this.verify({ id: memberID }, channel, true), 1000);
|
||||
}
|
||||
|
||||
verify(member, channel, checked) {
|
||||
if (this.channel.id === channel.id) {
|
||||
if ((this.member === member.id && this.channel.voiceMembers.has(member.id)) || (this.anyone && !checked)) {
|
||||
clearTimeout(this.stopTimeout);
|
||||
this.rejoined = true;
|
||||
this.stop(member);
|
||||
return true;
|
||||
} else if (this.anyone && (!checked || this.channel.voiceMembers.size > 1)) {
|
||||
clearTimeout(this.stopTimeout);
|
||||
this.rejoined = true;
|
||||
this.stop(random(this.channel.voiceMembers.filter((i) => i.id !== this.bot.user.id && !i.bot)));
|
||||
return true;
|
||||
}
|
||||
} else {
|
||||
return false;
|
||||
}
|
||||
}
|
||||
|
||||
stop(member) {
|
||||
if (this.ended) return;
|
||||
this.ended = true;
|
||||
clearInterval(this.checkInterval);
|
||||
this.bot.removeListener("voiceChannelJoin", this.listener);
|
||||
this.bot.removeListener("voiceChannelSwitch", this.listener);
|
||||
this.emit("end", this.rejoined, member);
|
||||
}
|
||||
}
|
||||
|
||||
export default AwaitRejoin;
|
|
@ -1,39 +1,39 @@
|
|||
export const commands = new Map();
|
||||
export const messageCommands = new Map();
|
||||
export const paths = new Map();
|
||||
export const aliases = new Map();
|
||||
export const info = new Map();
|
||||
export const sounds = new Map();
|
||||
export const categories = new Map();
|
||||
|
||||
class TimedMap extends Map {
|
||||
constructor(time, values) {
|
||||
super(values);
|
||||
this.time = time;
|
||||
}
|
||||
set(key, value) {
|
||||
super.set(key, value);
|
||||
setTimeout(() => {
|
||||
if (super.has(key)) super.delete(key);
|
||||
}, this.time);
|
||||
}
|
||||
}
|
||||
|
||||
export const runningCommands = new TimedMap(5000);
|
||||
export const selectedImages = new TimedMap(180000);
|
||||
|
||||
class Cache extends Map {
|
||||
constructor(values) {
|
||||
super(values);
|
||||
this.maxValues = 2048;
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
super.set(key, value);
|
||||
if (this.size > this.maxValues) this.delete(this.keys().next().value);
|
||||
}
|
||||
}
|
||||
|
||||
export const prefixCache = new Cache();
|
||||
export const disabledCache = new Cache();
|
||||
export const commands = new Map();
|
||||
export const messageCommands = new Map();
|
||||
export const paths = new Map();
|
||||
export const aliases = new Map();
|
||||
export const info = new Map();
|
||||
export const sounds = new Map();
|
||||
export const categories = new Map();
|
||||
|
||||
class TimedMap extends Map {
|
||||
constructor(time, values) {
|
||||
super(values);
|
||||
this.time = time;
|
||||
}
|
||||
set(key, value) {
|
||||
super.set(key, value);
|
||||
setTimeout(() => {
|
||||
if (super.has(key)) super.delete(key);
|
||||
}, this.time);
|
||||
}
|
||||
}
|
||||
|
||||
export const runningCommands = new TimedMap(5000);
|
||||
export const selectedImages = new TimedMap(180000);
|
||||
|
||||
class Cache extends Map {
|
||||
constructor(values) {
|
||||
super(values);
|
||||
this.maxValues = 2048;
|
||||
}
|
||||
|
||||
set(key, value) {
|
||||
super.set(key, value);
|
||||
if (this.size > this.maxValues) this.delete(this.keys().next().value);
|
||||
}
|
||||
}
|
||||
|
||||
export const prefixCache = new Cache();
|
||||
export const disabledCache = new Cache();
|
||||
export const disabledCmdCache = new Cache();
|
|
@ -1,33 +1,33 @@
|
|||
import { config } from "dotenv";
|
||||
config();
|
||||
import { Pool } from "pg";
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DB
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const guilds = (await pool.query("SELECT * FROM guilds")).rows;
|
||||
console.log("Migrating tags...");
|
||||
try {
|
||||
await pool.query("CREATE TABLE tags ( guild_id VARCHAR(30) NOT NULL, name text NOT NULL, content text NOT NULL, author VARCHAR(30) NOT NULL, UNIQUE(guild_id, name) )");
|
||||
} catch (e) {
|
||||
console.error(`Skipping table creation due to error: ${e}`);
|
||||
}
|
||||
for (const guild of guilds) {
|
||||
for (const [name, value] of Object.entries(guild.tags)) {
|
||||
if ((await pool.query("SELECT * FROM tags WHERE guild_id = $1 AND name = $2", [guild.guild_id, name])).rows.length !== 0) {
|
||||
await pool.query("UPDATE tags SET content = $1, author = $2 WHERE guild_id = $3 AND name = $4", [value.content, value.author, guild.guild_id, name]);
|
||||
} else {
|
||||
await pool.query("INSERT INTO tags (guild_id, name, content, author) VALUES ($1, $2, $3, $4)", [guild.guild_id, name, value.content, value.author]);
|
||||
}
|
||||
console.log(`Migrated tag ${name} in guild ${guild.guild_id}`);
|
||||
}
|
||||
}
|
||||
console.log("Migrating disabled commands...");
|
||||
for (const guild of guilds) {
|
||||
await pool.query("UPDATE guilds SET disabled_commands = $1 WHERE guild_id = $2", [guild.tags_disabled ? ["tags"] : [], guild.guild_id]);
|
||||
console.log(`Migrated disabled commands in guild ${guild.guild_id}`);
|
||||
}
|
||||
console.log("Done!");
|
||||
return process.exit(0);
|
||||
import { config } from "dotenv";
|
||||
config();
|
||||
import { Pool } from "pg";
|
||||
const pool = new Pool({
|
||||
connectionString: process.env.DB
|
||||
});
|
||||
|
||||
(async () => {
|
||||
const guilds = (await pool.query("SELECT * FROM guilds")).rows;
|
||||
console.log("Migrating tags...");
|
||||
try {
|
||||
await pool.query("CREATE TABLE tags ( guild_id VARCHAR(30) NOT NULL, name text NOT NULL, content text NOT NULL, author VARCHAR(30) NOT NULL, UNIQUE(guild_id, name) )");
|
||||
} catch (e) {
|
||||
console.error(`Skipping table creation due to error: ${e}`);
|
||||
}
|
||||
for (const guild of guilds) {
|
||||
for (const [name, value] of Object.entries(guild.tags)) {
|
||||
if ((await pool.query("SELECT * FROM tags WHERE guild_id = $1 AND name = $2", [guild.guild_id, name])).rows.length !== 0) {
|
||||
await pool.query("UPDATE tags SET content = $1, author = $2 WHERE guild_id = $3 AND name = $4", [value.content, value.author, guild.guild_id, name]);
|
||||
} else {
|
||||
await pool.query("INSERT INTO tags (guild_id, name, content, author) VALUES ($1, $2, $3, $4)", [guild.guild_id, name, value.content, value.author]);
|
||||
}
|
||||
console.log(`Migrated tag ${name} in guild ${guild.guild_id}`);
|
||||
}
|
||||
}
|
||||
console.log("Migrating disabled commands...");
|
||||
for (const guild of guilds) {
|
||||
await pool.query("UPDATE guilds SET disabled_commands = $1 WHERE guild_id = $2", [guild.tags_disabled ? ["tags"] : [], guild.guild_id]);
|
||||
console.log(`Migrated disabled commands in guild ${guild.guild_id}`);
|
||||
}
|
||||
console.log("Done!");
|
||||
return process.exit(0);
|
||||
})();
|
|
@ -1,20 +1,20 @@
|
|||
// wrapper for the database drivers in ./database/
|
||||
import { config } from "dotenv";
|
||||
config();
|
||||
|
||||
let db = null;
|
||||
|
||||
if (process.env.DB) {
|
||||
const dbtype = process.env.DB.split("://")[0];
|
||||
try {
|
||||
db = await import(`./database/${dbtype}.js`);
|
||||
} catch (error) {
|
||||
if (error.code == "ERR_MODULE_NOT_FOUND") {
|
||||
console.error(`DB config option has unknown database type '${dbtype}'`);
|
||||
process.exit(1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export default db;
|
||||
// wrapper for the database drivers in ./database/
|
||||
import { config } from "dotenv";
|
||||
config();
|
||||
|
||||
let db = null;
|
||||
|
||||
if (process.env.DB) {
|
||||
const dbtype = process.env.DB.split("://")[0];
|
||||
try {
|
||||
db = await import(`./database/${dbtype}.js`);
|
||||
} catch (error) {
|
||||
if (error.code == "ERR_MODULE_NOT_FOUND") {
|
||||
console.error(`DB config option has unknown database type '${dbtype}'`);
|
||||
process.exit(1);
|
||||
}
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
|
||||
export default db;
|
||||
|
|
|
@ -1,187 +1,187 @@
|
|||
import { prefixCache, disabledCmdCache, disabledCache, commands, messageCommands } from "../collections.js";
|
||||
|
||||
import Postgres from "postgres";
|
||||
const sql = Postgres(process.env.DB, {
|
||||
onnotice: () => {}
|
||||
});
|
||||
|
||||
const settingsSchema = `
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id smallint PRIMARY KEY,
|
||||
version integer NOT NULL, CHECK(id = 1)
|
||||
);
|
||||
`;
|
||||
|
||||
const schema = `
|
||||
ALTER TABLE settings ADD COLUMN broadcast text;
|
||||
CREATE TABLE guilds (
|
||||
guild_id VARCHAR(30) NOT NULL PRIMARY KEY,
|
||||
prefix VARCHAR(15) NOT NULL,
|
||||
disabled text ARRAY NOT NULL,
|
||||
disabled_commands text ARRAY NOT NULL
|
||||
);
|
||||
CREATE TABLE counts (
|
||||
command VARCHAR NOT NULL PRIMARY KEY,
|
||||
count integer NOT NULL
|
||||
);
|
||||
CREATE TABLE tags (
|
||||
guild_id VARCHAR(30) NOT NULL,
|
||||
name text NOT NULL,
|
||||
content text NOT NULL,
|
||||
author VARCHAR(30) NOT NULL,
|
||||
UNIQUE(guild_id, name)
|
||||
);
|
||||
`;
|
||||
|
||||
const updates = [
|
||||
"", // reserved
|
||||
"CREATE TABLE IF NOT EXISTS settings ( id smallint PRIMARY KEY, version integer NOT NULL, CHECK(id = 1) );\nALTER TABLE guilds ADD COLUMN accessed timestamp;",
|
||||
"ALTER TABLE guilds DROP COLUMN accessed",
|
||||
"ALTER TABLE settings ADD COLUMN IF NOT EXISTS broadcast text"
|
||||
];
|
||||
|
||||
export async function setup() {
|
||||
const existingCommands = (await sql`SELECT command FROM counts`).map(x => x.command);
|
||||
const commandNames = [...commands.keys(), ...messageCommands.keys()];
|
||||
for (const command of existingCommands) {
|
||||
if (!commandNames.includes(command)) {
|
||||
await sql`DELETE FROM counts WHERE command = ${command}`;
|
||||
}
|
||||
}
|
||||
for (const command of commandNames) {
|
||||
if (!existingCommands.includes(command)) {
|
||||
await sql`INSERT INTO counts ${sql({ command, count: 0 }, "command", "count")}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function upgrade(logger) {
|
||||
try {
|
||||
await sql.begin(async (sql) => {
|
||||
await sql.unsafe(settingsSchema);
|
||||
let version;
|
||||
const settingsrow = (await sql`SELECT version FROM settings WHERE id = 1`);
|
||||
if (settingsrow.length == 0) {
|
||||
version = 0;
|
||||
} else {
|
||||
version = settingsrow[0].version;
|
||||
}
|
||||
const latestVersion = updates.length - 1;
|
||||
if (version === 0) {
|
||||
logger.info("Initializing PostgreSQL database...");
|
||||
await sql.unsafe(schema);
|
||||
} else if (version < latestVersion) {
|
||||
logger.info(`Migrating PostgreSQL database, which is currently at version ${version}...`);
|
||||
while (version < latestVersion) {
|
||||
version++;
|
||||
logger.info(`Running version ${version} update script...`);
|
||||
await sql.unsafe(updates[version]);
|
||||
}
|
||||
} else if (version > latestVersion) {
|
||||
throw new Error(`PostgreSQL database is at version ${version}, but this version of the bot only supports up to version ${latestVersion}.`);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
await sql`INSERT INTO settings ${sql({ id: 1, version: latestVersion })} ON CONFLICT (id) DO UPDATE SET version = ${latestVersion}`;
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(`PostgreSQL migration failed: ${e}`);
|
||||
logger.error("Unable to start the bot, quitting now.");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getGuild(query) {
|
||||
let guild;
|
||||
await sql.begin(async (sql) => {
|
||||
guild = (await sql`SELECT * FROM guilds WHERE guild_id = ${query}`)[0];
|
||||
if (guild == undefined) {
|
||||
guild = { guild_id: query, prefix: process.env.PREFIX, disabled: [], disabled_commands: [] };
|
||||
await sql`INSERT INTO guilds ${sql(guild)}`;
|
||||
}
|
||||
});
|
||||
return guild;
|
||||
}
|
||||
|
||||
export async function setPrefix(prefix, guild) {
|
||||
await sql`UPDATE guilds SET prefix = ${prefix} WHERE guild_id = ${guild.id}`;
|
||||
prefixCache.set(guild.id, prefix);
|
||||
}
|
||||
|
||||
export async function getTag(guild, tag) {
|
||||
const tagResult = await sql`SELECT * FROM tags WHERE guild_id = ${guild} AND name = ${tag}`;
|
||||
return tagResult[0] ? { content: tagResult[0].content, author: tagResult[0].author } : undefined;
|
||||
}
|
||||
|
||||
export async function getTags(guild) {
|
||||
const tagArray = await sql`SELECT * FROM tags WHERE guild_id = ${guild}`;
|
||||
const tags = {};
|
||||
for (const tag of tagArray) {
|
||||
tags[tag.name] = { content: tag.content, author: tag.author };
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
export async function setTag(name, content, guild) {
|
||||
await sql`INSERT INTO tags ${sql({ guild_id: guild.id, name, content: content.content, author: content.author }, "guild_id", "name", "content", "author")}`;
|
||||
}
|
||||
|
||||
export async function editTag(name, content, guild) {
|
||||
await sql`UPDATE tags SET content = ${content.content}, author = ${content.author} WHERE guild_id = ${guild.id} AND name = ${name}`;
|
||||
}
|
||||
|
||||
export async function removeTag(name, guild) {
|
||||
await sql`DELETE FROM tags WHERE guild_id = ${guild.id} AND name = ${name}`;
|
||||
}
|
||||
|
||||
export async function setBroadcast(msg) {
|
||||
await sql`UPDATE settings SET broadcast = ${msg} WHERE id = 1`;
|
||||
}
|
||||
|
||||
export async function getBroadcast() {
|
||||
const result = await sql`SELECT broadcast FROM settings WHERE id = 1`;
|
||||
return result[0].broadcast;
|
||||
}
|
||||
|
||||
export async function disableCommand(guild, command) {
|
||||
const guildDB = await this.getGuild(guild);
|
||||
await sql`UPDATE guilds SET disabled_commands = ${(guildDB.disabled_commands ? [...guildDB.disabled_commands, command] : [command]).filter((v) => !!v)} WHERE guild_id = ${guild}`;
|
||||
disabledCmdCache.set(guild, guildDB.disabled_commands ? [...guildDB.disabled_commands, command] : [command].filter((v) => !!v));
|
||||
}
|
||||
|
||||
export async function enableCommand(guild, command) {
|
||||
const guildDB = await this.getGuild(guild);
|
||||
const newDisabled = guildDB.disabled_commands ? guildDB.disabled_commands.filter(item => item !== command) : [];
|
||||
await sql`UPDATE guilds SET disabled_commands = ${newDisabled} WHERE guild_id = ${guild}`;
|
||||
disabledCmdCache.set(guild, newDisabled);
|
||||
}
|
||||
|
||||
export async function disableChannel(channel) {
|
||||
const guildDB = await this.getGuild(channel.guildID);
|
||||
await sql`UPDATE guilds SET disabled_commands = ${[...guildDB.disabled, channel.id]} WHERE guild_id = ${channel.guildID}`;
|
||||
disabledCache.set(channel.guildID, [...guildDB.disabled, channel.id]);
|
||||
}
|
||||
|
||||
export async function enableChannel(channel) {
|
||||
const guildDB = await this.getGuild(channel.guildID);
|
||||
const newDisabled = guildDB.disabled.filter(item => item !== channel.id);
|
||||
await sql`UPDATE guilds SET disabled_commands = ${newDisabled} WHERE guild_id = ${channel.guildID}`;
|
||||
disabledCache.set(channel.guildID, newDisabled);
|
||||
}
|
||||
|
||||
export async function getCounts() {
|
||||
const counts = await sql`SELECT * FROM counts`;
|
||||
const countObject = {};
|
||||
for (const { command, count } of counts) {
|
||||
countObject[command] = count;
|
||||
}
|
||||
return countObject;
|
||||
}
|
||||
|
||||
export async function addCount(command) {
|
||||
await sql`INSERT INTO counts ${sql({ command, count: 1 }, "command", "count")} ON CONFLICT (command) DO UPDATE SET count = counts.count + 1 WHERE counts.command = ${command}`;
|
||||
}
|
||||
|
||||
export async function stop() {
|
||||
await sql.end();
|
||||
}
|
||||
import { prefixCache, disabledCmdCache, disabledCache, commands, messageCommands } from "../collections.js";
|
||||
|
||||
import Postgres from "postgres";
|
||||
const sql = Postgres(process.env.DB, {
|
||||
onnotice: () => {}
|
||||
});
|
||||
|
||||
const settingsSchema = `
|
||||
CREATE TABLE IF NOT EXISTS settings (
|
||||
id smallint PRIMARY KEY,
|
||||
version integer NOT NULL, CHECK(id = 1)
|
||||
);
|
||||
`;
|
||||
|
||||
const schema = `
|
||||
ALTER TABLE settings ADD COLUMN broadcast text;
|
||||
CREATE TABLE guilds (
|
||||
guild_id VARCHAR(30) NOT NULL PRIMARY KEY,
|
||||
prefix VARCHAR(15) NOT NULL,
|
||||
disabled text ARRAY NOT NULL,
|
||||
disabled_commands text ARRAY NOT NULL
|
||||
);
|
||||
CREATE TABLE counts (
|
||||
command VARCHAR NOT NULL PRIMARY KEY,
|
||||
count integer NOT NULL
|
||||
);
|
||||
CREATE TABLE tags (
|
||||
guild_id VARCHAR(30) NOT NULL,
|
||||
name text NOT NULL,
|
||||
content text NOT NULL,
|
||||
author VARCHAR(30) NOT NULL,
|
||||
UNIQUE(guild_id, name)
|
||||
);
|
||||
`;
|
||||
|
||||
const updates = [
|
||||
"", // reserved
|
||||
"CREATE TABLE IF NOT EXISTS settings ( id smallint PRIMARY KEY, version integer NOT NULL, CHECK(id = 1) );\nALTER TABLE guilds ADD COLUMN accessed timestamp;",
|
||||
"ALTER TABLE guilds DROP COLUMN accessed",
|
||||
"ALTER TABLE settings ADD COLUMN IF NOT EXISTS broadcast text"
|
||||
];
|
||||
|
||||
export async function setup() {
|
||||
const existingCommands = (await sql`SELECT command FROM counts`).map(x => x.command);
|
||||
const commandNames = [...commands.keys(), ...messageCommands.keys()];
|
||||
for (const command of existingCommands) {
|
||||
if (!commandNames.includes(command)) {
|
||||
await sql`DELETE FROM counts WHERE command = ${command}`;
|
||||
}
|
||||
}
|
||||
for (const command of commandNames) {
|
||||
if (!existingCommands.includes(command)) {
|
||||
await sql`INSERT INTO counts ${sql({ command, count: 0 }, "command", "count")}`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function upgrade(logger) {
|
||||
try {
|
||||
await sql.begin(async (sql) => {
|
||||
await sql.unsafe(settingsSchema);
|
||||
let version;
|
||||
const settingsrow = (await sql`SELECT version FROM settings WHERE id = 1`);
|
||||
if (settingsrow.length == 0) {
|
||||
version = 0;
|
||||
} else {
|
||||
version = settingsrow[0].version;
|
||||
}
|
||||
const latestVersion = updates.length - 1;
|
||||
if (version === 0) {
|
||||
logger.info("Initializing PostgreSQL database...");
|
||||
await sql.unsafe(schema);
|
||||
} else if (version < latestVersion) {
|
||||
logger.info(`Migrating PostgreSQL database, which is currently at version ${version}...`);
|
||||
while (version < latestVersion) {
|
||||
version++;
|
||||
logger.info(`Running version ${version} update script...`);
|
||||
await sql.unsafe(updates[version]);
|
||||
}
|
||||
} else if (version > latestVersion) {
|
||||
throw new Error(`PostgreSQL database is at version ${version}, but this version of the bot only supports up to version ${latestVersion}.`);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
await sql`INSERT INTO settings ${sql({ id: 1, version: latestVersion })} ON CONFLICT (id) DO UPDATE SET version = ${latestVersion}`;
|
||||
});
|
||||
} catch (e) {
|
||||
logger.error(`PostgreSQL migration failed: ${e}`);
|
||||
logger.error("Unable to start the bot, quitting now.");
|
||||
return 1;
|
||||
}
|
||||
}
|
||||
|
||||
export async function getGuild(query) {
|
||||
let guild;
|
||||
await sql.begin(async (sql) => {
|
||||
guild = (await sql`SELECT * FROM guilds WHERE guild_id = ${query}`)[0];
|
||||
if (guild == undefined) {
|
||||
guild = { guild_id: query, prefix: process.env.PREFIX, disabled: [], disabled_commands: [] };
|
||||
await sql`INSERT INTO guilds ${sql(guild)}`;
|
||||
}
|
||||
});
|
||||
return guild;
|
||||
}
|
||||
|
||||
export async function setPrefix(prefix, guild) {
|
||||
await sql`UPDATE guilds SET prefix = ${prefix} WHERE guild_id = ${guild.id}`;
|
||||
prefixCache.set(guild.id, prefix);
|
||||
}
|
||||
|
||||
export async function getTag(guild, tag) {
|
||||
const tagResult = await sql`SELECT * FROM tags WHERE guild_id = ${guild} AND name = ${tag}`;
|
||||
return tagResult[0] ? { content: tagResult[0].content, author: tagResult[0].author } : undefined;
|
||||
}
|
||||
|
||||
export async function getTags(guild) {
|
||||
const tagArray = await sql`SELECT * FROM tags WHERE guild_id = ${guild}`;
|
||||
const tags = {};
|
||||
for (const tag of tagArray) {
|
||||
tags[tag.name] = { content: tag.content, author: tag.author };
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
export async function setTag(name, content, guild) {
|
||||
await sql`INSERT INTO tags ${sql({ guild_id: guild.id, name, content: content.content, author: content.author }, "guild_id", "name", "content", "author")}`;
|
||||
}
|
||||
|
||||
export async function editTag(name, content, guild) {
|
||||
await sql`UPDATE tags SET content = ${content.content}, author = ${content.author} WHERE guild_id = ${guild.id} AND name = ${name}`;
|
||||
}
|
||||
|
||||
export async function removeTag(name, guild) {
|
||||
await sql`DELETE FROM tags WHERE guild_id = ${guild.id} AND name = ${name}`;
|
||||
}
|
||||
|
||||
export async function setBroadcast(msg) {
|
||||
await sql`UPDATE settings SET broadcast = ${msg} WHERE id = 1`;
|
||||
}
|
||||
|
||||
export async function getBroadcast() {
|
||||
const result = await sql`SELECT broadcast FROM settings WHERE id = 1`;
|
||||
return result[0].broadcast;
|
||||
}
|
||||
|
||||
export async function disableCommand(guild, command) {
|
||||
const guildDB = await this.getGuild(guild);
|
||||
await sql`UPDATE guilds SET disabled_commands = ${(guildDB.disabled_commands ? [...guildDB.disabled_commands, command] : [command]).filter((v) => !!v)} WHERE guild_id = ${guild}`;
|
||||
disabledCmdCache.set(guild, guildDB.disabled_commands ? [...guildDB.disabled_commands, command] : [command].filter((v) => !!v));
|
||||
}
|
||||
|
||||
export async function enableCommand(guild, command) {
|
||||
const guildDB = await this.getGuild(guild);
|
||||
const newDisabled = guildDB.disabled_commands ? guildDB.disabled_commands.filter(item => item !== command) : [];
|
||||
await sql`UPDATE guilds SET disabled_commands = ${newDisabled} WHERE guild_id = ${guild}`;
|
||||
disabledCmdCache.set(guild, newDisabled);
|
||||
}
|
||||
|
||||
export async function disableChannel(channel) {
|
||||
const guildDB = await this.getGuild(channel.guildID);
|
||||
await sql`UPDATE guilds SET disabled_commands = ${[...guildDB.disabled, channel.id]} WHERE guild_id = ${channel.guildID}`;
|
||||
disabledCache.set(channel.guildID, [...guildDB.disabled, channel.id]);
|
||||
}
|
||||
|
||||
export async function enableChannel(channel) {
|
||||
const guildDB = await this.getGuild(channel.guildID);
|
||||
const newDisabled = guildDB.disabled.filter(item => item !== channel.id);
|
||||
await sql`UPDATE guilds SET disabled_commands = ${newDisabled} WHERE guild_id = ${channel.guildID}`;
|
||||
disabledCache.set(channel.guildID, newDisabled);
|
||||
}
|
||||
|
||||
export async function getCounts() {
|
||||
const counts = await sql`SELECT * FROM counts`;
|
||||
const countObject = {};
|
||||
for (const { command, count } of counts) {
|
||||
countObject[command] = count;
|
||||
}
|
||||
return countObject;
|
||||
}
|
||||
|
||||
export async function addCount(command) {
|
||||
await sql`INSERT INTO counts ${sql({ command, count: 1 }, "command", "count")} ON CONFLICT (command) DO UPDATE SET count = counts.count + 1 WHERE counts.command = ${command}`;
|
||||
}
|
||||
|
||||
export async function stop() {
|
||||
await sql.end();
|
||||
}
|
||||
|
|
|
@ -1,194 +1,194 @@
|
|||
import { commands, messageCommands, disabledCache, disabledCmdCache, prefixCache } from "../collections.js";
|
||||
|
||||
import sqlite3 from "better-sqlite3";
|
||||
const connection = sqlite3(process.env.DB.replace("sqlite://", ""));
|
||||
|
||||
const schema = `
|
||||
CREATE TABLE guilds (
|
||||
guild_id VARCHAR(30) NOT NULL PRIMARY KEY,
|
||||
prefix VARCHAR(15) NOT NULL,
|
||||
disabled text NOT NULL,
|
||||
disabled_commands text NOT NULL
|
||||
);
|
||||
CREATE TABLE counts (
|
||||
command VARCHAR NOT NULL PRIMARY KEY,
|
||||
count integer NOT NULL
|
||||
);
|
||||
CREATE TABLE tags (
|
||||
guild_id VARCHAR(30) NOT NULL,
|
||||
name text NOT NULL,
|
||||
content text NOT NULL,
|
||||
author VARCHAR(30) NOT NULL,
|
||||
UNIQUE(guild_id, name)
|
||||
);
|
||||
CREATE TABLE settings (
|
||||
id smallint PRIMARY KEY,
|
||||
broadcast VARCHAR,
|
||||
CHECK(id = 1)
|
||||
);
|
||||
INSERT INTO settings (id) VALUES (1);
|
||||
`;
|
||||
|
||||
const updates = [
|
||||
"", // reserved
|
||||
"ALTER TABLE guilds ADD COLUMN accessed int",
|
||||
"ALTER TABLE guilds DROP COLUMN accessed",
|
||||
`CREATE TABLE settings (
|
||||
id smallint PRIMARY KEY,
|
||||
broadcast VARCHAR,
|
||||
CHECK(id = 1)
|
||||
);
|
||||
INSERT INTO settings (id) VALUES (1);`,
|
||||
];
|
||||
|
||||
export async function setup() {
|
||||
const existingCommands = connection.prepare("SELECT command FROM counts").all().map(x => x.command);
|
||||
const commandNames = [...commands.keys(), ...messageCommands.keys()];
|
||||
for (const command of existingCommands) {
|
||||
if (!commandNames.includes(command)) {
|
||||
connection.prepare("DELETE FROM counts WHERE command = ?").run(command);
|
||||
}
|
||||
}
|
||||
for (const command of commandNames) {
|
||||
if (!existingCommands.includes(command)) {
|
||||
connection.prepare("INSERT INTO counts (command, count) VALUES (?, ?)").run(command, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function stop() {
|
||||
connection.close();
|
||||
}
|
||||
|
||||
export async function upgrade(logger) {
|
||||
connection.exec("BEGIN TRANSACTION");
|
||||
try {
|
||||
let version = connection.pragma("user_version", { simple: true });
|
||||
const latestVersion = updates.length - 1;
|
||||
if (version == 0) {
|
||||
logger.info("Initializing SQLite database...");
|
||||
connection.exec(schema);
|
||||
} else if (version < latestVersion) {
|
||||
logger.info(`Migrating SQLite database at ${process.env.DB}, which is currently at version ${version}...`);
|
||||
while (version < latestVersion) {
|
||||
version++;
|
||||
logger.info(`Running version ${version} update script...`);
|
||||
connection.exec(updates[version]);
|
||||
}
|
||||
} else if (version > latestVersion) {
|
||||
throw new Error(`SQLite database is at version ${version}, but this version of the bot only supports up to version ${latestVersion}.`);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
connection.pragma(`user_version = ${latestVersion}`); // prepared statements don't seem to work here
|
||||
} catch (e) {
|
||||
logger.error(`SQLite migration failed: ${e}`);
|
||||
connection.exec("ROLLBACK");
|
||||
logger.error("Unable to start the bot, quitting now.");
|
||||
return 1;
|
||||
}
|
||||
connection.exec("COMMIT");
|
||||
}
|
||||
|
||||
export async function addCount(command) {
|
||||
connection.prepare("UPDATE counts SET count = count + 1 WHERE command = ?").run(command);
|
||||
}
|
||||
|
||||
export async function getCounts() {
|
||||
const counts = connection.prepare("SELECT * FROM counts").all();
|
||||
const countObject = {};
|
||||
for (const { command, count } of counts) {
|
||||
countObject[command] = count;
|
||||
}
|
||||
return countObject;
|
||||
}
|
||||
|
||||
export async function disableCommand(guild, command) {
|
||||
const guildDB = await this.getGuild(guild);
|
||||
connection.prepare("UPDATE guilds SET disabled_commands = ? WHERE guild_id = ?").run(JSON.stringify((guildDB.disabledCommands ? [...JSON.parse(guildDB.disabledCommands), command] : [command]).filter((v) => !!v)), guild);
|
||||
disabledCmdCache.set(guild, guildDB.disabled_commands ? [...JSON.parse(guildDB.disabledCommands), command] : [command].filter((v) => !!v));
|
||||
}
|
||||
|
||||
export async function enableCommand(guild, command) {
|
||||
const guildDB = await this.getGuild(guild);
|
||||
const newDisabled = guildDB.disabledCommands ? JSON.parse(guildDB.disabledCommands).filter(item => item !== command) : [];
|
||||
connection.prepare("UPDATE guilds SET disabled_commands = ? WHERE guild_id = ?").run(JSON.stringify(newDisabled), guild);
|
||||
disabledCmdCache.set(guild, newDisabled);
|
||||
}
|
||||
|
||||
export async function disableChannel(channel) {
|
||||
const guildDB = await this.getGuild(channel.guildID);
|
||||
connection.prepare("UPDATE guilds SET disabled = ? WHERE guild_id = ?").run(JSON.stringify([...JSON.parse(guildDB.disabled), channel.id]), channel.guildID);
|
||||
disabledCache.set(channel.guildID, [...JSON.parse(guildDB.disabled), channel.id]);
|
||||
}
|
||||
|
||||
export async function enableChannel(channel) {
|
||||
const guildDB = await this.getGuild(channel.guildID);
|
||||
const newDisabled = JSON.parse(guildDB.disabled).filter(item => item !== channel.id);
|
||||
connection.prepare("UPDATE guilds SET disabled = ? WHERE guild_id = ?").run(JSON.stringify(newDisabled), channel.guildID);
|
||||
disabledCache.set(channel.guildID, newDisabled);
|
||||
}
|
||||
|
||||
export async function getTag(guild, tag) {
|
||||
const tagResult = connection.prepare("SELECT * FROM tags WHERE guild_id = ? AND name = ?").get(guild, tag);
|
||||
return tagResult ? { content: tagResult.content, author: tagResult.author } : undefined;
|
||||
}
|
||||
|
||||
export async function getTags(guild) {
|
||||
const tagArray = connection.prepare("SELECT * FROM tags WHERE guild_id = ?").all(guild);
|
||||
const tags = {};
|
||||
if (!tagArray) return [];
|
||||
for (const tag of tagArray) {
|
||||
tags[tag.name] = { content: tag.content, author: tag.author };
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
export async function setTag(name, content, guild) {
|
||||
const tag = {
|
||||
id: guild.id,
|
||||
name: name,
|
||||
content: content.content,
|
||||
author: content.author
|
||||
};
|
||||
connection.prepare("INSERT INTO tags (guild_id, name, content, author) VALUES (@id, @name, @content, @author)").run(tag);
|
||||
}
|
||||
|
||||
export async function removeTag(name, guild) {
|
||||
connection.prepare("DELETE FROM tags WHERE guild_id = ? AND name = ?").run(guild.id, name);
|
||||
}
|
||||
|
||||
export async function editTag(name, content, guild) {
|
||||
connection.prepare("UPDATE tags SET content = ?, author = ? WHERE guild_id = ? AND name = ?").run(content.content, content.author, guild.id, name);
|
||||
}
|
||||
|
||||
export async function setBroadcast(msg) {
|
||||
connection.prepare("UPDATE settings SET broadcast = ? WHERE id = 1").run(msg);
|
||||
}
|
||||
|
||||
export async function getBroadcast() {
|
||||
const result = connection.prepare("SELECT broadcast FROM settings WHERE id = 1").all();
|
||||
return result[0].broadcast;
|
||||
}
|
||||
|
||||
export async function setPrefix(prefix, guild) {
|
||||
connection.prepare("UPDATE guilds SET prefix = ? WHERE guild_id = ?").run(prefix, guild.id);
|
||||
prefixCache.set(guild.id, prefix);
|
||||
}
|
||||
|
||||
export async function getGuild(query) {
|
||||
let guild;
|
||||
connection.transaction(() => {
|
||||
guild = connection.prepare("SELECT * FROM guilds WHERE guild_id = ?").get(query);
|
||||
if (!guild) {
|
||||
guild = {
|
||||
id: query,
|
||||
prefix: process.env.PREFIX,
|
||||
disabled: "[]",
|
||||
disabledCommands: "[]"
|
||||
};
|
||||
connection.prepare("INSERT INTO guilds (guild_id, prefix, disabled, disabled_commands) VALUES (@id, @prefix, @disabled, @disabledCommands)").run(guild);
|
||||
}
|
||||
})();
|
||||
return guild;
|
||||
}
|
||||
import { commands, messageCommands, disabledCache, disabledCmdCache, prefixCache } from "../collections.js";
|
||||
|
||||
import sqlite3 from "better-sqlite3";
|
||||
const connection = sqlite3(process.env.DB.replace("sqlite://", ""));
|
||||
|
||||
const schema = `
|
||||
CREATE TABLE guilds (
|
||||
guild_id VARCHAR(30) NOT NULL PRIMARY KEY,
|
||||
prefix VARCHAR(15) NOT NULL,
|
||||
disabled text NOT NULL,
|
||||
disabled_commands text NOT NULL
|
||||
);
|
||||
CREATE TABLE counts (
|
||||
command VARCHAR NOT NULL PRIMARY KEY,
|
||||
count integer NOT NULL
|
||||
);
|
||||
CREATE TABLE tags (
|
||||
guild_id VARCHAR(30) NOT NULL,
|
||||
name text NOT NULL,
|
||||
content text NOT NULL,
|
||||
author VARCHAR(30) NOT NULL,
|
||||
UNIQUE(guild_id, name)
|
||||
);
|
||||
CREATE TABLE settings (
|
||||
id smallint PRIMARY KEY,
|
||||
broadcast VARCHAR,
|
||||
CHECK(id = 1)
|
||||
);
|
||||
INSERT INTO settings (id) VALUES (1);
|
||||
`;
|
||||
|
||||
const updates = [
|
||||
"", // reserved
|
||||
"ALTER TABLE guilds ADD COLUMN accessed int",
|
||||
"ALTER TABLE guilds DROP COLUMN accessed",
|
||||
`CREATE TABLE settings (
|
||||
id smallint PRIMARY KEY,
|
||||
broadcast VARCHAR,
|
||||
CHECK(id = 1)
|
||||
);
|
||||
INSERT INTO settings (id) VALUES (1);`,
|
||||
];
|
||||
|
||||
export async function setup() {
|
||||
const existingCommands = connection.prepare("SELECT command FROM counts").all().map(x => x.command);
|
||||
const commandNames = [...commands.keys(), ...messageCommands.keys()];
|
||||
for (const command of existingCommands) {
|
||||
if (!commandNames.includes(command)) {
|
||||
connection.prepare("DELETE FROM counts WHERE command = ?").run(command);
|
||||
}
|
||||
}
|
||||
for (const command of commandNames) {
|
||||
if (!existingCommands.includes(command)) {
|
||||
connection.prepare("INSERT INTO counts (command, count) VALUES (?, ?)").run(command, 0);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export async function stop() {
|
||||
connection.close();
|
||||
}
|
||||
|
||||
export async function upgrade(logger) {
|
||||
connection.exec("BEGIN TRANSACTION");
|
||||
try {
|
||||
let version = connection.pragma("user_version", { simple: true });
|
||||
const latestVersion = updates.length - 1;
|
||||
if (version == 0) {
|
||||
logger.info("Initializing SQLite database...");
|
||||
connection.exec(schema);
|
||||
} else if (version < latestVersion) {
|
||||
logger.info(`Migrating SQLite database at ${process.env.DB}, which is currently at version ${version}...`);
|
||||
while (version < latestVersion) {
|
||||
version++;
|
||||
logger.info(`Running version ${version} update script...`);
|
||||
connection.exec(updates[version]);
|
||||
}
|
||||
} else if (version > latestVersion) {
|
||||
throw new Error(`SQLite database is at version ${version}, but this version of the bot only supports up to version ${latestVersion}.`);
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
connection.pragma(`user_version = ${latestVersion}`); // prepared statements don't seem to work here
|
||||
} catch (e) {
|
||||
logger.error(`SQLite migration failed: ${e}`);
|
||||
connection.exec("ROLLBACK");
|
||||
logger.error("Unable to start the bot, quitting now.");
|
||||
return 1;
|
||||
}
|
||||
connection.exec("COMMIT");
|
||||
}
|
||||
|
||||
export async function addCount(command) {
|
||||
connection.prepare("UPDATE counts SET count = count + 1 WHERE command = ?").run(command);
|
||||
}
|
||||
|
||||
export async function getCounts() {
|
||||
const counts = connection.prepare("SELECT * FROM counts").all();
|
||||
const countObject = {};
|
||||
for (const { command, count } of counts) {
|
||||
countObject[command] = count;
|
||||
}
|
||||
return countObject;
|
||||
}
|
||||
|
||||
export async function disableCommand(guild, command) {
|
||||
const guildDB = await this.getGuild(guild);
|
||||
connection.prepare("UPDATE guilds SET disabled_commands = ? WHERE guild_id = ?").run(JSON.stringify((guildDB.disabledCommands ? [...JSON.parse(guildDB.disabledCommands), command] : [command]).filter((v) => !!v)), guild);
|
||||
disabledCmdCache.set(guild, guildDB.disabled_commands ? [...JSON.parse(guildDB.disabledCommands), command] : [command].filter((v) => !!v));
|
||||
}
|
||||
|
||||
export async function enableCommand(guild, command) {
|
||||
const guildDB = await this.getGuild(guild);
|
||||
const newDisabled = guildDB.disabledCommands ? JSON.parse(guildDB.disabledCommands).filter(item => item !== command) : [];
|
||||
connection.prepare("UPDATE guilds SET disabled_commands = ? WHERE guild_id = ?").run(JSON.stringify(newDisabled), guild);
|
||||
disabledCmdCache.set(guild, newDisabled);
|
||||
}
|
||||
|
||||
export async function disableChannel(channel) {
|
||||
const guildDB = await this.getGuild(channel.guildID);
|
||||
connection.prepare("UPDATE guilds SET disabled = ? WHERE guild_id = ?").run(JSON.stringify([...JSON.parse(guildDB.disabled), channel.id]), channel.guildID);
|
||||
disabledCache.set(channel.guildID, [...JSON.parse(guildDB.disabled), channel.id]);
|
||||
}
|
||||
|
||||
export async function enableChannel(channel) {
|
||||
const guildDB = await this.getGuild(channel.guildID);
|
||||
const newDisabled = JSON.parse(guildDB.disabled).filter(item => item !== channel.id);
|
||||
connection.prepare("UPDATE guilds SET disabled = ? WHERE guild_id = ?").run(JSON.stringify(newDisabled), channel.guildID);
|
||||
disabledCache.set(channel.guildID, newDisabled);
|
||||
}
|
||||
|
||||
export async function getTag(guild, tag) {
|
||||
const tagResult = connection.prepare("SELECT * FROM tags WHERE guild_id = ? AND name = ?").get(guild, tag);
|
||||
return tagResult ? { content: tagResult.content, author: tagResult.author } : undefined;
|
||||
}
|
||||
|
||||
export async function getTags(guild) {
|
||||
const tagArray = connection.prepare("SELECT * FROM tags WHERE guild_id = ?").all(guild);
|
||||
const tags = {};
|
||||
if (!tagArray) return [];
|
||||
for (const tag of tagArray) {
|
||||
tags[tag.name] = { content: tag.content, author: tag.author };
|
||||
}
|
||||
return tags;
|
||||
}
|
||||
|
||||
export async function setTag(name, content, guild) {
|
||||
const tag = {
|
||||
id: guild.id,
|
||||
name: name,
|
||||
content: content.content,
|
||||
author: content.author
|
||||
};
|
||||
connection.prepare("INSERT INTO tags (guild_id, name, content, author) VALUES (@id, @name, @content, @author)").run(tag);
|
||||
}
|
||||
|
||||
export async function removeTag(name, guild) {
|
||||
connection.prepare("DELETE FROM tags WHERE guild_id = ? AND name = ?").run(guild.id, name);
|
||||
}
|
||||
|
||||
export async function editTag(name, content, guild) {
|
||||
connection.prepare("UPDATE tags SET content = ?, author = ? WHERE guild_id = ? AND name = ?").run(content.content, content.author, guild.id, name);
|
||||
}
|
||||
|
||||
export async function setBroadcast(msg) {
|
||||
connection.prepare("UPDATE settings SET broadcast = ? WHERE id = 1").run(msg);
|
||||
}
|
||||
|
||||
export async function getBroadcast() {
|
||||
const result = connection.prepare("SELECT broadcast FROM settings WHERE id = 1").all();
|
||||
return result[0].broadcast;
|
||||
}
|
||||
|
||||
export async function setPrefix(prefix, guild) {
|
||||
connection.prepare("UPDATE guilds SET prefix = ? WHERE guild_id = ?").run(prefix, guild.id);
|
||||
prefixCache.set(guild.id, prefix);
|
||||
}
|
||||
|
||||
export async function getGuild(query) {
|
||||
let guild;
|
||||
connection.transaction(() => {
|
||||
guild = connection.prepare("SELECT * FROM guilds WHERE guild_id = ?").get(query);
|
||||
if (!guild) {
|
||||
guild = {
|
||||
id: query,
|
||||
prefix: process.env.PREFIX,
|
||||
disabled: "[]",
|
||||
disabledCommands: "[]"
|
||||
};
|
||||
connection.prepare("INSERT INTO guilds (guild_id, prefix, disabled, disabled_commands) VALUES (@id, @prefix, @disabled, @disabledCommands)").run(guild);
|
||||
}
|
||||
})();
|
||||
return guild;
|
||||
}
|
||||
|
|
250
utils/handler.js
250
utils/handler.js
|
@ -1,126 +1,126 @@
|
|||
import { paths, commands, messageCommands, info, sounds, categories, aliases as _aliases } from "./collections.js";
|
||||
import { log } from "./logger.js";
|
||||
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
const { blacklist } = JSON.parse(readFileSync(new URL("../config/commands.json", import.meta.url)));
|
||||
|
||||
let queryValue = 0;
|
||||
|
||||
// load command into memory
|
||||
export async function load(client, command, slashReload = false) {
|
||||
const { default: props } = await import(`${command}?v=${queryValue}`);
|
||||
queryValue++;
|
||||
const commandArray = command.split("/");
|
||||
let commandName = commandArray[commandArray.length - 1].split(".")[0];
|
||||
const category = commandArray[commandArray.length - 2];
|
||||
|
||||
if (blacklist.includes(commandName)) {
|
||||
log("warn", `Skipped loading blacklisted command ${command}...`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (category === "message") {
|
||||
const nameStringArray = commandName.split("-");
|
||||
for (const index of nameStringArray.keys()) {
|
||||
nameStringArray[index] = nameStringArray[index].charAt(0).toUpperCase() + nameStringArray[index].slice(1);
|
||||
}
|
||||
commandName = nameStringArray.join(" ");
|
||||
}
|
||||
|
||||
props.init();
|
||||
paths.set(commandName, command);
|
||||
|
||||
const commandInfo = {
|
||||
category: category,
|
||||
description: props.description,
|
||||
aliases: props.aliases,
|
||||
params: props.arguments,
|
||||
flags: props.flags,
|
||||
slashAllowed: props.slashAllowed,
|
||||
directAllowed: props.directAllowed,
|
||||
adminOnly: props.adminOnly,
|
||||
type: 1
|
||||
};
|
||||
|
||||
if (category === "message") {
|
||||
messageCommands.set(commandName, props);
|
||||
commandInfo.type = 3;
|
||||
} else {
|
||||
commands.set(commandName, props);
|
||||
}
|
||||
|
||||
if (slashReload && props.slashAllowed) {
|
||||
await send(client);
|
||||
}
|
||||
|
||||
if (Object.getPrototypeOf(props).name === "SoundboardCommand") sounds.set(commandName, props.file);
|
||||
|
||||
info.set(commandName, commandInfo);
|
||||
|
||||
const categoryCommands = categories.get(category);
|
||||
categories.set(category, categoryCommands ? [...categoryCommands, commandName] : [commandName]);
|
||||
|
||||
if (props.aliases) {
|
||||
for (const alias of props.aliases) {
|
||||
_aliases.set(alias, commandName);
|
||||
paths.set(alias, command);
|
||||
}
|
||||
}
|
||||
return commandName;
|
||||
}
|
||||
|
||||
export function update() {
|
||||
const commandArray = [];
|
||||
const privateCommandArray = [];
|
||||
const merged = new Map([...commands, ...messageCommands]);
|
||||
for (const [name, command] of merged.entries()) {
|
||||
let cmdInfo = info.get(name);
|
||||
if (command.postInit) {
|
||||
const cmd = command.postInit();
|
||||
cmdInfo = {
|
||||
category: cmdInfo.category,
|
||||
description: cmd.description,
|
||||
aliases: cmd.aliases,
|
||||
params: cmd.arguments,
|
||||
flags: cmd.flags,
|
||||
slashAllowed: cmd.slashAllowed,
|
||||
directAllowed: cmd.directAllowed,
|
||||
adminOnly: cmd.adminOnly,
|
||||
type: cmdInfo.type
|
||||
};
|
||||
info.set(name, cmdInfo);
|
||||
}
|
||||
if (cmdInfo?.type === 3) {
|
||||
(cmdInfo.adminOnly ? privateCommandArray : commandArray).push({
|
||||
name: name,
|
||||
type: cmdInfo.type,
|
||||
dm_permission: cmdInfo.directAllowed
|
||||
});
|
||||
} else if (cmdInfo?.slashAllowed) {
|
||||
(cmdInfo.adminOnly ? privateCommandArray : commandArray).push({
|
||||
name,
|
||||
type: cmdInfo.type,
|
||||
description: cmdInfo.description,
|
||||
options: cmdInfo.flags,
|
||||
dm_permission: cmdInfo.directAllowed
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
main: commandArray,
|
||||
private: privateCommandArray
|
||||
};
|
||||
}
|
||||
|
||||
export async function send(bot) {
|
||||
const commandArray = update();
|
||||
log("info", "Sending application command data to Discord...");
|
||||
let cmdArray = commandArray.main;
|
||||
if (process.env.ADMIN_SERVER && process.env.ADMIN_SERVER !== "") {
|
||||
await bot.application.bulkEditGuildCommands(process.env.ADMIN_SERVER, commandArray.private);
|
||||
} else {
|
||||
cmdArray = [...commandArray.main, ...commandArray.private];
|
||||
}
|
||||
await bot.application.bulkEditGlobalCommands(cmdArray);
|
||||
import { paths, commands, messageCommands, info, sounds, categories, aliases as _aliases } from "./collections.js";
|
||||
import { log } from "./logger.js";
|
||||
|
||||
import { readFileSync } from "fs";
|
||||
|
||||
const { blacklist } = JSON.parse(readFileSync(new URL("../config/commands.json", import.meta.url)));
|
||||
|
||||
let queryValue = 0;
|
||||
|
||||
// load command into memory
|
||||
export async function load(client, command, slashReload = false) {
|
||||
const { default: props } = await import(`${command}?v=${queryValue}`);
|
||||
queryValue++;
|
||||
const commandArray = command.split("/");
|
||||
let commandName = commandArray[commandArray.length - 1].split(".")[0];
|
||||
const category = commandArray[commandArray.length - 2];
|
||||
|
||||
if (blacklist.includes(commandName)) {
|
||||
log("warn", `Skipped loading blacklisted command ${command}...`);
|
||||
return;
|
||||
}
|
||||
|
||||
if (category === "message") {
|
||||
const nameStringArray = commandName.split("-");
|
||||
for (const index of nameStringArray.keys()) {
|
||||
nameStringArray[index] = nameStringArray[index].charAt(0).toUpperCase() + nameStringArray[index].slice(1);
|
||||
}
|
||||
commandName = nameStringArray.join(" ");
|
||||
}
|
||||
|
||||
props.init();
|
||||
paths.set(commandName, command);
|
||||
|
||||
const commandInfo = {
|
||||
category: category,
|
||||
description: props.description,
|
||||
aliases: props.aliases,
|
||||
params: props.arguments,
|
||||
flags: props.flags,
|
||||
slashAllowed: props.slashAllowed,
|
||||
directAllowed: props.directAllowed,
|
||||
adminOnly: props.adminOnly,
|
||||
type: 1
|
||||
};
|
||||
|
||||
if (category === "message") {
|
||||
messageCommands.set(commandName, props);
|
||||
commandInfo.type = 3;
|
||||
} else {
|
||||
commands.set(commandName, props);
|
||||
}
|
||||
|
||||
if (slashReload && props.slashAllowed) {
|
||||
await send(client);
|
||||
}
|
||||
|
||||
if (Object.getPrototypeOf(props).name === "SoundboardCommand") sounds.set(commandName, props.file);
|
||||
|
||||
info.set(commandName, commandInfo);
|
||||
|
||||
const categoryCommands = categories.get(category);
|
||||
categories.set(category, categoryCommands ? [...categoryCommands, commandName] : [commandName]);
|
||||
|
||||
if (props.aliases) {
|
||||
for (const alias of props.aliases) {
|
||||
_aliases.set(alias, commandName);
|
||||
paths.set(alias, command);
|
||||
}
|
||||
}
|
||||
return commandName;
|
||||
}
|
||||
|
||||
export function update() {
|
||||
const commandArray = [];
|
||||
const privateCommandArray = [];
|
||||
const merged = new Map([...commands, ...messageCommands]);
|
||||
for (const [name, command] of merged.entries()) {
|
||||
let cmdInfo = info.get(name);
|
||||
if (command.postInit) {
|
||||
const cmd = command.postInit();
|
||||
cmdInfo = {
|
||||
category: cmdInfo.category,
|
||||
description: cmd.description,
|
||||
aliases: cmd.aliases,
|
||||
params: cmd.arguments,
|
||||
flags: cmd.flags,
|
||||
slashAllowed: cmd.slashAllowed,
|
||||
directAllowed: cmd.directAllowed,
|
||||
adminOnly: cmd.adminOnly,
|
||||
type: cmdInfo.type
|
||||
};
|
||||
info.set(name, cmdInfo);
|
||||
}
|
||||
if (cmdInfo?.type === 3) {
|
||||
(cmdInfo.adminOnly ? privateCommandArray : commandArray).push({
|
||||
name: name,
|
||||
type: cmdInfo.type,
|
||||
dm_permission: cmdInfo.directAllowed
|
||||
});
|
||||
} else if (cmdInfo?.slashAllowed) {
|
||||
(cmdInfo.adminOnly ? privateCommandArray : commandArray).push({
|
||||
name,
|
||||
type: cmdInfo.type,
|
||||
description: cmdInfo.description,
|
||||
options: cmdInfo.flags,
|
||||
dm_permission: cmdInfo.directAllowed
|
||||
});
|
||||
}
|
||||
}
|
||||
return {
|
||||
main: commandArray,
|
||||
private: privateCommandArray
|
||||
};
|
||||
}
|
||||
|
||||
export async function send(bot) {
|
||||
const commandArray = update();
|
||||
log("info", "Sending application command data to Discord...");
|
||||
let cmdArray = commandArray.main;
|
||||
if (process.env.ADMIN_SERVER && process.env.ADMIN_SERVER !== "") {
|
||||
await bot.application.bulkEditGuildCommands(process.env.ADMIN_SERVER, commandArray.private);
|
||||
} else {
|
||||
cmdArray = [...commandArray.main, ...commandArray.private];
|
||||
}
|
||||
await bot.application.bulkEditGlobalCommands(cmdArray);
|
||||
}
|
136
utils/help.js
136
utils/help.js
|
@ -1,69 +1,69 @@
|
|||
import { commands, info } from "./collections.js";
|
||||
import { promises } from "fs";
|
||||
|
||||
export const categoryTemplate = {
|
||||
general: [],
|
||||
tags: ["> **Every command in this category is a subcommand of the tag command.**\n"],
|
||||
"image-editing": ["> **These commands support the PNG, JPEG, WEBP (static), and GIF (animated or static) formats.**\n"]
|
||||
};
|
||||
export let categories = categoryTemplate;
|
||||
|
||||
export let generated = false;
|
||||
|
||||
export function generateList() {
|
||||
categories = categoryTemplate;
|
||||
for (const [command] of commands) {
|
||||
const category = info.get(command).category;
|
||||
const description = info.get(command).description;
|
||||
const params = info.get(command).params;
|
||||
if (category === "tags") {
|
||||
const subCommands = info.get(command).flags;
|
||||
categories.tags.push(`**tags** ${params.default} - ${description}`);
|
||||
for (const subCommand of subCommands) {
|
||||
categories.tags.push(`**tags ${subCommand.name}**${params[subCommand.name] ? ` ${params[subCommand.name].join(" ")}` : ""} - ${subCommand.description}`);
|
||||
}
|
||||
} else {
|
||||
if (!categories[category]) categories[category] = [];
|
||||
categories[category].push(`**${command}**${params ? ` ${params.join(" ")}` : ""} - ${description}`);
|
||||
}
|
||||
}
|
||||
generated = true;
|
||||
}
|
||||
|
||||
export async function createPage(output) {
|
||||
let template = `# <img src="https://raw.githubusercontent.com/esmBot/esmBot/master/docs/assets/esmbot.png" width="64"> esmBot${process.env.NODE_ENV === "development" ? " Dev" : ""} Command List
|
||||
|
||||
This page was last generated on \`${new Date().toString()}\`.
|
||||
|
||||
\`[]\` means an argument is required, \`{}\` means an argument is optional.
|
||||
|
||||
**Want to help support esmBot's development? Consider donating on Patreon!** https://patreon.com/TheEssem
|
||||
`;
|
||||
|
||||
template += "\n## Table of Contents\n";
|
||||
for (const category of Object.keys(categories)) {
|
||||
const categoryStringArray = category.split("-");
|
||||
for (const index of categoryStringArray.keys()) {
|
||||
categoryStringArray[index] = categoryStringArray[index].charAt(0).toUpperCase() + categoryStringArray[index].slice(1);
|
||||
}
|
||||
template += `+ [**${categoryStringArray.join(" ")}**](#${category})\n`;
|
||||
}
|
||||
|
||||
// hell
|
||||
for (const category of Object.keys(categories)) {
|
||||
const categoryStringArray = category.split("-");
|
||||
for (const index of categoryStringArray.keys()) {
|
||||
categoryStringArray[index] = categoryStringArray[index].charAt(0).toUpperCase() + categoryStringArray[index].slice(1);
|
||||
}
|
||||
template += `\n## ${categoryStringArray.join(" ")}\n`;
|
||||
for (const command of categories[category]) {
|
||||
if (command.startsWith(">")) {
|
||||
template += `${command}\n`;
|
||||
} else {
|
||||
template += `+ ${command}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await promises.writeFile(output, template);
|
||||
import { commands, info } from "./collections.js";
|
||||
import { promises } from "fs";
|
||||
|
||||
export const categoryTemplate = {
|
||||
general: [],
|
||||
tags: ["> **Every command in this category is a subcommand of the tag command.**\n"],
|
||||
"image-editing": ["> **These commands support the PNG, JPEG, WEBP (static), and GIF (animated or static) formats.**\n"]
|
||||
};
|
||||
export let categories = categoryTemplate;
|
||||
|
||||
export let generated = false;
|
||||
|
||||
export function generateList() {
|
||||
categories = categoryTemplate;
|
||||
for (const [command] of commands) {
|
||||
const category = info.get(command).category;
|
||||
const description = info.get(command).description;
|
||||
const params = info.get(command).params;
|
||||
if (category === "tags") {
|
||||
const subCommands = info.get(command).flags;
|
||||
categories.tags.push(`**tags** ${params.default} - ${description}`);
|
||||
for (const subCommand of subCommands) {
|
||||
categories.tags.push(`**tags ${subCommand.name}**${params[subCommand.name] ? ` ${params[subCommand.name].join(" ")}` : ""} - ${subCommand.description}`);
|
||||
}
|
||||
} else {
|
||||
if (!categories[category]) categories[category] = [];
|
||||
categories[category].push(`**${command}**${params ? ` ${params.join(" ")}` : ""} - ${description}`);
|
||||
}
|
||||
}
|
||||
generated = true;
|
||||
}
|
||||
|
||||
export async function createPage(output) {
|
||||
let template = `# <img src="https://raw.githubusercontent.com/esmBot/esmBot/master/docs/assets/esmbot.png" width="64"> esmBot${process.env.NODE_ENV === "development" ? " Dev" : ""} Command List
|
||||
|
||||
This page was last generated on \`${new Date().toString()}\`.
|
||||
|
||||
\`[]\` means an argument is required, \`{}\` means an argument is optional.
|
||||
|
||||
**Want to help support esmBot's development? Consider donating on Patreon!** https://patreon.com/TheEssem
|
||||
`;
|
||||
|
||||
template += "\n## Table of Contents\n";
|
||||
for (const category of Object.keys(categories)) {
|
||||
const categoryStringArray = category.split("-");
|
||||
for (const index of categoryStringArray.keys()) {
|
||||
categoryStringArray[index] = categoryStringArray[index].charAt(0).toUpperCase() + categoryStringArray[index].slice(1);
|
||||
}
|
||||
template += `+ [**${categoryStringArray.join(" ")}**](#${category})\n`;
|
||||
}
|
||||
|
||||
// hell
|
||||
for (const category of Object.keys(categories)) {
|
||||
const categoryStringArray = category.split("-");
|
||||
for (const index of categoryStringArray.keys()) {
|
||||
categoryStringArray[index] = categoryStringArray[index].charAt(0).toUpperCase() + categoryStringArray[index].slice(1);
|
||||
}
|
||||
template += `\n## ${categoryStringArray.join(" ")}\n`;
|
||||
for (const command of categories[category]) {
|
||||
if (command.startsWith(">")) {
|
||||
template += `${command}\n`;
|
||||
} else {
|
||||
template += `+ ${command}\n`;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
await promises.writeFile(output, template);
|
||||
}
|
|
@ -1,73 +1,73 @@
|
|||
import { createRequire } from "module";
|
||||
import { isMainThread, parentPort, workerData } from "worker_threads";
|
||||
import { request } from "undici";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const nodeRequire = createRequire(import.meta.url);
|
||||
|
||||
const relPath = `../build/${process.env.DEBUG && process.env.DEBUG === "true" ? "Debug" : "Release"}/image.node`;
|
||||
const img = nodeRequire(relPath);
|
||||
|
||||
const enumMap = {
|
||||
"forget": 0,
|
||||
"northwest": 1,
|
||||
"north": 2,
|
||||
"northeast": 3,
|
||||
"west": 4,
|
||||
"center": 5,
|
||||
"east": 6,
|
||||
"southwest": 7,
|
||||
"south": 8,
|
||||
"southeast": 9
|
||||
};
|
||||
|
||||
export default function run(object) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// If the image has a path, it must also have a type
|
||||
let promise = Promise.resolve();
|
||||
if (object.path) {
|
||||
if (object.params.type !== "image/gif" && object.onlyGIF) resolve({
|
||||
buffer: Buffer.alloc(0),
|
||||
fileExtension: "nogif"
|
||||
});
|
||||
promise = request(object.path).then(res => res.body.arrayBuffer()).then(buf => Buffer.from(buf));
|
||||
}
|
||||
// Convert from a MIME type (e.g. "image/png") to something the image processor understands (e.g. "png").
|
||||
// Don't set `type` directly on the object we are passed as it will be read afterwards.
|
||||
// If no image type is given (say, the command generates its own image), make it a PNG.
|
||||
const fileExtension = object.params.type ? object.params.type.split("/")[1] : "png";
|
||||
promise.then(buf => {
|
||||
if (buf) object.params.data = buf;
|
||||
const objectWithFixedType = Object.assign({}, object.params, { type: fileExtension });
|
||||
if (objectWithFixedType.gravity) {
|
||||
if (isNaN(objectWithFixedType.gravity)) {
|
||||
objectWithFixedType.gravity = enumMap[objectWithFixedType.gravity];
|
||||
}
|
||||
}
|
||||
objectWithFixedType.basePath = path.join(path.dirname(fileURLToPath(import.meta.url)), "../");
|
||||
try {
|
||||
const result = img.image(object.cmd, objectWithFixedType);
|
||||
const returnObject = {
|
||||
buffer: result.data,
|
||||
fileExtension: result.type
|
||||
};
|
||||
resolve(returnObject);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!isMainThread) {
|
||||
run(workerData)
|
||||
.then(returnObject => {
|
||||
parentPort.postMessage(returnObject);
|
||||
process.exit();
|
||||
})
|
||||
.catch(err => {
|
||||
// turn promise rejection into normal error
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
import { createRequire } from "module";
|
||||
import { isMainThread, parentPort, workerData } from "worker_threads";
|
||||
import { request } from "undici";
|
||||
import path from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
|
||||
const nodeRequire = createRequire(import.meta.url);
|
||||
|
||||
const relPath = `../build/${process.env.DEBUG && process.env.DEBUG === "true" ? "Debug" : "Release"}/image.node`;
|
||||
const img = nodeRequire(relPath);
|
||||
|
||||
const enumMap = {
|
||||
"forget": 0,
|
||||
"northwest": 1,
|
||||
"north": 2,
|
||||
"northeast": 3,
|
||||
"west": 4,
|
||||
"center": 5,
|
||||
"east": 6,
|
||||
"southwest": 7,
|
||||
"south": 8,
|
||||
"southeast": 9
|
||||
};
|
||||
|
||||
export default function run(object) {
|
||||
return new Promise((resolve, reject) => {
|
||||
// If the image has a path, it must also have a type
|
||||
let promise = Promise.resolve();
|
||||
if (object.path) {
|
||||
if (object.params.type !== "image/gif" && object.onlyGIF) resolve({
|
||||
buffer: Buffer.alloc(0),
|
||||
fileExtension: "nogif"
|
||||
});
|
||||
promise = request(object.path).then(res => res.body.arrayBuffer()).then(buf => Buffer.from(buf));
|
||||
}
|
||||
// Convert from a MIME type (e.g. "image/png") to something the image processor understands (e.g. "png").
|
||||
// Don't set `type` directly on the object we are passed as it will be read afterwards.
|
||||
// If no image type is given (say, the command generates its own image), make it a PNG.
|
||||
const fileExtension = object.params.type ? object.params.type.split("/")[1] : "png";
|
||||
promise.then(buf => {
|
||||
if (buf) object.params.data = buf;
|
||||
const objectWithFixedType = Object.assign({}, object.params, { type: fileExtension });
|
||||
if (objectWithFixedType.gravity) {
|
||||
if (isNaN(objectWithFixedType.gravity)) {
|
||||
objectWithFixedType.gravity = enumMap[objectWithFixedType.gravity];
|
||||
}
|
||||
}
|
||||
objectWithFixedType.basePath = path.join(path.dirname(fileURLToPath(import.meta.url)), "../");
|
||||
try {
|
||||
const result = img.image(object.cmd, objectWithFixedType);
|
||||
const returnObject = {
|
||||
buffer: result.data,
|
||||
fileExtension: result.type
|
||||
};
|
||||
resolve(returnObject);
|
||||
} catch (e) {
|
||||
reject(e);
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (!isMainThread) {
|
||||
run(workerData)
|
||||
.then(returnObject => {
|
||||
parentPort.postMessage(returnObject);
|
||||
process.exit();
|
||||
})
|
||||
.catch(err => {
|
||||
// turn promise rejection into normal error
|
||||
throw err;
|
||||
});
|
||||
}
|
||||
|
|
338
utils/image.js
338
utils/image.js
|
@ -1,170 +1,170 @@
|
|||
import { request } from "undici";
|
||||
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 * 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"];
|
||||
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 async function getType(image, extraReturnTypes) {
|
||||
if (!image.startsWith("http")) {
|
||||
const imageType = await fileTypeFromFile(image);
|
||||
if (imageType && formats.includes(imageType.mime)) {
|
||||
return imageType.mime;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
let type;
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => {
|
||||
controller.abort();
|
||||
}, 3000);
|
||||
try {
|
||||
const imageRequest = await request(image, {
|
||||
signal: controller.signal,
|
||||
method: "HEAD"
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
const size = imageRequest.headers["content-range"] ? imageRequest.headers["content-range"].split("/")[1] : imageRequest.headers["content-length"];
|
||||
if (parseInt(size) > 26214400 && extraReturnTypes) { // 25 MB
|
||||
type = "large";
|
||||
return type;
|
||||
}
|
||||
const typeHeader = imageRequest.headers["content-type"];
|
||||
if (typeHeader) {
|
||||
type = typeHeader;
|
||||
} else {
|
||||
const timeout = setTimeout(() => {
|
||||
controller.abort();
|
||||
}, 3000);
|
||||
const bufRequest = await request(image, {
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
range: "bytes=0-1023"
|
||||
}
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
const imageBuffer = await bufRequest.body.arrayBuffer();
|
||||
const imageType = await fileTypeFromBuffer(imageBuffer);
|
||||
if (imageType && formats.includes(imageType.mime)) {
|
||||
type = imageType.mime;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name === "AbortError") {
|
||||
throw Error("Timed out");
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
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);
|
||||
}
|
||||
import { request } from "undici";
|
||||
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 * 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"];
|
||||
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 async function getType(image, extraReturnTypes) {
|
||||
if (!image.startsWith("http")) {
|
||||
const imageType = await fileTypeFromFile(image);
|
||||
if (imageType && formats.includes(imageType.mime)) {
|
||||
return imageType.mime;
|
||||
}
|
||||
return undefined;
|
||||
}
|
||||
let type;
|
||||
const controller = new AbortController();
|
||||
const timeout = setTimeout(() => {
|
||||
controller.abort();
|
||||
}, 3000);
|
||||
try {
|
||||
const imageRequest = await request(image, {
|
||||
signal: controller.signal,
|
||||
method: "HEAD"
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
const size = imageRequest.headers["content-range"] ? imageRequest.headers["content-range"].split("/")[1] : imageRequest.headers["content-length"];
|
||||
if (parseInt(size) > 26214400 && extraReturnTypes) { // 25 MB
|
||||
type = "large";
|
||||
return type;
|
||||
}
|
||||
const typeHeader = imageRequest.headers["content-type"];
|
||||
if (typeHeader) {
|
||||
type = typeHeader;
|
||||
} else {
|
||||
const timeout = setTimeout(() => {
|
||||
controller.abort();
|
||||
}, 3000);
|
||||
const bufRequest = await request(image, {
|
||||
signal: controller.signal,
|
||||
headers: {
|
||||
range: "bytes=0-1023"
|
||||
}
|
||||
});
|
||||
clearTimeout(timeout);
|
||||
const imageBuffer = await bufRequest.body.arrayBuffer();
|
||||
const imageType = await fileTypeFromBuffer(imageBuffer);
|
||||
if (imageType && formats.includes(imageType.mime)) {
|
||||
type = imageType.mime;
|
||||
}
|
||||
}
|
||||
} catch (error) {
|
||||
if (error.name === "AbortError") {
|
||||
throw Error("Timed out");
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
} finally {
|
||||
clearTimeout(timeout);
|
||||
}
|
||||
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);
|
||||
}
|
||||
}
|
|
@ -1,171 +1,171 @@
|
|||
import { request } from "undici";
|
||||
import WebSocket from "ws";
|
||||
import * as logger from "./logger.js";
|
||||
import { setTimeout } from "timers/promises";
|
||||
|
||||
const Rerror = 0x01;
|
||||
const Tqueue = 0x02;
|
||||
const Rqueue = 0x03;
|
||||
const Tcancel = 0x04;
|
||||
const Rcancel = 0x05;
|
||||
const Twait = 0x06;
|
||||
const Rwait = 0x07;
|
||||
const Rinit = 0x08;
|
||||
|
||||
class ImageConnection {
|
||||
constructor(host, auth, tls = false) {
|
||||
this.requests = new Map();
|
||||
if (!host.includes(":")) {
|
||||
host += ":3762";
|
||||
}
|
||||
this.host = host;
|
||||
this.auth = auth;
|
||||
this.tag = 0;
|
||||
this.disconnected = false;
|
||||
this.formats = {};
|
||||
this.wsproto = null;
|
||||
if (tls) {
|
||||
this.wsproto = "wss";
|
||||
} else {
|
||||
this.wsproto = "ws";
|
||||
}
|
||||
this.sockurl = `${this.wsproto}://${host}/sock`;
|
||||
const headers = {};
|
||||
if (auth) {
|
||||
headers.Authentication = auth;
|
||||
}
|
||||
this.conn = new WebSocket(this.sockurl, { headers });
|
||||
let httpproto;
|
||||
if (tls) {
|
||||
httpproto = "https";
|
||||
} else {
|
||||
httpproto = "http";
|
||||
}
|
||||
this.httpurl = `${httpproto}://${host}`;
|
||||
this.conn.on("message", (msg) => this.onMessage(msg));
|
||||
this.conn.once("error", (err) => this.onError(err));
|
||||
this.conn.once("close", () => this.onClose());
|
||||
}
|
||||
|
||||
async onMessage(msg) {
|
||||
const op = msg.readUint8(0);
|
||||
if (op === Rinit) {
|
||||
this.formats = JSON.parse(msg.toString("utf8", 7));
|
||||
return;
|
||||
}
|
||||
const tag = msg.readUint16LE(1);
|
||||
const promise = this.requests.get(tag);
|
||||
if (!promise) {
|
||||
logger.error(`Received response for unknown request ${tag}`);
|
||||
return;
|
||||
}
|
||||
this.requests.delete(tag);
|
||||
if (op === Rerror) {
|
||||
promise.reject(new Error(msg.slice(3, msg.length).toString()));
|
||||
return;
|
||||
}
|
||||
promise.resolve();
|
||||
}
|
||||
|
||||
onError(e) {
|
||||
logger.error(e.toString());
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
for (const [tag, obj] of this.requests.entries()) {
|
||||
obj.reject("Request ended prematurely due to a closed connection");
|
||||
this.requests.delete(tag);
|
||||
}
|
||||
if (!this.disconnected) {
|
||||
logger.warn(`Lost connection to ${this.host}, attempting to reconnect in 5 seconds...`);
|
||||
await setTimeout(5000);
|
||||
this.conn = new WebSocket(this.sockurl, {
|
||||
headers: {
|
||||
"Authentication": this.auth
|
||||
}
|
||||
});
|
||||
this.conn.on("message", (msg) => this.onMessage(msg));
|
||||
this.conn.once("error", (err) => this.onError(err));
|
||||
this.conn.once("close", () => this.onClose());
|
||||
}
|
||||
this.disconnected = false;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.disconnected = true;
|
||||
this.conn.close();
|
||||
}
|
||||
|
||||
queue(jobid, jobobj) {
|
||||
const str = JSON.stringify(jobobj);
|
||||
const buf = Buffer.alloc(8);
|
||||
buf.writeBigUint64LE(jobid);
|
||||
return this.do(Tqueue, jobid, Buffer.concat([buf, Buffer.from(str)]));
|
||||
}
|
||||
|
||||
wait(jobid) {
|
||||
const buf = Buffer.alloc(8);
|
||||
buf.writeBigUint64LE(jobid);
|
||||
return this.do(Twait, jobid, buf);
|
||||
}
|
||||
|
||||
cancel(jobid) {
|
||||
const buf = Buffer.alloc(8);
|
||||
buf.writeBigUint64LE(jobid);
|
||||
return this.do(Tcancel, jobid, buf);
|
||||
}
|
||||
|
||||
async getOutput(jobid) {
|
||||
const req = await request(`${this.httpurl}/image?id=${jobid}`, {
|
||||
headers: {
|
||||
authentication: this.auth || undefined
|
||||
}
|
||||
});
|
||||
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;
|
||||
}
|
||||
return { buffer: Buffer.from(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) {
|
||||
const buf = Buffer.alloc(1 + 2);
|
||||
let tag = this.tag++;
|
||||
if (tag > 65535) tag = this.tag = 0;
|
||||
buf.writeUint8(op);
|
||||
buf.writeUint16LE(tag, 1);
|
||||
this.conn.send(Buffer.concat([buf, data]));
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
this.requests.set(tag, { resolve, reject, id, op });
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
export default ImageConnection;
|
||||
import { request } from "undici";
|
||||
import WebSocket from "ws";
|
||||
import * as logger from "./logger.js";
|
||||
import { setTimeout } from "timers/promises";
|
||||
|
||||
const Rerror = 0x01;
|
||||
const Tqueue = 0x02;
|
||||
const Rqueue = 0x03;
|
||||
const Tcancel = 0x04;
|
||||
const Rcancel = 0x05;
|
||||
const Twait = 0x06;
|
||||
const Rwait = 0x07;
|
||||
const Rinit = 0x08;
|
||||
|
||||
class ImageConnection {
|
||||
constructor(host, auth, tls = false) {
|
||||
this.requests = new Map();
|
||||
if (!host.includes(":")) {
|
||||
host += ":3762";
|
||||
}
|
||||
this.host = host;
|
||||
this.auth = auth;
|
||||
this.tag = 0;
|
||||
this.disconnected = false;
|
||||
this.formats = {};
|
||||
this.wsproto = null;
|
||||
if (tls) {
|
||||
this.wsproto = "wss";
|
||||
} else {
|
||||
this.wsproto = "ws";
|
||||
}
|
||||
this.sockurl = `${this.wsproto}://${host}/sock`;
|
||||
const headers = {};
|
||||
if (auth) {
|
||||
headers.Authentication = auth;
|
||||
}
|
||||
this.conn = new WebSocket(this.sockurl, { headers });
|
||||
let httpproto;
|
||||
if (tls) {
|
||||
httpproto = "https";
|
||||
} else {
|
||||
httpproto = "http";
|
||||
}
|
||||
this.httpurl = `${httpproto}://${host}`;
|
||||
this.conn.on("message", (msg) => this.onMessage(msg));
|
||||
this.conn.once("error", (err) => this.onError(err));
|
||||
this.conn.once("close", () => this.onClose());
|
||||
}
|
||||
|
||||
async onMessage(msg) {
|
||||
const op = msg.readUint8(0);
|
||||
if (op === Rinit) {
|
||||
this.formats = JSON.parse(msg.toString("utf8", 7));
|
||||
return;
|
||||
}
|
||||
const tag = msg.readUint16LE(1);
|
||||
const promise = this.requests.get(tag);
|
||||
if (!promise) {
|
||||
logger.error(`Received response for unknown request ${tag}`);
|
||||
return;
|
||||
}
|
||||
this.requests.delete(tag);
|
||||
if (op === Rerror) {
|
||||
promise.reject(new Error(msg.slice(3, msg.length).toString()));
|
||||
return;
|
||||
}
|
||||
promise.resolve();
|
||||
}
|
||||
|
||||
onError(e) {
|
||||
logger.error(e.toString());
|
||||
}
|
||||
|
||||
async onClose() {
|
||||
for (const [tag, obj] of this.requests.entries()) {
|
||||
obj.reject("Request ended prematurely due to a closed connection");
|
||||
this.requests.delete(tag);
|
||||
}
|
||||
if (!this.disconnected) {
|
||||
logger.warn(`Lost connection to ${this.host}, attempting to reconnect in 5 seconds...`);
|
||||
await setTimeout(5000);
|
||||
this.conn = new WebSocket(this.sockurl, {
|
||||
headers: {
|
||||
"Authentication": this.auth
|
||||
}
|
||||
});
|
||||
this.conn.on("message", (msg) => this.onMessage(msg));
|
||||
this.conn.once("error", (err) => this.onError(err));
|
||||
this.conn.once("close", () => this.onClose());
|
||||
}
|
||||
this.disconnected = false;
|
||||
}
|
||||
|
||||
close() {
|
||||
this.disconnected = true;
|
||||
this.conn.close();
|
||||
}
|
||||
|
||||
queue(jobid, jobobj) {
|
||||
const str = JSON.stringify(jobobj);
|
||||
const buf = Buffer.alloc(8);
|
||||
buf.writeBigUint64LE(jobid);
|
||||
return this.do(Tqueue, jobid, Buffer.concat([buf, Buffer.from(str)]));
|
||||
}
|
||||
|
||||
wait(jobid) {
|
||||
const buf = Buffer.alloc(8);
|
||||
buf.writeBigUint64LE(jobid);
|
||||
return this.do(Twait, jobid, buf);
|
||||
}
|
||||
|
||||
cancel(jobid) {
|
||||
const buf = Buffer.alloc(8);
|
||||
buf.writeBigUint64LE(jobid);
|
||||
return this.do(Tcancel, jobid, buf);
|
||||
}
|
||||
|
||||
async getOutput(jobid) {
|
||||
const req = await request(`${this.httpurl}/image?id=${jobid}`, {
|
||||
headers: {
|
||||
authentication: this.auth || undefined
|
||||
}
|
||||
});
|
||||
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;
|
||||
}
|
||||
return { buffer: Buffer.from(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) {
|
||||
const buf = Buffer.alloc(1 + 2);
|
||||
let tag = this.tag++;
|
||||
if (tag > 65535) tag = this.tag = 0;
|
||||
buf.writeUint8(op);
|
||||
buf.writeUint16LE(tag, 1);
|
||||
this.conn.send(Buffer.concat([buf, data]));
|
||||
const promise = new Promise((resolve, reject) => {
|
||||
this.requests.set(tag, { resolve, reject, id, op });
|
||||
});
|
||||
return promise;
|
||||
}
|
||||
}
|
||||
|
||||
export default ImageConnection;
|
||||
|
|
|
@ -1,189 +1,190 @@
|
|||
import { request } from "undici";
|
||||
import { getType } from "./image.js";
|
||||
|
||||
const tenorURLs = [
|
||||
"tenor.com",
|
||||
"www.tenor.com"
|
||||
];
|
||||
const giphyURLs = [
|
||||
"giphy.com",
|
||||
"www.giphy.com",
|
||||
"i.giphy.com"
|
||||
];
|
||||
const giphyMediaURLs = [ // there could be more of these
|
||||
"media.giphy.com",
|
||||
"media0.giphy.com",
|
||||
"media1.giphy.com",
|
||||
"media2.giphy.com",
|
||||
"media3.giphy.com",
|
||||
"media4.giphy.com"
|
||||
];
|
||||
const imgurURLs = [
|
||||
"imgur.com",
|
||||
"www.imgur.com",
|
||||
"i.imgur.com"
|
||||
];
|
||||
const gfycatURLs = [
|
||||
"gfycat.com",
|
||||
"www.gfycat.com",
|
||||
"thumbs.gfycat.com",
|
||||
"giant.gfycat.com"
|
||||
];
|
||||
|
||||
const combined = [...tenorURLs, ...giphyURLs, ...giphyMediaURLs, ...imgurURLs, ...gfycatURLs];
|
||||
|
||||
const imageFormats = ["image/jpeg", "image/png", "image/webp", "image/gif", "large"];
|
||||
const videoFormats = ["video/mp4", "video/webm", "video/mov"];
|
||||
|
||||
// gets the proper image paths
|
||||
const getImage = async (image, image2, video, extraReturnTypes, gifv = false, type = null, link = false) => {
|
||||
try {
|
||||
const fileNameSplit = new URL(image).pathname.split("/");
|
||||
const fileName = fileNameSplit[fileNameSplit.length - 1];
|
||||
const fileNameNoExtension = fileName.slice(0, fileName.lastIndexOf("."));
|
||||
const payload = {
|
||||
url: image2,
|
||||
path: image,
|
||||
name: fileNameNoExtension
|
||||
};
|
||||
const host = new URL(image2).host;
|
||||
if (gifv || (link && combined.includes(host))) {
|
||||
if (tenorURLs.includes(host)) {
|
||||
// Tenor doesn't let us access a raw GIF without going through their API,
|
||||
// so we use that if there's a key in the config
|
||||
if (process.env.TENOR !== "") {
|
||||
let id;
|
||||
if (image2.includes("tenor.com/view/")) {
|
||||
id = image2.split("-").pop();
|
||||
} else if (image2.endsWith(".gif")) {
|
||||
const redirect = (await request(image2, { method: "HEAD" })).headers.location;
|
||||
id = redirect.split("-").pop();
|
||||
}
|
||||
const data = await request(`https://tenor.googleapis.com/v2/posts?ids=${id}&media_filter=gif&limit=1&client_key=esmBot%20${process.env.ESMBOT_VER}&key=${process.env.TENOR}`);
|
||||
if (data.statusCode === 429) {
|
||||
if (extraReturnTypes) {
|
||||
payload.type = "tenorlimit";
|
||||
return payload;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const json = await data.body.json();
|
||||
if (json.error) throw Error(json.error.message);
|
||||
payload.path = json.results[0].media_formats.gif.url;
|
||||
}
|
||||
} else if (giphyURLs.includes(host)) {
|
||||
// Can result in an HTML page instead of a GIF
|
||||
payload.path = `https://media0.giphy.com/media/${image2.split("/")[4].split("-").pop()}/giphy.gif`;
|
||||
} else if (giphyMediaURLs.includes(host)) {
|
||||
payload.path = `https://media0.giphy.com/media/${image2.split("/")[4]}/giphy.gif`;
|
||||
} else if (imgurURLs.includes(host)) {
|
||||
// Seems that Imgur has a possibility of making GIFs static
|
||||
payload.path = image.replace(".mp4", ".gif");
|
||||
} else if (gfycatURLs.includes(host)) {
|
||||
// iirc Gfycat also seems to sometimes make GIFs static
|
||||
if (link) {
|
||||
const data = await request(`https://api.gfycat.com/v1/gfycats/${image.split("/").pop().split(".mp4")[0]}`);
|
||||
const json = await data.body.json();
|
||||
if (json.errorMessage) throw Error(json.errorMessage);
|
||||
payload.path = json.gfyItem.gifUrl;
|
||||
} else {
|
||||
payload.path = `https://thumbs.gfycat.com/${image.split("/").pop().split(".mp4")[0]}-size_restricted.gif`;
|
||||
}
|
||||
}
|
||||
payload.type = "image/gif";
|
||||
} else if (video) {
|
||||
payload.type = type ?? await getType(payload.path, extraReturnTypes);
|
||||
if (!payload.type || (!videoFormats.includes(payload.type) && !imageFormats.includes(payload.type))) return;
|
||||
} else {
|
||||
payload.type = type ?? await getType(payload.path, extraReturnTypes);
|
||||
if (!payload.type || !imageFormats.includes(payload.type)) return;
|
||||
}
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error.name === "AbortError") {
|
||||
throw Error("Timed out");
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const checkImages = async (message, extraReturnTypes, video, sticker) => {
|
||||
let type;
|
||||
if (sticker && message.stickerItems) {
|
||||
type = message.stickerItems[0];
|
||||
} else {
|
||||
// first check the embeds
|
||||
if (message.embeds.length !== 0) {
|
||||
// embeds can vary in types, we check for tenor gifs first
|
||||
if (message.embeds[0].type === "gifv") {
|
||||
type = await getImage(message.embeds[0].video.url, message.embeds[0].url, video, extraReturnTypes, true);
|
||||
// then we check for other image types
|
||||
} else if ((message.embeds[0].type === "video" || message.embeds[0].type === "image") && message.embeds[0].thumbnail) {
|
||||
type = await getImage(message.embeds[0].thumbnail.proxyURL, message.embeds[0].thumbnail.url, video, extraReturnTypes);
|
||||
// finally we check both possible image fields for "generic" embeds
|
||||
} else if (message.embeds[0].type === "rich" || message.embeds[0].type === "article") {
|
||||
if (message.embeds[0].thumbnail) {
|
||||
type = await getImage(message.embeds[0].thumbnail.proxyURL, message.embeds[0].thumbnail.url, video, extraReturnTypes);
|
||||
} else if (message.embeds[0].image) {
|
||||
type = await getImage(message.embeds[0].image.proxyURL, message.embeds[0].image.url, video, extraReturnTypes);
|
||||
}
|
||||
}
|
||||
// then check the attachments
|
||||
} else if (message.attachments.size !== 0 && message.attachments.first().width) {
|
||||
type = await getImage(message.attachments.first().proxyURL, message.attachments.first().url, video);
|
||||
}
|
||||
}
|
||||
// if the return value exists then return it
|
||||
return type ?? false;
|
||||
};
|
||||
|
||||
// this checks for the latest message containing an image and returns the url of the image
|
||||
export default async (client, cmdMessage, interaction, options, extraReturnTypes = false, video = false, sticker = false, singleMessage = false) => {
|
||||
// we start by determining whether or not we're dealing with an interaction or a message
|
||||
if (interaction) {
|
||||
// we can get a raw attachment or a URL in the interaction itself
|
||||
if (options) {
|
||||
if (options.image) {
|
||||
const attachment = interaction.data.resolved.attachments.get(options.image);
|
||||
const result = await getImage(attachment.proxyURL, attachment.url, video, attachment.contentType);
|
||||
if (result !== false) return result;
|
||||
} else if (options.link) {
|
||||
const result = await getImage(options.link, options.link, video, extraReturnTypes, false, null, true);
|
||||
if (result !== false) return result;
|
||||
}
|
||||
}
|
||||
}
|
||||
if (cmdMessage) {
|
||||
// check if the message is a reply to another message
|
||||
if (cmdMessage.messageReference && !singleMessage) {
|
||||
const replyMessage = await client.rest.channels.getMessage(cmdMessage.messageReference.channelID, cmdMessage.messageReference.messageID).catch(() => undefined);
|
||||
if (replyMessage) {
|
||||
const replyResult = await checkImages(replyMessage, extraReturnTypes, video, sticker);
|
||||
if (replyResult !== false) return replyResult;
|
||||
}
|
||||
}
|
||||
// then we check the current message
|
||||
const result = await checkImages(cmdMessage, extraReturnTypes, video, sticker);
|
||||
if (result !== false) return result;
|
||||
}
|
||||
if (!singleMessage) {
|
||||
// if there aren't any replies or interaction attachments then iterate over the last few messages in the channel
|
||||
try {
|
||||
const channel = (interaction ? interaction : cmdMessage).channel ?? await client.rest.channels.get((interaction ? interaction : cmdMessage).channelID);
|
||||
const messages = await channel.getMessages();
|
||||
// iterate over each message
|
||||
for (const message of messages) {
|
||||
const result = await checkImages(message, extraReturnTypes, video, sticker);
|
||||
if (result === false) {
|
||||
continue;
|
||||
} else {
|
||||
return result;
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
};
|
||||
import { request } from "undici";
|
||||
import { getType } from "./image.js";
|
||||
|
||||
const tenorURLs = [
|
||||
"tenor.com",
|
||||
"www.tenor.com"
|
||||
];
|
||||
const giphyURLs = [
|
||||
"giphy.com",
|
||||
"www.giphy.com",
|
||||
"i.giphy.com"
|
||||
];
|
||||
const giphyMediaURLs = [ // there could be more of these
|
||||
"media.giphy.com",
|
||||
"media0.giphy.com",
|
||||
"media1.giphy.com",
|
||||
"media2.giphy.com",
|
||||
"media3.giphy.com",
|
||||
"media4.giphy.com"
|
||||
];
|
||||
const imgurURLs = [
|
||||
"imgur.com",
|
||||
"www.imgur.com",
|
||||
"i.imgur.com"
|
||||
];
|
||||
const gfycatURLs = [
|
||||
"gfycat.com",
|
||||
"www.gfycat.com",
|
||||
"thumbs.gfycat.com",
|
||||
"giant.gfycat.com"
|
||||
];
|
||||
|
||||
const combined = [...tenorURLs, ...giphyURLs, ...giphyMediaURLs, ...imgurURLs, ...gfycatURLs];
|
||||
|
||||
const imageFormats = ["image/jpeg", "image/png", "image/webp", "image/gif", "large"];
|
||||
const videoFormats = ["video/mp4", "video/webm", "video/mov"];
|
||||
|
||||
// gets the proper image paths
|
||||
const getImage = async (image, image2, video, extraReturnTypes, gifv = false, type = null, link = false) => {
|
||||
try {
|
||||
const fileNameSplit = new URL(image).pathname.split("/");
|
||||
const fileName = fileNameSplit[fileNameSplit.length - 1];
|
||||
const fileNameNoExtension = fileName.slice(0, fileName.lastIndexOf("."));
|
||||
const payload = {
|
||||
url: image2,
|
||||
path: image,
|
||||
name: fileNameNoExtension
|
||||
};
|
||||
const host = new URL(image2).host;
|
||||
if (gifv || (link && combined.includes(host))) {
|
||||
if (tenorURLs.includes(host)) {
|
||||
// Tenor doesn't let us access a raw GIF without going through their API,
|
||||
// so we use that if there's a key in the config
|
||||
if (process.env.TENOR !== "") {
|
||||
let id;
|
||||
if (image2.includes("tenor.com/view/")) {
|
||||
id = image2.split("-").pop();
|
||||
} else if (image2.endsWith(".gif")) {
|
||||
const redirect = (await request(image2, { method: "HEAD" })).headers.location;
|
||||
id = redirect.split("-").pop();
|
||||
}
|
||||
const data = await request(`https://tenor.googleapis.com/v2/posts?ids=${id}&media_filter=gif&limit=1&client_key=esmBot%20${process.env.ESMBOT_VER}&key=${process.env.TENOR}`);
|
||||
if (data.statusCode === 429) {
|
||||
if (extraReturnTypes) {
|
||||
payload.type = "tenorlimit";
|
||||
return payload;
|
||||
} else {
|
||||
return;
|
||||
}
|
||||
}
|
||||
const json = await data.body.json();
|
||||
if (json.error) throw Error(json.error.message);
|
||||
payload.path = json.results[0].media_formats.gif.url;
|
||||
}
|
||||
} else if (giphyURLs.includes(host)) {
|
||||
// Can result in an HTML page instead of a GIF
|
||||
payload.path = `https://media0.giphy.com/media/${image2.split("/")[4].split("-").pop()}/giphy.gif`;
|
||||
} else if (giphyMediaURLs.includes(host)) {
|
||||
payload.path = `https://media0.giphy.com/media/${image2.split("/")[4]}/giphy.gif`;
|
||||
} else if (imgurURLs.includes(host)) {
|
||||
// Seems that Imgur has a possibility of making GIFs static
|
||||
payload.path = image.replace(".mp4", ".gif");
|
||||
} else if (gfycatURLs.includes(host)) {
|
||||
// iirc Gfycat also seems to sometimes make GIFs static
|
||||
if (link) {
|
||||
const data = await request(`https://api.gfycat.com/v1/gfycats/${image.split("/").pop().split(".mp4")[0]}`);
|
||||
const json = await data.body.json();
|
||||
if (json.errorMessage) throw Error(json.errorMessage);
|
||||
payload.path = json.gfyItem.gifUrl;
|
||||
} else {
|
||||
payload.path = `https://thumbs.gfycat.com/${image.split("/").pop().split(".mp4")[0]}-size_restricted.gif`;
|
||||
}
|
||||
}
|
||||
payload.type = "image/gif";
|
||||
} else if (video) {
|
||||
payload.type = type ?? await getType(payload.path, extraReturnTypes);
|
||||
if (!payload.type || (!videoFormats.includes(payload.type) && !imageFormats.includes(payload.type))) return;
|
||||
} else {
|
||||
payload.type = type ?? await getType(payload.path, extraReturnTypes);
|
||||
if (!payload.type || !imageFormats.includes(payload.type)) return;
|
||||
}
|
||||
return payload;
|
||||
} catch (error) {
|
||||
if (error.name === "AbortError") {
|
||||
throw Error("Timed out");
|
||||
} else {
|
||||
throw error;
|
||||
}
|
||||
}
|
||||
};
|
||||
|
||||
const urlFromMxc = async (mxcUri) => {
|
||||
const stripped = mxcUri.replace("mxc://", "")
|
||||
return process.env.MATRIX_BASEURL+"/_matrix/media/r0/download/"+stripped
|
||||
}
|
||||
|
||||
const checkImages = async (message, extraReturnTypes, video, sticker) => {
|
||||
let type;
|
||||
// console.log(message)
|
||||
if (typeof message.content.info !== undefined) {
|
||||
if (message.content.msgtype == "m.image") {
|
||||
const url = await urlFromMxc(message.content.url)
|
||||
const fileNameNoExtension = message.content.body.slice(0, message.content.body.lastIndexOf("."));
|
||||
type = {name: fileNameNoExtension, path: url, url: url, type: message.content.info.mimetype}
|
||||
}
|
||||
}
|
||||
// // first check the embeds
|
||||
// if (message.embeds.length !== 0) {
|
||||
// // embeds can vary in types, we check for tenor gifs first
|
||||
// if (message.embeds[0].type === "gifv") {
|
||||
// type = await getImage(message.embeds[0].video.url, message.embeds[0].url, video, extraReturnTypes, true);
|
||||
// // then we check for other image types
|
||||
// } else if ((message.embeds[0].type === "video" || message.embeds[0].type === "image") && message.embeds[0].thumbnail) {
|
||||
// type = await getImage(message.embeds[0].thumbnail.proxyURL, message.embeds[0].thumbnail.url, video, extraReturnTypes);
|
||||
// // finally we check both possible image fields for "generic" embeds
|
||||
// } else if (message.embeds[0].type === "rich" || message.embeds[0].type === "article") {
|
||||
// if (message.embeds[0].thumbnail) {
|
||||
// type = await getImage(message.embeds[0].thumbnail.proxyURL, message.embeds[0].thumbnail.url, video, extraReturnTypes);
|
||||
// } else if (message.embeds[0].image) {
|
||||
// type = await getImage(message.embeds[0].image.proxyURL, message.embeds[0].image.url, video, extraReturnTypes);
|
||||
// }
|
||||
// }
|
||||
// // then check the attachments
|
||||
// } else if (message.attachments.size !== 0 && message.attachments.first().width) {
|
||||
// type = await getImage(message.attachments.first().proxyURL, message.attachments.first().url, video);
|
||||
// }
|
||||
// if the return value exists then return it
|
||||
return type ?? false;
|
||||
};
|
||||
|
||||
// this checks for the latest message containing an image and returns the url of the image
|
||||
export default async (client, cmdMessage, interaction, options, extraReturnTypes = false, video = false, sticker = false, singleMessage = false) => {
|
||||
// we start by determining whether or not we're dealing with an interaction or a message
|
||||
if (cmdMessage) {
|
||||
// console.log(cmdMessage)
|
||||
// let channel = await client.getRoom(cmdMessage.room_id);
|
||||
// console.log(channel)
|
||||
// check if the message is a reply to another message
|
||||
// console.log(cmdMessage.content['m.relates_to'])
|
||||
if (cmdMessage.content['m.relates_to'] !== undefined) {
|
||||
const replyMessage = await client.fetchRoomEvent(cmdMessage.room_id, cmdMessage.content['m.relates_to']['m.in_reply_to'].event_id)
|
||||
// console.log(replyMessage)
|
||||
if (replyMessage) {
|
||||
const replyResult = await checkImages(replyMessage, extraReturnTypes, video, sticker);
|
||||
if (replyResult !== false) return replyResult;
|
||||
}
|
||||
}
|
||||
// then we check the current message
|
||||
const result = await checkImages(cmdMessage, extraReturnTypes, video, sticker);
|
||||
if (result !== false) return result;
|
||||
}
|
||||
// if (!singleMessage) {
|
||||
// // if there aren't any replies or interaction attachments then iterate over the last few messages in the channel
|
||||
// try {
|
||||
// const channel = (interaction ? interaction : cmdMessage).channel ?? await client.rest.channels.get((interaction ? interaction : cmdMessage).channelID);
|
||||
// const messages = await channel.getMessages();
|
||||
// // iterate over each message
|
||||
// for (const message of messages) {
|
||||
// const result = await checkImages(message, extraReturnTypes, video, sticker);
|
||||
// if (result === false) {
|
||||
// continue;
|
||||
// } else {
|
||||
// return result;
|
||||
// }
|
||||
// }
|
||||
// } catch {
|
||||
// // no-op
|
||||
// }
|
||||
// }
|
||||
};
|
||||
|
|
|
@ -1,44 +1,44 @@
|
|||
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 warn(...args) { return log("warn", ...args); }
|
||||
|
||||
export function debug(...args) { return log("debug", ...args); }
|
||||
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 warn(...args) { return log("warn", ...args); }
|
||||
|
||||
export function debug(...args) { return log("debug", ...args); }
|
||||
|
|
325
utils/misc.js
325
utils/misc.js
|
@ -1,163 +1,164 @@
|
|||
import util from "util";
|
||||
import fs from "fs";
|
||||
import pm2 from "pm2";
|
||||
import { config } from "dotenv";
|
||||
import db from "./database.js";
|
||||
|
||||
// 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
|
||||
export function random(array) {
|
||||
if (!array || array.length < 1) return null;
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
||||
|
||||
const optionalReplace = (token) => {
|
||||
return token === undefined || token === "" ? "" : (token === "true" || token === "false" ? token : "<redacted>");
|
||||
};
|
||||
|
||||
// clean(text) to clean message of any private info or mentions
|
||||
export function clean(text) {
|
||||
if (typeof text !== "string")
|
||||
text = util.inspect(text, { depth: 1 });
|
||||
|
||||
text = text
|
||||
.replaceAll("`", `\`${String.fromCharCode(8203)}`)
|
||||
.replaceAll("@", `@${String.fromCharCode(8203)}`);
|
||||
|
||||
let { parsed } = config();
|
||||
if (!parsed) parsed = process.env;
|
||||
const imageServers = JSON.parse(fs.readFileSync(new URL("../config/servers.json", import.meta.url), { encoding: "utf8" })).image;
|
||||
|
||||
if (imageServers?.length !== 0) {
|
||||
for (const { server, auth } of imageServers) {
|
||||
text = text.replaceAll(server, optionalReplace(server));
|
||||
text = text.replaceAll(auth, optionalReplace(auth));
|
||||
}
|
||||
}
|
||||
|
||||
for (const env of Object.keys(parsed)) {
|
||||
text = text.replaceAll(parsed[env], optionalReplace(parsed[env]));
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
// textEncode(string) to encode characters for image processing
|
||||
export function textEncode(string) {
|
||||
return string.replaceAll("&", "&").replaceAll(">", ">").replaceAll("<", "<").replaceAll("\"", """).replaceAll("'", "'").replaceAll("\\n", "\n").replaceAll("\\:", ":").replaceAll("\\,", ",");
|
||||
}
|
||||
|
||||
// set activity (a.k.a. the gamer code)
|
||||
export async function activityChanger(bot) {
|
||||
if (!broadcast) {
|
||||
await bot.editStatus("dnd", [{
|
||||
type: 0,
|
||||
name: random(messages) + (types.classic ? ` | @${bot.user.username} help` : "")
|
||||
}]);
|
||||
}
|
||||
setTimeout(() => activityChanger(bot), 900000);
|
||||
}
|
||||
|
||||
export async function checkBroadcast(bot) {
|
||||
if (!db) {
|
||||
return;
|
||||
}
|
||||
const message = await db.getBroadcast();
|
||||
if (message) {
|
||||
startBroadcast(bot, message);
|
||||
}
|
||||
}
|
||||
|
||||
export function startBroadcast(bot, message) {
|
||||
bot.editStatus("dnd", [{
|
||||
type: 0,
|
||||
name: message + (types.classic ? ` | @${bot.user.username} help` : "")
|
||||
}]);
|
||||
broadcast = true;
|
||||
}
|
||||
|
||||
export function endBroadcast(bot) {
|
||||
bot.editStatus("dnd", [{
|
||||
type: 0,
|
||||
name: random(messages) + (types.classic ? ` | @${bot.user.username} help` : "")
|
||||
}]);
|
||||
broadcast = false;
|
||||
}
|
||||
|
||||
export function getServers(bot) {
|
||||
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.list((err, list) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
const managerProc = list.filter((v) => v.name === "esmBot-manager")[0];
|
||||
pm2.sendDataToProcessId(managerProc.pm_id, {
|
||||
id: managerProc.pm_id,
|
||||
type: "process:msg",
|
||||
data: {
|
||||
type: "getCount"
|
||||
},
|
||||
topic: true
|
||||
}, (err) => {
|
||||
if (err) reject(err);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
resolve(bot.guilds.size);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// copied from eris
|
||||
export function cleanMessage(message, content) {
|
||||
let cleanContent = content && content.replace(/<a?(:\w+:)[0-9]+>/g, "$1") || "";
|
||||
|
||||
const author = message.author ?? message.member ?? message.user;
|
||||
let authorName = author.username;
|
||||
if (message.member?.nick) {
|
||||
authorName = message.member.nick;
|
||||
}
|
||||
cleanContent = cleanContent.replace(new RegExp(`<@!?${author.id}>`, "g"), `@${authorName}`);
|
||||
|
||||
if (message.mentions) {
|
||||
for (const mention of message.mentions.members) {
|
||||
if (mention.nick) {
|
||||
cleanContent = cleanContent.replace(new RegExp(`<@!?${mention.id}>`, "g"), `@${mention.nick}`);
|
||||
}
|
||||
cleanContent = cleanContent.replace(new RegExp(`<@!?${mention.id}>`, "g"), `@${mention.username}`);
|
||||
}
|
||||
|
||||
if (message.guildID && message.mentions.roles) {
|
||||
for (const roleID of message.mentions.roles) {
|
||||
const role = message.guild.roles.get(roleID);
|
||||
const roleName = role ? role.name : "deleted-role";
|
||||
cleanContent = cleanContent.replace(new RegExp(`<@&${roleID}>`, "g"), `@${roleName}`);
|
||||
}
|
||||
}
|
||||
|
||||
for (const id of message.mentions.channels) {
|
||||
const channel = message.client.getChannel(id);
|
||||
if (channel && channel.name && channel.mention) {
|
||||
cleanContent = cleanContent.replace(channel.mention, `#${channel.name}`);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return textEncode(cleanContent);
|
||||
import util from "util";
|
||||
import fs from "fs";
|
||||
import pm2 from "pm2";
|
||||
import { config } from "dotenv";
|
||||
import db from "./database.js";
|
||||
|
||||
// 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
|
||||
export function random(array) {
|
||||
if (!array || array.length < 1) return null;
|
||||
return array[Math.floor(Math.random() * array.length)];
|
||||
}
|
||||
|
||||
const optionalReplace = (token) => {
|
||||
return token === undefined || token === "" ? "" : (token === "true" || token === "false" ? token : "<redacted>");
|
||||
};
|
||||
|
||||
// clean(text) to clean message of any private info or mentions
|
||||
export function clean(text) {
|
||||
if (typeof text !== "string")
|
||||
text = util.inspect(text, { depth: 1 });
|
||||
|
||||
text = text
|
||||
.replaceAll("`", `\`${String.fromCharCode(8203)}`)
|
||||
.replaceAll("@", `@${String.fromCharCode(8203)}`);
|
||||
|
||||
let { parsed } = config();
|
||||
if (!parsed) parsed = process.env;
|
||||
const imageServers = JSON.parse(fs.readFileSync(new URL("../config/servers.json", import.meta.url), { encoding: "utf8" })).image;
|
||||
|
||||
if (imageServers?.length !== 0) {
|
||||
for (const { server, auth } of imageServers) {
|
||||
text = text.replaceAll(server, optionalReplace(server));
|
||||
text = text.replaceAll(auth, optionalReplace(auth));
|
||||
}
|
||||
}
|
||||
|
||||
for (const env of Object.keys(parsed)) {
|
||||
text = text.replaceAll(parsed[env], optionalReplace(parsed[env]));
|
||||
}
|
||||
|
||||
return text;
|
||||
}
|
||||
|
||||
// textEncode(string) to encode characters for image processing
|
||||
export function textEncode(string) {
|
||||
return string.replaceAll("&", "&").replaceAll(">", ">").replaceAll("<", "<").replaceAll("\"", """).replaceAll("'", "'").replaceAll("\\n", "\n").replaceAll("\\:", ":").replaceAll("\\,", ",");
|
||||
}
|
||||
|
||||
// set activity (a.k.a. the gamer code)
|
||||
export async function activityChanger(bot) {
|
||||
if (!broadcast) {
|
||||
await bot.editStatus("dnd", [{
|
||||
type: 0,
|
||||
name: random(messages) + (types.classic ? ` | @${bot.user.username} help` : "")
|
||||
}]);
|
||||
}
|
||||
setTimeout(() => activityChanger(bot), 900000);
|
||||
}
|
||||
|
||||
export async function checkBroadcast(bot) {
|
||||
if (!db) {
|
||||
return;
|
||||
}
|
||||
const message = await db.getBroadcast();
|
||||
if (message) {
|
||||
startBroadcast(bot, message);
|
||||
}
|
||||
}
|
||||
|
||||
export function startBroadcast(bot, message) {
|
||||
bot.editStatus("dnd", [{
|
||||
type: 0,
|
||||
name: message + (types.classic ? ` | @${bot.user.username} help` : "")
|
||||
}]);
|
||||
broadcast = true;
|
||||
}
|
||||
|
||||
export function endBroadcast(bot) {
|
||||
bot.editStatus("dnd", [{
|
||||
type: 0,
|
||||
name: random(messages) + (types.classic ? ` | @${bot.user.username} help` : "")
|
||||
}]);
|
||||
broadcast = false;
|
||||
}
|
||||
|
||||
export function getServers(bot) {
|
||||
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.list((err, list) => {
|
||||
if (err) {
|
||||
reject(err);
|
||||
return;
|
||||
}
|
||||
const managerProc = list.filter((v) => v.name === "esmBot-manager")[0];
|
||||
pm2.sendDataToProcessId(managerProc.pm_id, {
|
||||
id: managerProc.pm_id,
|
||||
type: "process:msg",
|
||||
data: {
|
||||
type: "getCount"
|
||||
},
|
||||
topic: true
|
||||
}, (err) => {
|
||||
if (err) reject(err);
|
||||
});
|
||||
});
|
||||
} else {
|
||||
resolve(bot.guilds.size);
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
// copied from eris
|
||||
export function cleanMessage(message, content) {
|
||||
let cleanContent = content && content.replace(/<a?(:\w+:)[0-9]+>/g, "$1") || "";
|
||||
// TODO: see if I need to fuck with this
|
||||
|
||||
// const author = message.author ?? message.member ?? message.user;
|
||||
// let authorName = author.username;
|
||||
// if (message.member?.nick) {
|
||||
// authorName = message.member.nick;
|
||||
// }
|
||||
// cleanContent = cleanContent.replace(new RegExp(`<@!?${author.id}>`, "g"), `@${authorName}`);
|
||||
|
||||
// if (message.mentions) {
|
||||
// for (const mention of message.mentions.members) {
|
||||
// if (mention.nick) {
|
||||
// cleanContent = cleanContent.replace(new RegExp(`<@!?${mention.id}>`, "g"), `@${mention.nick}`);
|
||||
// }
|
||||
// cleanContent = cleanContent.replace(new RegExp(`<@!?${mention.id}>`, "g"), `@${mention.username}`);
|
||||
// }
|
||||
|
||||
// if (message.guildID && message.mentions.roles) {
|
||||
// for (const roleID of message.mentions.roles) {
|
||||
// const role = message.guild.roles.get(roleID);
|
||||
// const roleName = role ? role.name : "deleted-role";
|
||||
// cleanContent = cleanContent.replace(new RegExp(`<@&${roleID}>`, "g"), `@${roleName}`);
|
||||
// }
|
||||
// }
|
||||
|
||||
// for (const id of message.mentions.channels) {
|
||||
// const channel = message.client.getChannel(id);
|
||||
// if (channel && channel.name && channel.mention) {
|
||||
// cleanContent = cleanContent.replace(channel.mention, `#${channel.name}`);
|
||||
// }
|
||||
// }
|
||||
// }
|
||||
|
||||
return textEncode(cleanContent);
|
||||
}
|
|
@ -1,39 +1,39 @@
|
|||
// oceanic doesn't come with a method to wait for interactions by default, so we make our own
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
class InteractionCollector extends EventEmitter {
|
||||
constructor(client, message, type, timeout = 120000) {
|
||||
super();
|
||||
this.message = message;
|
||||
this.type = type;
|
||||
this.ended = false;
|
||||
this.bot = client;
|
||||
this.timeout = timeout;
|
||||
this.listener = async (interaction) => {
|
||||
await this.verify(interaction);
|
||||
};
|
||||
this.bot.on("interactionCreate", this.listener);
|
||||
this.end = setTimeout(() => this.stop("time"), timeout);
|
||||
}
|
||||
|
||||
async verify(interaction) {
|
||||
if (!(interaction instanceof this.type)) return false;
|
||||
if (this.message.id !== interaction.message.id) return false;
|
||||
this.emit("interaction", interaction);
|
||||
return true;
|
||||
}
|
||||
|
||||
extend() {
|
||||
clearTimeout(this.end);
|
||||
this.end = setTimeout(() => this.stop("time"), this.timeout);
|
||||
}
|
||||
|
||||
stop(reason) {
|
||||
if (this.ended) return;
|
||||
this.ended = true;
|
||||
this.bot.removeListener("interactionCreate", this.listener);
|
||||
this.emit("end", this.collected, reason);
|
||||
}
|
||||
}
|
||||
|
||||
export default InteractionCollector;
|
||||
// oceanic doesn't come with a method to wait for interactions by default, so we make our own
|
||||
import { EventEmitter } from "events";
|
||||
|
||||
class InteractionCollector extends EventEmitter {
|
||||
constructor(client, message, type, timeout = 120000) {
|
||||
super();
|
||||
this.message = message;
|
||||
this.type = type;
|
||||
this.ended = false;
|
||||
this.bot = client;
|
||||
this.timeout = timeout;
|
||||
this.listener = async (interaction) => {
|
||||
await this.verify(interaction);
|
||||
};
|
||||
this.bot.on("interactionCreate", this.listener);
|
||||
this.end = setTimeout(() => this.stop("time"), timeout);
|
||||
}
|
||||
|
||||
async verify(interaction) {
|
||||
if (!(interaction instanceof this.type)) return false;
|
||||
if (this.message.id !== interaction.message.id) return false;
|
||||
this.emit("interaction", interaction);
|
||||
return true;
|
||||
}
|
||||
|
||||
extend() {
|
||||
clearTimeout(this.end);
|
||||
this.end = setTimeout(() => this.stop("time"), this.timeout);
|
||||
}
|
||||
|
||||
stop(reason) {
|
||||
if (this.ended) return;
|
||||
this.ended = true;
|
||||
this.bot.removeListener("interactionCreate", this.listener);
|
||||
this.emit("end", this.collected, reason);
|
||||
}
|
||||
}
|
||||
|
||||
export default InteractionCollector;
|
||||
|
|
|
@ -1,187 +1,187 @@
|
|||
import InteractionCollector from "./awaitinteractions.js";
|
||||
import { ComponentInteraction } from "oceanic.js";
|
||||
|
||||
export default async (client, info, pages, timeout = 120000) => {
|
||||
const options = info.type === "classic" ? {
|
||||
messageReference: {
|
||||
channelID: info.message.channelID,
|
||||
messageID: info.message.id,
|
||||
guildID: info.message.guildID,
|
||||
failIfNotExists: false
|
||||
},
|
||||
allowedMentions: {
|
||||
repliedUser: false
|
||||
}
|
||||
} : {};
|
||||
let page = 0;
|
||||
const components = {
|
||||
components: [{
|
||||
type: 1,
|
||||
components: [
|
||||
{
|
||||
type: 2,
|
||||
label: "Back",
|
||||
emoji: {
|
||||
id: null,
|
||||
name: "◀"
|
||||
},
|
||||
style: 1,
|
||||
customID: "back"
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
label: "Forward",
|
||||
emoji: {
|
||||
id: null,
|
||||
name: "▶"
|
||||
},
|
||||
style: 1,
|
||||
customID: "forward"
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
label: "Jump",
|
||||
emoji: {
|
||||
id: null,
|
||||
name: "🔢"
|
||||
},
|
||||
style: 1,
|
||||
customID: "jump"
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
label: "Delete",
|
||||
emoji: {
|
||||
id: null,
|
||||
name: "🗑"
|
||||
},
|
||||
style: 4,
|
||||
customID: "delete"
|
||||
}
|
||||
]
|
||||
}]
|
||||
};
|
||||
let currentPage;
|
||||
if (info.type === "classic") {
|
||||
currentPage = await client.rest.channels.createMessage(info.message.channelID, Object.assign(pages[page], options, pages.length > 1 ? components : {}));
|
||||
} else {
|
||||
currentPage = await info.interaction[info.interaction.acknowledged ? "editOriginal" : "createMessage"](Object.assign(pages[page], pages.length > 1 ? components : {}));
|
||||
if (!currentPage) currentPage = await info.interaction.getOriginal();
|
||||
}
|
||||
|
||||
if (pages.length > 1) {
|
||||
const interactionCollector = new InteractionCollector(client, currentPage, ComponentInteraction, timeout);
|
||||
interactionCollector.on("interaction", async (interaction) => {
|
||||
if ((interaction.member ?? interaction.user).id === info.author.id) {
|
||||
switch (interaction.data.customID) {
|
||||
case "back":
|
||||
await interaction.deferUpdate();
|
||||
page = page > 0 ? --page : pages.length - 1;
|
||||
currentPage = await currentPage.edit(Object.assign(pages[page], options));
|
||||
interactionCollector.extend();
|
||||
break;
|
||||
case "forward":
|
||||
await interaction.deferUpdate();
|
||||
page = page + 1 < pages.length ? ++page : 0;
|
||||
currentPage = await currentPage.edit(Object.assign(pages[page], options));
|
||||
interactionCollector.extend();
|
||||
break;
|
||||
case "jump":
|
||||
await interaction.deferUpdate();
|
||||
var newComponents = JSON.parse(JSON.stringify(components));
|
||||
for (const index of newComponents.components[0].components.keys()) {
|
||||
newComponents.components[0].components[index].disabled = true;
|
||||
}
|
||||
currentPage = await currentPage.edit(newComponents);
|
||||
interactionCollector.extend();
|
||||
var jumpComponents = {
|
||||
components: [{
|
||||
type: 1,
|
||||
components: [{
|
||||
type: 3,
|
||||
customID: "seekDropdown",
|
||||
placeholder: "Page Number",
|
||||
options: []
|
||||
}]
|
||||
}]
|
||||
};
|
||||
for (let i = 0; i < pages.length && i < 25; i++) {
|
||||
const payload = {
|
||||
label: i + 1,
|
||||
value: i
|
||||
};
|
||||
jumpComponents.components[0].components[0].options[i] = payload;
|
||||
}
|
||||
var promise;
|
||||
if (info.type === "classic") {
|
||||
promise = client.rest.channels.createMessage(info.message.channelID, Object.assign({ content: "What page do you want to jump to?" }, {
|
||||
messageReference: {
|
||||
channelID: currentPage.channelID,
|
||||
messageID: currentPage.id,
|
||||
guildID: currentPage.guildID,
|
||||
failIfNotExists: false
|
||||
},
|
||||
allowedMentions: {
|
||||
repliedUser: false
|
||||
}
|
||||
}, jumpComponents));
|
||||
} else {
|
||||
promise = info.interaction.createFollowup(Object.assign({ content: "What page do you want to jump to?" }, jumpComponents));
|
||||
}
|
||||
promise.then(askMessage => {
|
||||
const dropdownCollector = new InteractionCollector(client, askMessage, ComponentInteraction, timeout);
|
||||
let ended = false;
|
||||
dropdownCollector.on("interaction", async (response) => {
|
||||
if (response.data.customID !== "seekDropdown") return;
|
||||
try {
|
||||
await askMessage.delete();
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
page = Number(response.data.values.raw[0]);
|
||||
currentPage = await currentPage.edit(Object.assign(pages[page], options, components));
|
||||
ended = true;
|
||||
dropdownCollector.stop();
|
||||
});
|
||||
dropdownCollector.once("end", async () => {
|
||||
if (ended) return;
|
||||
try {
|
||||
await askMessage.delete();
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
currentPage = await currentPage.edit(Object.assign(pages[page], options, components));
|
||||
});
|
||||
}).catch(error => {
|
||||
throw error;
|
||||
});
|
||||
break;
|
||||
case "delete":
|
||||
await interaction.deferUpdate();
|
||||
interactionCollector.emit("end", true);
|
||||
try {
|
||||
await currentPage.delete();
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
interactionCollector.once("end", async (deleted = false) => {
|
||||
interactionCollector.removeAllListeners("interaction");
|
||||
if (!deleted) {
|
||||
for (const index of components.components[0].components.keys()) {
|
||||
components.components[0].components[index].disabled = true;
|
||||
}
|
||||
try {
|
||||
await currentPage.edit(components);
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
import InteractionCollector from "./awaitinteractions.js";
|
||||
import { ComponentInteraction } from "oceanic.js";
|
||||
|
||||
export default async (client, info, pages, timeout = 120000) => {
|
||||
const options = info.type === "classic" ? {
|
||||
messageReference: {
|
||||
channelID: info.message.channelID,
|
||||
messageID: info.message.id,
|
||||
guildID: info.message.guildID,
|
||||
failIfNotExists: false
|
||||
},
|
||||
allowedMentions: {
|
||||
repliedUser: false
|
||||
}
|
||||
} : {};
|
||||
let page = 0;
|
||||
const components = {
|
||||
components: [{
|
||||
type: 1,
|
||||
components: [
|
||||
{
|
||||
type: 2,
|
||||
label: "Back",
|
||||
emoji: {
|
||||
id: null,
|
||||
name: "◀"
|
||||
},
|
||||
style: 1,
|
||||
customID: "back"
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
label: "Forward",
|
||||
emoji: {
|
||||
id: null,
|
||||
name: "▶"
|
||||
},
|
||||
style: 1,
|
||||
customID: "forward"
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
label: "Jump",
|
||||
emoji: {
|
||||
id: null,
|
||||
name: "🔢"
|
||||
},
|
||||
style: 1,
|
||||
customID: "jump"
|
||||
},
|
||||
{
|
||||
type: 2,
|
||||
label: "Delete",
|
||||
emoji: {
|
||||
id: null,
|
||||
name: "🗑"
|
||||
},
|
||||
style: 4,
|
||||
customID: "delete"
|
||||
}
|
||||
]
|
||||
}]
|
||||
};
|
||||
let currentPage;
|
||||
if (info.type === "classic") {
|
||||
currentPage = await client.rest.channels.createMessage(info.message.channelID, Object.assign(pages[page], options, pages.length > 1 ? components : {}));
|
||||
} else {
|
||||
currentPage = await info.interaction[info.interaction.acknowledged ? "editOriginal" : "createMessage"](Object.assign(pages[page], pages.length > 1 ? components : {}));
|
||||
if (!currentPage) currentPage = await info.interaction.getOriginal();
|
||||
}
|
||||
|
||||
if (pages.length > 1) {
|
||||
const interactionCollector = new InteractionCollector(client, currentPage, ComponentInteraction, timeout);
|
||||
interactionCollector.on("interaction", async (interaction) => {
|
||||
if ((interaction.member ?? interaction.user).id === info.author.id) {
|
||||
switch (interaction.data.customID) {
|
||||
case "back":
|
||||
await interaction.deferUpdate();
|
||||
page = page > 0 ? --page : pages.length - 1;
|
||||
currentPage = await currentPage.edit(Object.assign(pages[page], options));
|
||||
interactionCollector.extend();
|
||||
break;
|
||||
case "forward":
|
||||
await interaction.deferUpdate();
|
||||
page = page + 1 < pages.length ? ++page : 0;
|
||||
currentPage = await currentPage.edit(Object.assign(pages[page], options));
|
||||
interactionCollector.extend();
|
||||
break;
|
||||
case "jump":
|
||||
await interaction.deferUpdate();
|
||||
var newComponents = JSON.parse(JSON.stringify(components));
|
||||
for (const index of newComponents.components[0].components.keys()) {
|
||||
newComponents.components[0].components[index].disabled = true;
|
||||
}
|
||||
currentPage = await currentPage.edit(newComponents);
|
||||
interactionCollector.extend();
|
||||
var jumpComponents = {
|
||||
components: [{
|
||||
type: 1,
|
||||
components: [{
|
||||
type: 3,
|
||||
customID: "seekDropdown",
|
||||
placeholder: "Page Number",
|
||||
options: []
|
||||
}]
|
||||
}]
|
||||
};
|
||||
for (let i = 0; i < pages.length && i < 25; i++) {
|
||||
const payload = {
|
||||
label: i + 1,
|
||||
value: i
|
||||
};
|
||||
jumpComponents.components[0].components[0].options[i] = payload;
|
||||
}
|
||||
var promise;
|
||||
if (info.type === "classic") {
|
||||
promise = client.rest.channels.createMessage(info.message.channelID, Object.assign({ content: "What page do you want to jump to?" }, {
|
||||
messageReference: {
|
||||
channelID: currentPage.channelID,
|
||||
messageID: currentPage.id,
|
||||
guildID: currentPage.guildID,
|
||||
failIfNotExists: false
|
||||
},
|
||||
allowedMentions: {
|
||||
repliedUser: false
|
||||
}
|
||||
}, jumpComponents));
|
||||
} else {
|
||||
promise = info.interaction.createFollowup(Object.assign({ content: "What page do you want to jump to?" }, jumpComponents));
|
||||
}
|
||||
promise.then(askMessage => {
|
||||
const dropdownCollector = new InteractionCollector(client, askMessage, ComponentInteraction, timeout);
|
||||
let ended = false;
|
||||
dropdownCollector.on("interaction", async (response) => {
|
||||
if (response.data.customID !== "seekDropdown") return;
|
||||
try {
|
||||
await askMessage.delete();
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
page = Number(response.data.values.raw[0]);
|
||||
currentPage = await currentPage.edit(Object.assign(pages[page], options, components));
|
||||
ended = true;
|
||||
dropdownCollector.stop();
|
||||
});
|
||||
dropdownCollector.once("end", async () => {
|
||||
if (ended) return;
|
||||
try {
|
||||
await askMessage.delete();
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
currentPage = await currentPage.edit(Object.assign(pages[page], options, components));
|
||||
});
|
||||
}).catch(error => {
|
||||
throw error;
|
||||
});
|
||||
break;
|
||||
case "delete":
|
||||
await interaction.deferUpdate();
|
||||
interactionCollector.emit("end", true);
|
||||
try {
|
||||
await currentPage.delete();
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
return;
|
||||
default:
|
||||
break;
|
||||
}
|
||||
}
|
||||
});
|
||||
interactionCollector.once("end", async (deleted = false) => {
|
||||
interactionCollector.removeAllListeners("interaction");
|
||||
if (!deleted) {
|
||||
for (const index of components.components[0].components.keys()) {
|
||||
components.components[0].components[index].disabled = true;
|
||||
}
|
||||
try {
|
||||
await currentPage.edit(components);
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
|
|
@ -1,112 +1,112 @@
|
|||
export default (input) => {
|
||||
if (typeof input === "string") input = input.split(/\s+/g);
|
||||
const args = { _: [] };
|
||||
let curr = null;
|
||||
let concated = "";
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const a = input[i];
|
||||
if ((a.startsWith("--") || a.startsWith("—")) && !curr) {
|
||||
if (a.includes("=")) {
|
||||
const [arg, value] = (a.startsWith("--") ? a.slice(2).split("=") : a.slice(1).split("="));
|
||||
let ended = true;
|
||||
if (arg !== "_") {
|
||||
if (value.startsWith("\"")) {
|
||||
if (value.endsWith("\"")) {
|
||||
args[arg] = value.slice(1).slice(0, -1);
|
||||
} else {
|
||||
args[arg] = `${value.slice(1)} `;
|
||||
ended = false;
|
||||
}
|
||||
} else if (value.endsWith("\"")) {
|
||||
args[arg] += a.slice(0, -1);
|
||||
} else if (value !== "") {
|
||||
args[arg] = value;
|
||||
} else {
|
||||
args[arg] = true;
|
||||
}
|
||||
if (args[arg] === "true") {
|
||||
args[arg] = true;
|
||||
} else if (args[arg] === "false") {
|
||||
args[arg] = false;
|
||||
}
|
||||
if (!ended) curr = arg;
|
||||
}
|
||||
} else {
|
||||
args[a.slice(2)] = true;
|
||||
}
|
||||
} else if (curr) {
|
||||
if (a.endsWith("\"")) {
|
||||
args[curr] += a.slice(0, -1);
|
||||
curr = null;
|
||||
} else {
|
||||
args[curr] += `${a} `;
|
||||
}
|
||||
} else {
|
||||
if (concated !== "") {
|
||||
concated += `${a} `;
|
||||
} else {
|
||||
args._.push(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (curr && args[curr] == "") {
|
||||
args[curr] = true;
|
||||
}
|
||||
|
||||
return args;
|
||||
};
|
||||
|
||||
// /*
|
||||
// Format:
|
||||
// [{name: "verbose", type: "bool"}, {name: "username", type: "string"}]
|
||||
// */
|
||||
// export default (input, format) => {
|
||||
// let results = {};
|
||||
// let text = input.split(' ').slice(1).join(' ');
|
||||
// format.forEach(element => {
|
||||
// if(element.pos !== undefined) return;
|
||||
// switch (element.type) {
|
||||
// case "bool":
|
||||
// res = text.match(`--${element.name}[ |=](.*?)($| )`);
|
||||
// if(res) {
|
||||
// text = text.replace(res[0], "");
|
||||
// results[element.name] = (res[1].toLowerCase() == "true");
|
||||
// } else {
|
||||
// res = text.match(`--${element.name}`);
|
||||
// if(res) text = text.replace(res[0], "");
|
||||
// results[element.name] = (res != null);
|
||||
// }
|
||||
// break;
|
||||
// case "string":
|
||||
// res = text.match(`--${element.name}[ |=](.*?)($| )`);
|
||||
// if(res) text = text.replace(res[0], "");
|
||||
// results[element.name] = (res ? res[1].replace('\\','') : null);
|
||||
// break;
|
||||
// case "int":
|
||||
// res = text.match(`--${element.name}[ |=](.*?)($| )`);
|
||||
// if(res) text = text.replace(res[0], "");
|
||||
// results[element.name] = (res ? parseInt(res[1]) : null);
|
||||
// break;
|
||||
// case "float":
|
||||
// res = text.match(`--${element.name}[ |=](.*?)($| )`);
|
||||
// if(res) text = text.replace(res[0], "");
|
||||
// results[element.name] = (res ? parseFloat(res[1]) : null);
|
||||
// break;
|
||||
// default:
|
||||
// throw Error("unknown type");
|
||||
// break;
|
||||
// }
|
||||
// });
|
||||
// let s = text.split(' ');
|
||||
// results._ = text;
|
||||
// format.forEach(element => {
|
||||
// if(element.pos === undefined) return;
|
||||
// if(element.pos <= s.length) {
|
||||
// results[element.name] = s[element.pos];
|
||||
// } else {
|
||||
// results[element.name] = null;
|
||||
// }
|
||||
// })
|
||||
// return results;
|
||||
// }
|
||||
export default (input) => {
|
||||
if (typeof input === "string") input = input.split(/\s+/g);
|
||||
const args = { _: [] };
|
||||
let curr = null;
|
||||
let concated = "";
|
||||
for (let i = 0; i < input.length; i++) {
|
||||
const a = input[i];
|
||||
if ((a.startsWith("--") || a.startsWith("—")) && !curr) {
|
||||
if (a.includes("=")) {
|
||||
const [arg, value] = (a.startsWith("--") ? a.slice(2).split("=") : a.slice(1).split("="));
|
||||
let ended = true;
|
||||
if (arg !== "_") {
|
||||
if (value.startsWith("\"")) {
|
||||
if (value.endsWith("\"")) {
|
||||
args[arg] = value.slice(1).slice(0, -1);
|
||||
} else {
|
||||
args[arg] = `${value.slice(1)} `;
|
||||
ended = false;
|
||||
}
|
||||
} else if (value.endsWith("\"")) {
|
||||
args[arg] += a.slice(0, -1);
|
||||
} else if (value !== "") {
|
||||
args[arg] = value;
|
||||
} else {
|
||||
args[arg] = true;
|
||||
}
|
||||
if (args[arg] === "true") {
|
||||
args[arg] = true;
|
||||
} else if (args[arg] === "false") {
|
||||
args[arg] = false;
|
||||
}
|
||||
if (!ended) curr = arg;
|
||||
}
|
||||
} else {
|
||||
args[a.slice(2)] = true;
|
||||
}
|
||||
} else if (curr) {
|
||||
if (a.endsWith("\"")) {
|
||||
args[curr] += a.slice(0, -1);
|
||||
curr = null;
|
||||
} else {
|
||||
args[curr] += `${a} `;
|
||||
}
|
||||
} else {
|
||||
if (concated !== "") {
|
||||
concated += `${a} `;
|
||||
} else {
|
||||
args._.push(a);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if (curr && args[curr] == "") {
|
||||
args[curr] = true;
|
||||
}
|
||||
|
||||
return args;
|
||||
};
|
||||
|
||||
// /*
|
||||
// Format:
|
||||
// [{name: "verbose", type: "bool"}, {name: "username", type: "string"}]
|
||||
// */
|
||||
// export default (input, format) => {
|
||||
// let results = {};
|
||||
// let text = input.split(' ').slice(1).join(' ');
|
||||
// format.forEach(element => {
|
||||
// if(element.pos !== undefined) return;
|
||||
// switch (element.type) {
|
||||
// case "bool":
|
||||
// res = text.match(`--${element.name}[ |=](.*?)($| )`);
|
||||
// if(res) {
|
||||
// text = text.replace(res[0], "");
|
||||
// results[element.name] = (res[1].toLowerCase() == "true");
|
||||
// } else {
|
||||
// res = text.match(`--${element.name}`);
|
||||
// if(res) text = text.replace(res[0], "");
|
||||
// results[element.name] = (res != null);
|
||||
// }
|
||||
// break;
|
||||
// case "string":
|
||||
// res = text.match(`--${element.name}[ |=](.*?)($| )`);
|
||||
// if(res) text = text.replace(res[0], "");
|
||||
// results[element.name] = (res ? res[1].replace('\\','') : null);
|
||||
// break;
|
||||
// case "int":
|
||||
// res = text.match(`--${element.name}[ |=](.*?)($| )`);
|
||||
// if(res) text = text.replace(res[0], "");
|
||||
// results[element.name] = (res ? parseInt(res[1]) : null);
|
||||
// break;
|
||||
// case "float":
|
||||
// res = text.match(`--${element.name}[ |=](.*?)($| )`);
|
||||
// if(res) text = text.replace(res[0], "");
|
||||
// results[element.name] = (res ? parseFloat(res[1]) : null);
|
||||
// break;
|
||||
// default:
|
||||
// throw Error("unknown type");
|
||||
// break;
|
||||
// }
|
||||
// });
|
||||
// let s = text.split(' ');
|
||||
// results._ = text;
|
||||
// format.forEach(element => {
|
||||
// if(element.pos === undefined) return;
|
||||
// if(element.pos <= s.length) {
|
||||
// results[element.name] = s[element.pos];
|
||||
// } else {
|
||||
// results[element.name] = null;
|
||||
// }
|
||||
// })
|
||||
// return results;
|
||||
// }
|
||||
|
|
478
utils/pm2/ext.js
478
utils/pm2/ext.js
|
@ -1,240 +1,240 @@
|
|||
import pm2 from "pm2";
|
||||
import winston from "winston";
|
||||
|
||||
// load config from .env file
|
||||
import { resolve, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { readFileSync } from "fs";
|
||||
import { createServer } from "http";
|
||||
import { config } from "dotenv";
|
||||
config({ path: resolve(dirname(fileURLToPath(import.meta.url)), "../../.env") });
|
||||
|
||||
// oceanic client used for getting shard counts
|
||||
import { Client } from "oceanic.js";
|
||||
|
||||
import database from "../database.js";
|
||||
import { cpus } from "os";
|
||||
|
||||
const logger = winston.createLogger({
|
||||
levels: {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
main: 3,
|
||||
debug: 4
|
||||
},
|
||||
transports: [
|
||||
new winston.transports.Console({ format: winston.format.colorize({ all: true }), stderrLevels: ["error", "warn"] })
|
||||
],
|
||||
level: process.env.DEBUG_LOG ? "debug" : "main",
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
winston.format.printf((info) => {
|
||||
const {
|
||||
timestamp, level, message, ...args
|
||||
} = info;
|
||||
|
||||
return `[${timestamp}]: [${level.toUpperCase()}] - ${message} ${Object.keys(args).length ? JSON.stringify(args, null, 2) : ""}`;
|
||||
}),
|
||||
)
|
||||
});
|
||||
|
||||
winston.addColors({
|
||||
info: "green",
|
||||
main: "gray",
|
||||
debug: "magenta",
|
||||
warn: "yellow",
|
||||
error: "red"
|
||||
});
|
||||
|
||||
let serverCount = 0;
|
||||
let shardCount = 0;
|
||||
let clusterCount = 0;
|
||||
let responseCount = 0;
|
||||
|
||||
let timeout;
|
||||
|
||||
process.on("message", (packet) => {
|
||||
if (packet.data?.type === "getCount") {
|
||||
process.send({
|
||||
type: "process:msg",
|
||||
data: {
|
||||
type: "countResponse",
|
||||
serverCount
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function updateStats() {
|
||||
serverCount = 0;
|
||||
shardCount = 0;
|
||||
clusterCount = 0;
|
||||
responseCount = 0;
|
||||
return new Promise((resolve, reject) => {
|
||||
pm2.list((err, list) => {
|
||||
if (err) reject(err);
|
||||
const clusters = list.filter((v) => v.name.includes("esmBot-proc"));
|
||||
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"
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.METRICS && process.env.METRICS !== "") {
|
||||
const servers = [];
|
||||
if (process.env.API_TYPE === "ws") {
|
||||
const imageHosts = JSON.parse(readFileSync(new URL("../../config/servers.json", import.meta.url), { encoding: "utf8" })).image;
|
||||
for (let { server } of imageHosts) {
|
||||
if (!server.includes(":")) {
|
||||
server += ":3762";
|
||||
}
|
||||
servers.push(server);
|
||||
}
|
||||
}
|
||||
const httpServer = createServer(async (req, res) => {
|
||||
if (req.method !== "GET") {
|
||||
res.statusCode = 405;
|
||||
return res.end("GET only");
|
||||
}
|
||||
res.write(`# HELP esmbot_command_count Number of times a command has been run
|
||||
# TYPE esmbot_command_count counter
|
||||
# HELP esmbot_server_count Number of servers/guilds the bot is in
|
||||
# TYPE esmbot_server_count gauge
|
||||
# HELP esmbot_shard_count Number of shards the bot has
|
||||
# TYPE esmbot_shard_count gauge
|
||||
`);
|
||||
if (database) {
|
||||
const counts = await database.getCounts();
|
||||
for (const [i, w] of Object.entries(counts)) {
|
||||
res.write(`esmbot_command_count{command="${i}"} ${w}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
res.write(`esmbot_server_count ${serverCount}\n`);
|
||||
res.write(`esmbot_shard_count ${shardCount}\n`);
|
||||
res.end();
|
||||
});
|
||||
httpServer.listen(process.env.METRICS, () => {
|
||||
logger.log("info", `Serving metrics at ${process.env.METRICS}`);
|
||||
});
|
||||
}
|
||||
|
||||
setInterval(updateStats, 300000);
|
||||
|
||||
setTimeout(updateStats, 10000);
|
||||
|
||||
logger.info("Started esmBot management process.");
|
||||
|
||||
// from eris-fleet
|
||||
function calcShards(shards, procs) {
|
||||
if (procs < 2) return [shards];
|
||||
|
||||
const length = shards.length;
|
||||
const r = [];
|
||||
let i = 0;
|
||||
let size;
|
||||
|
||||
if (length % procs === 0) {
|
||||
size = Math.floor(length / procs);
|
||||
while (i < length) {
|
||||
r.push(shards.slice(i, (i += size)));
|
||||
}
|
||||
} else {
|
||||
while (i < length) {
|
||||
size = Math.ceil((length - i) / procs--);
|
||||
r.push(shards.slice(i, (i += size)));
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
(async function init() {
|
||||
logger.main("Getting gateway connection data...");
|
||||
const client = new Client({
|
||||
auth: `Bot ${process.env.TOKEN}`,
|
||||
gateway: {
|
||||
concurrency: "auto",
|
||||
maxShards: "auto",
|
||||
presence: {
|
||||
status: "idle",
|
||||
activities: [{
|
||||
type: 0,
|
||||
name: "Starting esmBot..."
|
||||
}]
|
||||
},
|
||||
intents: []
|
||||
}
|
||||
});
|
||||
|
||||
const connectionData = await client.rest.getBotGateway();
|
||||
const cpuAmount = cpus().length;
|
||||
const procAmount = Math.min(connectionData.shards, cpuAmount);
|
||||
logger.main(`Obtained data, connecting with ${connectionData.shards} shard(s) across ${procAmount} process(es)...`);
|
||||
|
||||
const lastShard = connectionData.shards - 1;
|
||||
const shardArray = [];
|
||||
for (let i = 0; i <= lastShard; i++) {
|
||||
shardArray.push(i);
|
||||
}
|
||||
const shardArrays = calcShards(shardArray, procAmount);
|
||||
|
||||
for (let i = 0; i < shardArrays.length; i++) {
|
||||
await awaitStart(i, shardArrays);
|
||||
}
|
||||
})();
|
||||
|
||||
function awaitStart(i, shardArrays) {
|
||||
return new Promise((resolve) => {
|
||||
pm2.start({
|
||||
name: `esmBot-proc${i}`,
|
||||
script: "app.js",
|
||||
autorestart: true,
|
||||
exp_backoff_restart_delay: 1000,
|
||||
wait_ready: true,
|
||||
listen_timeout: 60000,
|
||||
watch: false,
|
||||
exec_mode: "cluster",
|
||||
instances: 1,
|
||||
env: {
|
||||
"SHARDS": JSON.stringify(shardArrays)
|
||||
}
|
||||
}, (err) => {
|
||||
if (err) {
|
||||
logger.error(`Failed to start esmBot process ${i}: ${err}`);
|
||||
process.exit(0);
|
||||
} else {
|
||||
logger.info(`Started esmBot process ${i}.`);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
import pm2 from "pm2";
|
||||
import winston from "winston";
|
||||
|
||||
// load config from .env file
|
||||
import { resolve, dirname } from "path";
|
||||
import { fileURLToPath } from "url";
|
||||
import { readFileSync } from "fs";
|
||||
import { createServer } from "http";
|
||||
import { config } from "dotenv";
|
||||
config({ path: resolve(dirname(fileURLToPath(import.meta.url)), "../../.env") });
|
||||
|
||||
// oceanic client used for getting shard counts
|
||||
import { Client } from "oceanic.js";
|
||||
|
||||
import database from "../database.js";
|
||||
import { cpus } from "os";
|
||||
|
||||
const logger = winston.createLogger({
|
||||
levels: {
|
||||
error: 0,
|
||||
warn: 1,
|
||||
info: 2,
|
||||
main: 3,
|
||||
debug: 4
|
||||
},
|
||||
transports: [
|
||||
new winston.transports.Console({ format: winston.format.colorize({ all: true }), stderrLevels: ["error", "warn"] })
|
||||
],
|
||||
level: process.env.DEBUG_LOG ? "debug" : "main",
|
||||
format: winston.format.combine(
|
||||
winston.format.timestamp({ format: "YYYY-MM-DD HH:mm:ss" }),
|
||||
winston.format.printf((info) => {
|
||||
const {
|
||||
timestamp, level, message, ...args
|
||||
} = info;
|
||||
|
||||
return `[${timestamp}]: [${level.toUpperCase()}] - ${message} ${Object.keys(args).length ? JSON.stringify(args, null, 2) : ""}`;
|
||||
}),
|
||||
)
|
||||
});
|
||||
|
||||
winston.addColors({
|
||||
info: "green",
|
||||
main: "gray",
|
||||
debug: "magenta",
|
||||
warn: "yellow",
|
||||
error: "red"
|
||||
});
|
||||
|
||||
let serverCount = 0;
|
||||
let shardCount = 0;
|
||||
let clusterCount = 0;
|
||||
let responseCount = 0;
|
||||
|
||||
let timeout;
|
||||
|
||||
process.on("message", (packet) => {
|
||||
if (packet.data?.type === "getCount") {
|
||||
process.send({
|
||||
type: "process:msg",
|
||||
data: {
|
||||
type: "countResponse",
|
||||
serverCount
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
function updateStats() {
|
||||
serverCount = 0;
|
||||
shardCount = 0;
|
||||
clusterCount = 0;
|
||||
responseCount = 0;
|
||||
return new Promise((resolve, reject) => {
|
||||
pm2.list((err, list) => {
|
||||
if (err) reject(err);
|
||||
const clusters = list.filter((v) => v.name.includes("esmBot-proc"));
|
||||
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"
|
||||
}
|
||||
});
|
||||
});
|
||||
});
|
||||
}
|
||||
|
||||
if (process.env.METRICS && process.env.METRICS !== "") {
|
||||
const servers = [];
|
||||
if (process.env.API_TYPE === "ws") {
|
||||
const imageHosts = JSON.parse(readFileSync(new URL("../../config/servers.json", import.meta.url), { encoding: "utf8" })).image;
|
||||
for (let { server } of imageHosts) {
|
||||
if (!server.includes(":")) {
|
||||
server += ":3762";
|
||||
}
|
||||
servers.push(server);
|
||||
}
|
||||
}
|
||||
const httpServer = createServer(async (req, res) => {
|
||||
if (req.method !== "GET") {
|
||||
res.statusCode = 405;
|
||||
return res.end("GET only");
|
||||
}
|
||||
res.write(`# HELP esmbot_command_count Number of times a command has been run
|
||||
# TYPE esmbot_command_count counter
|
||||
# HELP esmbot_server_count Number of servers/guilds the bot is in
|
||||
# TYPE esmbot_server_count gauge
|
||||
# HELP esmbot_shard_count Number of shards the bot has
|
||||
# TYPE esmbot_shard_count gauge
|
||||
`);
|
||||
if (database) {
|
||||
const counts = await database.getCounts();
|
||||
for (const [i, w] of Object.entries(counts)) {
|
||||
res.write(`esmbot_command_count{command="${i}"} ${w}\n`);
|
||||
}
|
||||
}
|
||||
|
||||
res.write(`esmbot_server_count ${serverCount}\n`);
|
||||
res.write(`esmbot_shard_count ${shardCount}\n`);
|
||||
res.end();
|
||||
});
|
||||
httpServer.listen(process.env.METRICS, () => {
|
||||
logger.log("info", `Serving metrics at ${process.env.METRICS}`);
|
||||
});
|
||||
}
|
||||
|
||||
setInterval(updateStats, 300000);
|
||||
|
||||
setTimeout(updateStats, 10000);
|
||||
|
||||
logger.info("Started esmBot management process.");
|
||||
|
||||
// from eris-fleet
|
||||
function calcShards(shards, procs) {
|
||||
if (procs < 2) return [shards];
|
||||
|
||||
const length = shards.length;
|
||||
const r = [];
|
||||
let i = 0;
|
||||
let size;
|
||||
|
||||
if (length % procs === 0) {
|
||||
size = Math.floor(length / procs);
|
||||
while (i < length) {
|
||||
r.push(shards.slice(i, (i += size)));
|
||||
}
|
||||
} else {
|
||||
while (i < length) {
|
||||
size = Math.ceil((length - i) / procs--);
|
||||
r.push(shards.slice(i, (i += size)));
|
||||
}
|
||||
}
|
||||
|
||||
return r;
|
||||
}
|
||||
|
||||
(async function init() {
|
||||
logger.main("Getting gateway connection data...");
|
||||
const client = new Client({
|
||||
auth: `Bot ${process.env.TOKEN}`,
|
||||
gateway: {
|
||||
concurrency: "auto",
|
||||
maxShards: "auto",
|
||||
presence: {
|
||||
status: "idle",
|
||||
activities: [{
|
||||
type: 0,
|
||||
name: "Starting esmBot..."
|
||||
}]
|
||||
},
|
||||
intents: []
|
||||
}
|
||||
});
|
||||
|
||||
const connectionData = await client.rest.getBotGateway();
|
||||
const cpuAmount = cpus().length;
|
||||
const procAmount = Math.min(connectionData.shards, cpuAmount);
|
||||
logger.main(`Obtained data, connecting with ${connectionData.shards} shard(s) across ${procAmount} process(es)...`);
|
||||
|
||||
const lastShard = connectionData.shards - 1;
|
||||
const shardArray = [];
|
||||
for (let i = 0; i <= lastShard; i++) {
|
||||
shardArray.push(i);
|
||||
}
|
||||
const shardArrays = calcShards(shardArray, procAmount);
|
||||
|
||||
for (let i = 0; i < shardArrays.length; i++) {
|
||||
await awaitStart(i, shardArrays);
|
||||
}
|
||||
})();
|
||||
|
||||
function awaitStart(i, shardArrays) {
|
||||
return new Promise((resolve) => {
|
||||
pm2.start({
|
||||
name: `esmBot-proc${i}`,
|
||||
script: "app.js",
|
||||
autorestart: true,
|
||||
exp_backoff_restart_delay: 1000,
|
||||
wait_ready: true,
|
||||
listen_timeout: 60000,
|
||||
watch: false,
|
||||
exec_mode: "cluster",
|
||||
instances: 1,
|
||||
env: {
|
||||
"SHARDS": JSON.stringify(shardArrays)
|
||||
}
|
||||
}, (err) => {
|
||||
if (err) {
|
||||
logger.error(`Failed to start esmBot process ${i}: ${err}`);
|
||||
process.exit(0);
|
||||
} else {
|
||||
logger.info(`Started esmBot process ${i}.`);
|
||||
resolve();
|
||||
}
|
||||
});
|
||||
});
|
||||
}
|
|
@ -1,270 +1,270 @@
|
|||
import * as logger from "./logger.js";
|
||||
import fs from "fs";
|
||||
import format from "format-duration";
|
||||
import { Shoukaku, Connectors } from "shoukaku";
|
||||
import { setTimeout } from "timers/promises";
|
||||
|
||||
export const players = new Map();
|
||||
export const queues = new Map();
|
||||
export const skipVotes = new Map();
|
||||
|
||||
export let manager;
|
||||
export let nodes = JSON.parse(fs.readFileSync(new URL("../config/servers.json", import.meta.url), { encoding: "utf8" })).lava;
|
||||
export let connected = false;
|
||||
|
||||
export function connect(client) {
|
||||
manager = new Shoukaku(new Connectors.OceanicJS(client), nodes, { moveOnDisconnect: true, resume: true, reconnectInterval: 500, reconnectTries: 1 });
|
||||
manager.on("error", (node, error) => {
|
||||
logger.error(`An error occurred on Lavalink node ${node}: ${error}`);
|
||||
});
|
||||
manager.on("debug", (node, info) => {
|
||||
logger.debug(`Debug event from Lavalink node ${node}: ${info}`);
|
||||
});
|
||||
manager.once("ready", () => {
|
||||
logger.log(`Successfully connected to ${manager.nodes.size} Lavalink node(s).`);
|
||||
connected = true;
|
||||
});
|
||||
}
|
||||
|
||||
export async function reload(client) {
|
||||
if (!manager) connect(client);
|
||||
const activeNodes = manager.nodes;
|
||||
const json = await fs.promises.readFile(new URL("../config/servers.json", import.meta.url), { encoding: "utf8" });
|
||||
nodes = JSON.parse(json).lava;
|
||||
const names = nodes.map((a) => a.name);
|
||||
for (const name in activeNodes) {
|
||||
if (!names.includes(name)) {
|
||||
manager.removeNode(name);
|
||||
}
|
||||
}
|
||||
for (const node of nodes) {
|
||||
if (!activeNodes.has(node.name)) {
|
||||
manager.addNode(node);
|
||||
}
|
||||
}
|
||||
if (!manager.nodes.size) connected = false;
|
||||
return manager.nodes.size;
|
||||
}
|
||||
|
||||
export async function play(client, sound, options, music = false) {
|
||||
if (!connected) return { content: "I'm not connected to any audio servers!", flags: 64 };
|
||||
if (!manager) return { content: "The sound commands are still starting up!", flags: 64 };
|
||||
if (!options.channel.guild) return { content: "This command only works in servers!", flags: 64 };
|
||||
if (!options.member.voiceState) return { content: "You need to be in a voice channel first!", flags: 64 };
|
||||
if (!options.channel.guild.permissionsOf(client.user.id.toString()).has("CONNECT")) return { content: "I can't join this voice channel!", flags: 64 };
|
||||
const voiceChannel = options.channel.guild.channels.get(options.member.voiceState.channelID) ?? await client.rest.channels.get(options.member.voiceState.channelID);
|
||||
if (!voiceChannel.permissionsOf(client.user.id.toString()).has("CONNECT")) return { content: "I don't have permission to join this voice channel!", flags: 64 };
|
||||
if (!music && manager.players.has(options.channel.guildID)) return { content: "I can't play a sound effect while other audio is playing!", flags: 64 };
|
||||
const node = manager.getNode();
|
||||
if (!music && !nodes.filter(obj => obj.name === node.name)[0].local) {
|
||||
sound = sound.replace(/\.\//, "https://raw.githubusercontent.com/esmBot/esmBot/master/");
|
||||
}
|
||||
let response;
|
||||
try {
|
||||
response = await node.rest.resolve(sound);
|
||||
if (!response) return { content: "🔊 I couldn't get a response from the audio server.", flags: 64 };
|
||||
if (response.loadType === "NO_MATCHES" || response.loadType === "LOAD_FAILED") return { content: "I couldn't find that song!", flags: 64 };
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
return { content: "🔊 Hmmm, seems that all of the audio servers are down. Try again in a bit.", flags: 64 };
|
||||
}
|
||||
const oldQueue = queues.get(voiceChannel.guildID);
|
||||
if (!response.tracks || response.tracks.length === 0) return { content: "I couldn't find that song!", flags: 64 };
|
||||
if (process.env.YT_DISABLED === "true" && response.tracks[0].info.sourceName === "youtube") return "YouTube playback is disabled on this instance.";
|
||||
if (music) {
|
||||
const sortedTracks = response.tracks.map((val) => { return val.track; });
|
||||
const playlistTracks = response.playlistInfo.selectedTrack ? sortedTracks : [sortedTracks[0]];
|
||||
queues.set(voiceChannel.guildID, oldQueue ? [...oldQueue, ...playlistTracks] : playlistTracks);
|
||||
}
|
||||
const playerMeta = players.get(options.channel.guildID);
|
||||
let player;
|
||||
if (node.players.has(voiceChannel.guildID)) {
|
||||
player = node.players.get(voiceChannel.guildID);
|
||||
} else if (playerMeta?.player) {
|
||||
const storedState = playerMeta?.player?.connection.state;
|
||||
if (storedState && storedState === 1) {
|
||||
player = playerMeta?.player;
|
||||
}
|
||||
}
|
||||
const connection = player ?? await node.joinChannel({
|
||||
guildId: voiceChannel.guildID,
|
||||
channelId: voiceChannel.id,
|
||||
shardId: voiceChannel.guild.shard.id,
|
||||
deaf: true
|
||||
});
|
||||
|
||||
if (oldQueue?.length && music) {
|
||||
return `Your ${response.playlistInfo.name ? "playlist" : "tune"} \`${response.playlistInfo.name ? response.playlistInfo.name.trim() : (response.tracks[0].info.title !== "" ? response.tracks[0].info.title.trim() : "(blank)")}\` has been added to the queue!`;
|
||||
} else {
|
||||
nextSong(client, options, connection, response.tracks[0].track, response.tracks[0].info, music, voiceChannel, playerMeta?.host ?? options.member.id, playerMeta?.loop ?? false, playerMeta?.shuffle ?? false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export async function nextSong(client, options, connection, track, info, music, voiceChannel, host, loop = false, shuffle = false, lastTrack = null) {
|
||||
skipVotes.delete(voiceChannel.guildID);
|
||||
const parts = Math.floor((0 / info.length) * 10);
|
||||
let playingMessage;
|
||||
if (music && lastTrack === track && players.has(voiceChannel.guildID)) {
|
||||
playingMessage = players.get(voiceChannel.guildID).playMessage;
|
||||
} else {
|
||||
try {
|
||||
const content = !music ? { content: "🔊 Playing sound..." } : {
|
||||
embeds: [{
|
||||
color: 16711680,
|
||||
author: {
|
||||
name: "Now Playing",
|
||||
iconURL: client.user.avatarURL()
|
||||
},
|
||||
fields: [{
|
||||
name: "ℹ️ Title",
|
||||
value: info.title?.trim() !== "" ? info.title : "(blank)"
|
||||
},
|
||||
{
|
||||
name: "🎤 Artist",
|
||||
value: info.author?.trim() !== "" ? info.author : "(blank)"
|
||||
},
|
||||
{
|
||||
name: "💬 Channel",
|
||||
value: voiceChannel.name
|
||||
},
|
||||
{
|
||||
name: "🌐 Node",
|
||||
value: connection.node?.name ?? "Unknown"
|
||||
},
|
||||
{
|
||||
name: `${"▬".repeat(parts)}🔘${"▬".repeat(10 - parts)}`,
|
||||
value: `0:00/${info.isStream ? "∞" : format(info.length)}`
|
||||
}]
|
||||
}]
|
||||
};
|
||||
if (options.type === "classic") {
|
||||
playingMessage = await client.rest.channels.createMessage(options.channel.id, content);
|
||||
} else {
|
||||
if ((Date.now() - options.interaction.createdAt) >= 900000) { // discord interactions are only valid for 15 minutes
|
||||
playingMessage = await client.rest.channels.createMessage(options.channel.id, content);
|
||||
} else if (lastTrack && lastTrack !== track) {
|
||||
playingMessage = await options.interaction.createFollowup(content);
|
||||
} else {
|
||||
playingMessage = await options.interaction[options.interaction.acknowledged ? "editOriginal" : "createMessage"](content);
|
||||
if (!playingMessage) playingMessage = await options.interaction.getOriginal();
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
connection.removeAllListeners("exception");
|
||||
connection.removeAllListeners("stuck");
|
||||
connection.removeAllListeners("end");
|
||||
connection.setVolume(0.70);
|
||||
connection.playTrack({ track });
|
||||
players.set(voiceChannel.guildID, { player: connection, type: music ? "music" : "sound", host, voiceChannel, originalChannel: options.channel, loop, shuffle, playMessage: playingMessage });
|
||||
connection.once("exception", (exception) => errHandle(exception, client, connection, playingMessage, voiceChannel, options));
|
||||
connection.on("stuck", () => {
|
||||
const nodeName = manager.getNode().name;
|
||||
connection.move(nodeName);
|
||||
connection.resume();
|
||||
});
|
||||
connection.on("end", async (data) => {
|
||||
if (data.reason === "REPLACED") return;
|
||||
let queue = queues.get(voiceChannel.guildID);
|
||||
const player = players.get(voiceChannel.guildID);
|
||||
if (player && process.env.STAYVC === "true") {
|
||||
player.type = "idle";
|
||||
players.set(voiceChannel.guildID, player);
|
||||
}
|
||||
let newQueue;
|
||||
if (player?.shuffle) {
|
||||
if (player.loop) {
|
||||
queue.push(queue.shift());
|
||||
} else {
|
||||
queue = queue.slice(1);
|
||||
}
|
||||
queue.unshift(queue.splice(Math.floor(Math.random() * queue.length), 1)[0]);
|
||||
newQueue = queue;
|
||||
} else if (player?.loop) {
|
||||
queue.push(queue.shift());
|
||||
newQueue = queue;
|
||||
} else {
|
||||
newQueue = queue ? queue.slice(1) : [];
|
||||
}
|
||||
queues.set(voiceChannel.guildID, newQueue);
|
||||
if (newQueue.length !== 0) {
|
||||
const newTrack = await connection.node.rest.decode(newQueue[0]);
|
||||
nextSong(client, options, connection, newQueue[0], newTrack, music, voiceChannel, host, player.loop, player.shuffle, track);
|
||||
try {
|
||||
if (options.type === "classic") {
|
||||
if (newQueue[0] !== track && playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete();
|
||||
if (newQueue[0] !== track && player.playMessage.channel.messages.has(player.playMessage.id)) await player.playMessage.delete();
|
||||
}
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
} else if (process.env.STAYVC !== "true") {
|
||||
await setTimeout(400);
|
||||
connection.node.leaveChannel(voiceChannel.guildID);
|
||||
players.delete(voiceChannel.guildID);
|
||||
queues.delete(voiceChannel.guildID);
|
||||
skipVotes.delete(voiceChannel.guildID);
|
||||
try {
|
||||
const content = `🔊 The voice channel session in \`${voiceChannel.name}\` has ended.`;
|
||||
if (options.type === "classic") {
|
||||
await client.rest.channels.createMessage(options.channel.id, { content });
|
||||
} else {
|
||||
if ((Date.now() - options.interaction.createdAt) >= 900000) {
|
||||
await client.rest.channels.createMessage(options.channel.id, { content });
|
||||
} else {
|
||||
await options.interaction.createFollowup({ content });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
if (options.type === "classic") {
|
||||
try {
|
||||
if (playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete();
|
||||
if (player?.playMessage.channel.messages.has(player.playMessage.id)) await player.playMessage.delete();
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function errHandle(exception, client, connection, playingMessage, voiceChannel, options, closed) {
|
||||
try {
|
||||
if (playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete();
|
||||
const playMessage = players.get(voiceChannel.guildID).playMessage;
|
||||
if (playMessage.channel.messages.has(playMessage.id)) await playMessage.delete();
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
players.delete(voiceChannel.guildID);
|
||||
queues.delete(voiceChannel.guildID);
|
||||
skipVotes.delete(voiceChannel.guildID);
|
||||
logger.error(exception);
|
||||
try {
|
||||
connection.node.leaveChannel(voiceChannel.guildID);
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
connection.removeAllListeners("exception");
|
||||
connection.removeAllListeners("stuck");
|
||||
connection.removeAllListeners("end");
|
||||
try {
|
||||
const content = closed ? `🔊 I got disconnected by Discord and tried to reconnect; however, I got this error instead:\n\`\`\`${exception}\`\`\`` : `🔊 Looks like there was an error regarding sound playback:\n\`\`\`${exception.type}: ${exception.error}\`\`\``;
|
||||
if (options.type === "classic") {
|
||||
await client.rest.channels.createMessage(playingMessage.channel.id, { content });
|
||||
} else {
|
||||
if ((Date.now() - options.interaction.createdAt) >= 900000) {
|
||||
await client.rest.channels.createMessage(options.channel.id, { content });
|
||||
} else {
|
||||
await options.interaction.createFollowup({ content });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
import * as logger from "./logger.js";
|
||||
import fs from "fs";
|
||||
import format from "format-duration";
|
||||
import { Shoukaku, Connectors } from "shoukaku";
|
||||
import { setTimeout } from "timers/promises";
|
||||
|
||||
export const players = new Map();
|
||||
export const queues = new Map();
|
||||
export const skipVotes = new Map();
|
||||
|
||||
export let manager;
|
||||
export let nodes = JSON.parse(fs.readFileSync(new URL("../config/servers.json", import.meta.url), { encoding: "utf8" })).lava;
|
||||
export let connected = false;
|
||||
|
||||
export function connect(client) {
|
||||
manager = new Shoukaku(new Connectors.OceanicJS(client), nodes, { moveOnDisconnect: true, resume: true, reconnectInterval: 500, reconnectTries: 1 });
|
||||
manager.on("error", (node, error) => {
|
||||
logger.error(`An error occurred on Lavalink node ${node}: ${error}`);
|
||||
});
|
||||
manager.on("debug", (node, info) => {
|
||||
logger.debug(`Debug event from Lavalink node ${node}: ${info}`);
|
||||
});
|
||||
manager.once("ready", () => {
|
||||
logger.log(`Successfully connected to ${manager.nodes.size} Lavalink node(s).`);
|
||||
connected = true;
|
||||
});
|
||||
}
|
||||
|
||||
export async function reload(client) {
|
||||
if (!manager) connect(client);
|
||||
const activeNodes = manager.nodes;
|
||||
const json = await fs.promises.readFile(new URL("../config/servers.json", import.meta.url), { encoding: "utf8" });
|
||||
nodes = JSON.parse(json).lava;
|
||||
const names = nodes.map((a) => a.name);
|
||||
for (const name in activeNodes) {
|
||||
if (!names.includes(name)) {
|
||||
manager.removeNode(name);
|
||||
}
|
||||
}
|
||||
for (const node of nodes) {
|
||||
if (!activeNodes.has(node.name)) {
|
||||
manager.addNode(node);
|
||||
}
|
||||
}
|
||||
if (!manager.nodes.size) connected = false;
|
||||
return manager.nodes.size;
|
||||
}
|
||||
|
||||
export async function play(client, sound, options, music = false) {
|
||||
if (!connected) return { content: "I'm not connected to any audio servers!", flags: 64 };
|
||||
if (!manager) return { content: "The sound commands are still starting up!", flags: 64 };
|
||||
if (!options.channel.guild) return { content: "This command only works in servers!", flags: 64 };
|
||||
if (!options.member.voiceState) return { content: "You need to be in a voice channel first!", flags: 64 };
|
||||
if (!options.channel.guild.permissionsOf(client.user.id.toString()).has("CONNECT")) return { content: "I can't join this voice channel!", flags: 64 };
|
||||
const voiceChannel = options.channel.guild.channels.get(options.member.voiceState.channelID) ?? await client.rest.channels.get(options.member.voiceState.channelID);
|
||||
if (!voiceChannel.permissionsOf(client.user.id.toString()).has("CONNECT")) return { content: "I don't have permission to join this voice channel!", flags: 64 };
|
||||
if (!music && manager.players.has(options.channel.guildID)) return { content: "I can't play a sound effect while other audio is playing!", flags: 64 };
|
||||
const node = manager.getNode();
|
||||
if (!music && !nodes.filter(obj => obj.name === node.name)[0].local) {
|
||||
sound = sound.replace(/\.\//, "https://raw.githubusercontent.com/esmBot/esmBot/master/");
|
||||
}
|
||||
let response;
|
||||
try {
|
||||
response = await node.rest.resolve(sound);
|
||||
if (!response) return { content: "🔊 I couldn't get a response from the audio server.", flags: 64 };
|
||||
if (response.loadType === "NO_MATCHES" || response.loadType === "LOAD_FAILED") return { content: "I couldn't find that song!", flags: 64 };
|
||||
} catch (e) {
|
||||
logger.error(e);
|
||||
return { content: "🔊 Hmmm, seems that all of the audio servers are down. Try again in a bit.", flags: 64 };
|
||||
}
|
||||
const oldQueue = queues.get(voiceChannel.guildID);
|
||||
if (!response.tracks || response.tracks.length === 0) return { content: "I couldn't find that song!", flags: 64 };
|
||||
if (process.env.YT_DISABLED === "true" && response.tracks[0].info.sourceName === "youtube") return "YouTube playback is disabled on this instance.";
|
||||
if (music) {
|
||||
const sortedTracks = response.tracks.map((val) => { return val.track; });
|
||||
const playlistTracks = response.playlistInfo.selectedTrack ? sortedTracks : [sortedTracks[0]];
|
||||
queues.set(voiceChannel.guildID, oldQueue ? [...oldQueue, ...playlistTracks] : playlistTracks);
|
||||
}
|
||||
const playerMeta = players.get(options.channel.guildID);
|
||||
let player;
|
||||
if (node.players.has(voiceChannel.guildID)) {
|
||||
player = node.players.get(voiceChannel.guildID);
|
||||
} else if (playerMeta?.player) {
|
||||
const storedState = playerMeta?.player?.connection.state;
|
||||
if (storedState && storedState === 1) {
|
||||
player = playerMeta?.player;
|
||||
}
|
||||
}
|
||||
const connection = player ?? await node.joinChannel({
|
||||
guildId: voiceChannel.guildID,
|
||||
channelId: voiceChannel.id,
|
||||
shardId: voiceChannel.guild.shard.id,
|
||||
deaf: true
|
||||
});
|
||||
|
||||
if (oldQueue?.length && music) {
|
||||
return `Your ${response.playlistInfo.name ? "playlist" : "tune"} \`${response.playlistInfo.name ? response.playlistInfo.name.trim() : (response.tracks[0].info.title !== "" ? response.tracks[0].info.title.trim() : "(blank)")}\` has been added to the queue!`;
|
||||
} else {
|
||||
nextSong(client, options, connection, response.tracks[0].track, response.tracks[0].info, music, voiceChannel, playerMeta?.host ?? options.member.id, playerMeta?.loop ?? false, playerMeta?.shuffle ?? false);
|
||||
return;
|
||||
}
|
||||
}
|
||||
|
||||
export async function nextSong(client, options, connection, track, info, music, voiceChannel, host, loop = false, shuffle = false, lastTrack = null) {
|
||||
skipVotes.delete(voiceChannel.guildID);
|
||||
const parts = Math.floor((0 / info.length) * 10);
|
||||
let playingMessage;
|
||||
if (music && lastTrack === track && players.has(voiceChannel.guildID)) {
|
||||
playingMessage = players.get(voiceChannel.guildID).playMessage;
|
||||
} else {
|
||||
try {
|
||||
const content = !music ? { content: "🔊 Playing sound..." } : {
|
||||
embeds: [{
|
||||
color: 16711680,
|
||||
author: {
|
||||
name: "Now Playing",
|
||||
iconURL: client.user.avatarURL()
|
||||
},
|
||||
fields: [{
|
||||
name: "ℹ️ Title",
|
||||
value: info.title?.trim() !== "" ? info.title : "(blank)"
|
||||
},
|
||||
{
|
||||
name: "🎤 Artist",
|
||||
value: info.author?.trim() !== "" ? info.author : "(blank)"
|
||||
},
|
||||
{
|
||||
name: "💬 Channel",
|
||||
value: voiceChannel.name
|
||||
},
|
||||
{
|
||||
name: "🌐 Node",
|
||||
value: connection.node?.name ?? "Unknown"
|
||||
},
|
||||
{
|
||||
name: `${"▬".repeat(parts)}🔘${"▬".repeat(10 - parts)}`,
|
||||
value: `0:00/${info.isStream ? "∞" : format(info.length)}`
|
||||
}]
|
||||
}]
|
||||
};
|
||||
if (options.type === "classic") {
|
||||
playingMessage = await client.rest.channels.createMessage(options.channel.id, content);
|
||||
} else {
|
||||
if ((Date.now() - options.interaction.createdAt) >= 900000) { // discord interactions are only valid for 15 minutes
|
||||
playingMessage = await client.rest.channels.createMessage(options.channel.id, content);
|
||||
} else if (lastTrack && lastTrack !== track) {
|
||||
playingMessage = await options.interaction.createFollowup(content);
|
||||
} else {
|
||||
playingMessage = await options.interaction[options.interaction.acknowledged ? "editOriginal" : "createMessage"](content);
|
||||
if (!playingMessage) playingMessage = await options.interaction.getOriginal();
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
connection.removeAllListeners("exception");
|
||||
connection.removeAllListeners("stuck");
|
||||
connection.removeAllListeners("end");
|
||||
connection.setVolume(0.70);
|
||||
connection.playTrack({ track });
|
||||
players.set(voiceChannel.guildID, { player: connection, type: music ? "music" : "sound", host, voiceChannel, originalChannel: options.channel, loop, shuffle, playMessage: playingMessage });
|
||||
connection.once("exception", (exception) => errHandle(exception, client, connection, playingMessage, voiceChannel, options));
|
||||
connection.on("stuck", () => {
|
||||
const nodeName = manager.getNode().name;
|
||||
connection.move(nodeName);
|
||||
connection.resume();
|
||||
});
|
||||
connection.on("end", async (data) => {
|
||||
if (data.reason === "REPLACED") return;
|
||||
let queue = queues.get(voiceChannel.guildID);
|
||||
const player = players.get(voiceChannel.guildID);
|
||||
if (player && process.env.STAYVC === "true") {
|
||||
player.type = "idle";
|
||||
players.set(voiceChannel.guildID, player);
|
||||
}
|
||||
let newQueue;
|
||||
if (player?.shuffle) {
|
||||
if (player.loop) {
|
||||
queue.push(queue.shift());
|
||||
} else {
|
||||
queue = queue.slice(1);
|
||||
}
|
||||
queue.unshift(queue.splice(Math.floor(Math.random() * queue.length), 1)[0]);
|
||||
newQueue = queue;
|
||||
} else if (player?.loop) {
|
||||
queue.push(queue.shift());
|
||||
newQueue = queue;
|
||||
} else {
|
||||
newQueue = queue ? queue.slice(1) : [];
|
||||
}
|
||||
queues.set(voiceChannel.guildID, newQueue);
|
||||
if (newQueue.length !== 0) {
|
||||
const newTrack = await connection.node.rest.decode(newQueue[0]);
|
||||
nextSong(client, options, connection, newQueue[0], newTrack, music, voiceChannel, host, player.loop, player.shuffle, track);
|
||||
try {
|
||||
if (options.type === "classic") {
|
||||
if (newQueue[0] !== track && playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete();
|
||||
if (newQueue[0] !== track && player.playMessage.channel.messages.has(player.playMessage.id)) await player.playMessage.delete();
|
||||
}
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
} else if (process.env.STAYVC !== "true") {
|
||||
await setTimeout(400);
|
||||
connection.node.leaveChannel(voiceChannel.guildID);
|
||||
players.delete(voiceChannel.guildID);
|
||||
queues.delete(voiceChannel.guildID);
|
||||
skipVotes.delete(voiceChannel.guildID);
|
||||
try {
|
||||
const content = `🔊 The voice channel session in \`${voiceChannel.name}\` has ended.`;
|
||||
if (options.type === "classic") {
|
||||
await client.rest.channels.createMessage(options.channel.id, { content });
|
||||
} else {
|
||||
if ((Date.now() - options.interaction.createdAt) >= 900000) {
|
||||
await client.rest.channels.createMessage(options.channel.id, { content });
|
||||
} else {
|
||||
await options.interaction.createFollowup({ content });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
if (options.type === "classic") {
|
||||
try {
|
||||
if (playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete();
|
||||
if (player?.playMessage.channel.messages.has(player.playMessage.id)) await player.playMessage.delete();
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
export async function errHandle(exception, client, connection, playingMessage, voiceChannel, options, closed) {
|
||||
try {
|
||||
if (playingMessage.channel.messages.has(playingMessage.id)) await playingMessage.delete();
|
||||
const playMessage = players.get(voiceChannel.guildID).playMessage;
|
||||
if (playMessage.channel.messages.has(playMessage.id)) await playMessage.delete();
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
players.delete(voiceChannel.guildID);
|
||||
queues.delete(voiceChannel.guildID);
|
||||
skipVotes.delete(voiceChannel.guildID);
|
||||
logger.error(exception);
|
||||
try {
|
||||
connection.node.leaveChannel(voiceChannel.guildID);
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
connection.removeAllListeners("exception");
|
||||
connection.removeAllListeners("stuck");
|
||||
connection.removeAllListeners("end");
|
||||
try {
|
||||
const content = closed ? `🔊 I got disconnected by Discord and tried to reconnect; however, I got this error instead:\n\`\`\`${exception}\`\`\`` : `🔊 Looks like there was an error regarding sound playback:\n\`\`\`${exception.type}: ${exception.error}\`\`\``;
|
||||
if (options.type === "classic") {
|
||||
await client.rest.channels.createMessage(playingMessage.channel.id, { content });
|
||||
} else {
|
||||
if ((Date.now() - options.interaction.createdAt) >= 900000) {
|
||||
await client.rest.channels.createMessage(options.channel.id, { content });
|
||||
} else {
|
||||
await options.interaction.createFollowup({ content });
|
||||
}
|
||||
}
|
||||
} catch {
|
||||
// no-op
|
||||
}
|
||||
}
|
|
@ -1,98 +1,98 @@
|
|||
import * as logger from "../utils/logger.js";
|
||||
import { readdir, lstat, rm, writeFile, stat } from "fs/promises";
|
||||
|
||||
let dirSizeCache;
|
||||
|
||||
export async function upload(client, result, context, interaction = false) {
|
||||
const filename = `${Math.random().toString(36).substring(2, 15)}.${result.name.split(".")[1]}`;
|
||||
await writeFile(`${process.env.TEMPDIR}/${filename}`, result.contents);
|
||||
const imageURL = `${process.env.TMP_DOMAIN || "https://tmp.esmbot.net"}/${filename}`;
|
||||
const payload = {
|
||||
embeds: [{
|
||||
color: 16711680,
|
||||
title: "Here's your image!",
|
||||
url: imageURL,
|
||||
image: {
|
||||
url: imageURL
|
||||
},
|
||||
footer: {
|
||||
text: "The result image was more than 8MB in size, so it was uploaded to an external site instead."
|
||||
},
|
||||
}]
|
||||
};
|
||||
if (interaction) {
|
||||
await context[context.acknowledged ? "editOriginal" : "createMessage"](payload);
|
||||
} else {
|
||||
await client.rest.channels.createMessage(context.channelID, Object.assign(payload, {
|
||||
messageReference: {
|
||||
channelID: context.channelID,
|
||||
messageID: context.id,
|
||||
guildID: context.guildID ?? undefined,
|
||||
failIfNotExists: false
|
||||
},
|
||||
allowedMentions: {
|
||||
repliedUser: false
|
||||
}
|
||||
}));
|
||||
}
|
||||
if (process.env.THRESHOLD) {
|
||||
const size = dirSizeCache + result.contents.length;
|
||||
dirSizeCache = size;
|
||||
await removeOldImages(size);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeOldImages(size) {
|
||||
if (size > process.env.THRESHOLD) {
|
||||
const files = (await readdir(process.env.TEMPDIR)).map((file) => {
|
||||
return lstat(`${process.env.TEMPDIR}/${file}`).then((stats) => {
|
||||
if (stats.isSymbolicLink()) return;
|
||||
return {
|
||||
name: file,
|
||||
size: stats.size,
|
||||
ctime: stats.ctime
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const resolvedFiles = await Promise.all(files);
|
||||
const oldestFiles = resolvedFiles.filter(Boolean).sort((a, b) => a.ctime - b.ctime);
|
||||
|
||||
do {
|
||||
if (!oldestFiles[0]) break;
|
||||
await rm(`${process.env.TEMPDIR}/${oldestFiles[0].name}`);
|
||||
logger.log(`Removed oldest image file: ${oldestFiles[0].name}`);
|
||||
size -= oldestFiles[0].size;
|
||||
oldestFiles.shift();
|
||||
} while (size > process.env.THRESHOLD);
|
||||
|
||||
const newSize = oldestFiles.reduce((a, b) => {
|
||||
return a + b.size;
|
||||
}, 0);
|
||||
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;
|
||||
import * as logger from "../utils/logger.js";
|
||||
import { readdir, lstat, rm, writeFile, stat } from "fs/promises";
|
||||
|
||||
let dirSizeCache;
|
||||
|
||||
export async function upload(client, result, context, interaction = false) {
|
||||
const filename = `${Math.random().toString(36).substring(2, 15)}.${result.name.split(".")[1]}`;
|
||||
await writeFile(`${process.env.TEMPDIR}/${filename}`, result.contents);
|
||||
const imageURL = `${process.env.TMP_DOMAIN || "https://tmp.esmbot.net"}/${filename}`;
|
||||
const payload = {
|
||||
embeds: [{
|
||||
color: 16711680,
|
||||
title: "Here's your image!",
|
||||
url: imageURL,
|
||||
image: {
|
||||
url: imageURL
|
||||
},
|
||||
footer: {
|
||||
text: "The result image was more than 8MB in size, so it was uploaded to an external site instead."
|
||||
},
|
||||
}]
|
||||
};
|
||||
if (interaction) {
|
||||
await context[context.acknowledged ? "editOriginal" : "createMessage"](payload);
|
||||
} else {
|
||||
await client.rest.channels.createMessage(context.channelID, Object.assign(payload, {
|
||||
messageReference: {
|
||||
channelID: context.channelID,
|
||||
messageID: context.id,
|
||||
guildID: context.guildID ?? undefined,
|
||||
failIfNotExists: false
|
||||
},
|
||||
allowedMentions: {
|
||||
repliedUser: false
|
||||
}
|
||||
}));
|
||||
}
|
||||
if (process.env.THRESHOLD) {
|
||||
const size = dirSizeCache + result.contents.length;
|
||||
dirSizeCache = size;
|
||||
await removeOldImages(size);
|
||||
}
|
||||
}
|
||||
|
||||
async function removeOldImages(size) {
|
||||
if (size > process.env.THRESHOLD) {
|
||||
const files = (await readdir(process.env.TEMPDIR)).map((file) => {
|
||||
return lstat(`${process.env.TEMPDIR}/${file}`).then((stats) => {
|
||||
if (stats.isSymbolicLink()) return;
|
||||
return {
|
||||
name: file,
|
||||
size: stats.size,
|
||||
ctime: stats.ctime
|
||||
};
|
||||
});
|
||||
});
|
||||
|
||||
const resolvedFiles = await Promise.all(files);
|
||||
const oldestFiles = resolvedFiles.filter(Boolean).sort((a, b) => a.ctime - b.ctime);
|
||||
|
||||
do {
|
||||
if (!oldestFiles[0]) break;
|
||||
await rm(`${process.env.TEMPDIR}/${oldestFiles[0].name}`);
|
||||
logger.log(`Removed oldest image file: ${oldestFiles[0].name}`);
|
||||
size -= oldestFiles[0].size;
|
||||
oldestFiles.shift();
|
||||
} while (size > process.env.THRESHOLD);
|
||||
|
||||
const newSize = oldestFiles.reduce((a, b) => {
|
||||
return a + b.size;
|
||||
}, 0);
|
||||
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;
|
||||
}
|
|
@ -1,16 +1,16 @@
|
|||
export default (string) => {
|
||||
const protocolAndDomainRE = /^(?:\w+:)?\/\/(\S+)$/;
|
||||
const domainRE = /^[^\s.]+\.\S{2,}$/;
|
||||
const match = string.match(protocolAndDomainRE);
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
const everythingAfterProtocol = match[1];
|
||||
if (!everythingAfterProtocol) {
|
||||
return false;
|
||||
}
|
||||
if (domainRE.test(everythingAfterProtocol)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
export default (string) => {
|
||||
const protocolAndDomainRE = /^(?:\w+:)?\/\/(\S+)$/;
|
||||
const domainRE = /^[^\s.]+\.\S{2,}$/;
|
||||
const match = string.match(protocolAndDomainRE);
|
||||
if (!match) {
|
||||
return false;
|
||||
}
|
||||
const everythingAfterProtocol = match[1];
|
||||
if (!everythingAfterProtocol) {
|
||||
return false;
|
||||
}
|
||||
if (domainRE.test(everythingAfterProtocol)) {
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
};
|
||||
|
|
|
@ -1,12 +1,12 @@
|
|||
export default (str) => {
|
||||
var regexString = ".{1,15}([\\s\u200B]+|$)|[^\\s\u200B]+?([\\s\u200B]+|$)";
|
||||
var re = new RegExp(regexString, "g");
|
||||
var lines = str.match(re) || [];
|
||||
var result = lines.map((line) => {
|
||||
if (line.slice(-1) === "\n") {
|
||||
line = line.slice(0, line.length - 1);
|
||||
}
|
||||
return line;
|
||||
}).join("\n");
|
||||
return result;
|
||||
export default (str) => {
|
||||
var regexString = ".{1,15}([\\s\u200B]+|$)|[^\\s\u200B]+?([\\s\u200B]+|$)";
|
||||
var re = new RegExp(regexString, "g");
|
||||
var lines = str.match(re) || [];
|
||||
var result = lines.map((line) => {
|
||||
if (line.slice(-1) === "\n") {
|
||||
line = line.slice(0, line.length - 1);
|
||||
}
|
||||
return line;
|
||||
}).join("\n");
|
||||
return result;
|
||||
};
|
Loading…
Add table
Add a link
Reference in a new issue