233 lines
7.3 KiB
TypeScript
233 lines
7.3 KiB
TypeScript
import "./loadEnv";
|
|
|
|
import path from "path";
|
|
import yaml from "js-yaml";
|
|
|
|
import fs from "fs";
|
|
import { Knub, PluginError } from "knub";
|
|
import { SimpleError } from "./SimpleError";
|
|
|
|
import { Configs } from "./data/Configs";
|
|
// Always use UTC internally
|
|
// This is also enforced for the database in data/db.ts
|
|
import moment from "moment-timezone";
|
|
import { Client, TextChannel } from "eris";
|
|
import { connect } from "./data/db";
|
|
import { baseGuildPlugins, globalPlugins, guildPlugins } from "./plugins/availablePlugins";
|
|
import { errorMessage, isDiscordHTTPError, isDiscordRESTError, MINUTES, successMessage } from "./utils";
|
|
import { startUptimeCounter } from "./uptime";
|
|
import { AllowedGuilds } from "./data/AllowedGuilds";
|
|
import { ZeppelinGlobalConfig, ZeppelinGuildConfig } from "./types";
|
|
import { RecoverablePluginError } from "./RecoverablePluginError";
|
|
import { GuildLogs } from "./data/GuildLogs";
|
|
import { LogType } from "./data/LogType";
|
|
import { ZeppelinPlugin } from "./plugins/ZeppelinPlugin";
|
|
import { logger } from "./logger";
|
|
import { PluginLoadError } from "knub/dist/plugins/PluginLoadError";
|
|
|
|
const fsp = fs.promises;
|
|
|
|
if (!process.env.KEY) {
|
|
// tslint:disable-next-line:no-console
|
|
console.error("Project root .env with KEY is required!");
|
|
process.exit(1);
|
|
}
|
|
|
|
declare global {
|
|
// This is here so TypeScript doesn't give an error when importing twemoji
|
|
// since one of the signatures of twemoji.parse() takes an HTMLElement but
|
|
// we're not in a browser environment so including the DOM lib would not make
|
|
// sense
|
|
type HTMLElement = unknown;
|
|
}
|
|
|
|
// Error handling
|
|
let recentPluginErrors = 0;
|
|
const RECENT_PLUGIN_ERROR_EXIT_THRESHOLD = 5;
|
|
|
|
let recentDiscordErrors = 0;
|
|
const RECENT_DISCORD_ERROR_EXIT_THRESHOLD = 5;
|
|
|
|
setInterval(() => (recentPluginErrors = Math.max(0, recentPluginErrors - 1)), 2500);
|
|
setInterval(() => (recentDiscordErrors = Math.max(0, recentDiscordErrors - 1)), 2500);
|
|
|
|
if (process.env.NODE_ENV === "production") {
|
|
const errorHandler = err => {
|
|
const guildName = err.guild?.name || "Global";
|
|
const guildId = err.guild?.id || "0";
|
|
|
|
if (err instanceof RecoverablePluginError) {
|
|
// Recoverable plugin errors can be, well, recovered from.
|
|
// Log it in the console as a warning and post a warning to the guild's log.
|
|
|
|
// tslint:disable:no-console
|
|
console.warn(`${guildName}: [${err.code}] ${err.message}`);
|
|
|
|
if (err.guild) {
|
|
const logs = new GuildLogs(err.guild.id);
|
|
logs.log(LogType.BOT_ALERT, { body: `\`[${err.code}]\` ${err.message}` });
|
|
}
|
|
|
|
return;
|
|
}
|
|
|
|
if (err instanceof PluginLoadError) {
|
|
// tslint:disable:no-console
|
|
console.warn(`${guildName} (${guildId}): Failed to load plugin '${err.pluginName}': ${err.message}`);
|
|
return;
|
|
}
|
|
|
|
// tslint:disable:no-console
|
|
console.error(err);
|
|
|
|
if (err instanceof PluginError) {
|
|
// Tolerate a few recent plugin errors before crashing
|
|
if (++recentPluginErrors >= RECENT_PLUGIN_ERROR_EXIT_THRESHOLD) {
|
|
console.error(`Exiting after ${RECENT_PLUGIN_ERROR_EXIT_THRESHOLD} plugin errors`);
|
|
process.exit(1);
|
|
}
|
|
} else if (isDiscordRESTError(err) || isDiscordHTTPError(err)) {
|
|
// Discord API errors, usually safe to just log instead of crash
|
|
// We still bail if we get a ton of them in a short amount of time
|
|
if (++recentDiscordErrors >= RECENT_DISCORD_ERROR_EXIT_THRESHOLD) {
|
|
console.error(`Exiting after ${RECENT_DISCORD_ERROR_EXIT_THRESHOLD} API errors`);
|
|
process.exit(1);
|
|
}
|
|
} else {
|
|
// On other errors, crash immediately
|
|
process.exit(1);
|
|
}
|
|
// tslint:enable:no-console
|
|
};
|
|
|
|
process.on("uncaughtException", errorHandler);
|
|
}
|
|
|
|
// Verify required Node.js version
|
|
const REQUIRED_NODE_VERSION = "14.0.0";
|
|
const requiredParts = REQUIRED_NODE_VERSION.split(".").map(v => parseInt(v, 10));
|
|
const actualVersionParts = process.versions.node.split(".").map(v => parseInt(v, 10));
|
|
for (const [i, part] of actualVersionParts.entries()) {
|
|
if (part > requiredParts[i]) break;
|
|
if (part === requiredParts[i]) continue;
|
|
throw new SimpleError(`Unsupported Node.js version! Must be at least ${REQUIRED_NODE_VERSION}`);
|
|
}
|
|
|
|
moment.tz.setDefault("UTC");
|
|
|
|
logger.info("Connecting to database");
|
|
connect().then(async () => {
|
|
const client = new Client(`Bot ${process.env.TOKEN}`, {
|
|
getAllUsers: false,
|
|
restMode: true,
|
|
compress: false,
|
|
guildCreateTimeout: 0,
|
|
intents: [
|
|
// Privileged
|
|
"guildMembers",
|
|
// "guildPresences",
|
|
"guildMessageTyping",
|
|
|
|
// Regular
|
|
"directMessages",
|
|
"guildBans",
|
|
"guildEmojis",
|
|
"guildInvites",
|
|
"guildMessageReactions",
|
|
"guildMessages",
|
|
"guilds",
|
|
"guildVoiceStates",
|
|
],
|
|
});
|
|
client.setMaxListeners(200);
|
|
|
|
client.on("debug", message => {
|
|
if (message.includes(" 429 ")) {
|
|
logger.info(`[429] ${message}`);
|
|
}
|
|
});
|
|
|
|
client.on("error", err => {
|
|
logger.error(`[ERIS] ${String(err)}`);
|
|
});
|
|
|
|
const allowedGuilds = new AllowedGuilds();
|
|
const guildConfigs = new Configs();
|
|
|
|
const bot = new Knub<ZeppelinGuildConfig, ZeppelinGlobalConfig>(client, {
|
|
guildPlugins,
|
|
globalPlugins,
|
|
|
|
options: {
|
|
canLoadGuild(guildId): Promise<boolean> {
|
|
return allowedGuilds.isAllowed(guildId);
|
|
},
|
|
|
|
/**
|
|
* Plugins are enabled if they...
|
|
* - are base plugins, i.e. always enabled, or
|
|
* - are explicitly enabled in the guild config
|
|
* Dependencies are also automatically loaded by Knub.
|
|
*/
|
|
async getEnabledGuildPlugins(ctx, plugins): Promise<string[]> {
|
|
if (!ctx.config || !ctx.config.plugins) {
|
|
return [];
|
|
}
|
|
|
|
const configuredPlugins = ctx.config.plugins;
|
|
const basePluginNames = baseGuildPlugins.map(p => p.name);
|
|
|
|
return Array.from(plugins.keys()).filter(pluginName => {
|
|
if (basePluginNames.includes(pluginName)) return true;
|
|
return configuredPlugins[pluginName] && configuredPlugins[pluginName].enabled !== false;
|
|
});
|
|
},
|
|
|
|
async getConfig(id) {
|
|
const key = id === "global" ? "global" : `guild-${id}`;
|
|
const row = await guildConfigs.getActiveByKey(key);
|
|
if (row) {
|
|
return yaml.safeLoad(row.config);
|
|
}
|
|
|
|
logger.warn(`No config with key "${key}"`);
|
|
return {};
|
|
},
|
|
|
|
logFn: (level, msg) => {
|
|
if (level === "debug") return;
|
|
|
|
if (logger[level]) {
|
|
logger[level](msg);
|
|
} else {
|
|
logger.log(`[${level.toUpperCase()}] ${msg}`);
|
|
}
|
|
},
|
|
|
|
performanceDebug: {
|
|
enabled: false,
|
|
size: 30,
|
|
threshold: 200,
|
|
},
|
|
|
|
sendSuccessMessageFn(channel, body) {
|
|
const guildId = channel instanceof TextChannel ? channel.guild.id : undefined;
|
|
const emoji = guildId ? bot.getLoadedGuild(guildId)!.config.success_emoji : undefined;
|
|
channel.createMessage(successMessage(body, emoji));
|
|
},
|
|
|
|
sendErrorMessageFn(channel, body) {
|
|
const guildId = channel instanceof TextChannel ? channel.guild.id : undefined;
|
|
const emoji = guildId ? bot.getLoadedGuild(guildId)!.config.error_emoji : undefined;
|
|
channel.createMessage(errorMessage(body, emoji));
|
|
},
|
|
},
|
|
});
|
|
|
|
client.once("ready", () => {
|
|
startUptimeCounter();
|
|
});
|
|
|
|
logger.info("Starting the bot");
|
|
bot.run();
|
|
});
|