supabotbase
Version:
An easy-to-use Discord.js Bot base with messages and interactions support.
273 lines (221 loc) • 9.49 kB
JavaScript
const { Message, MessageEmbed } = require("discord.js");
const randomString = require("random-string");
module.exports = class ErrorHandler {
constructor(main) {
this.database = main.database;
this.client = main.client;
this.main = main;
if (this.database) {
this._getErrorList = this.database.db.prepare("SELECT key FROM `SupaBotBaseData` WHERE `key` LIKE 'error-________'");
this._getError = (errorId) => this.database.get(`error-${errorId}`);
this._deleteError = (errorId) => this.database.delete(`error-${errorId}`);
}
this._mountProcessErrors();
}
async _onMessageError(error, message, cmd) {
let errorMessage = `\n\n\x1b[1m[\x1b[31mERROR\x1b[0m\x1b[1m]\x1b[0m SupaBotBase encountered an error while running command \x1b[1m${cmd.help.name}\x1b[0m!\nFile: ${cmd._file}\n\x1b[32mMessage data.\x1b[0m\n- Guild: ${message.guild.name} (${message.guild.id})\n- Channel: ${message.channel.name} (${message.channel.id})\n- Author: ${message.author.tag} (${message.author.id})\n- Message link: ${message.url}\n========[ Content start ]========\n${message.content}\n========[ Content end ]========\n\n========[ Error Message ]========\n${error.stack}`;
console.error(errorMessage);
if (this.errorCallback) {
let res = await this.errorCallback(error, message, cmd, errorMessage);
if (typeof res === "boolean" && !res) return;
}
if (this.database) {
let id = this._generateErrorID();
this.database.set(`error-${id}`, {
message,
channelId: message.channel.id,
isMessageError: true,
error: {
stack: error.stack,
message: error.message,
name: error.name
},
cmd: {
help: cmd.help,
args: cmd.args,
examples: cmd.examples,
aliases: cmd.aliases,
slashCommands: cmd.slashCommands,
slashCommandType: cmd.slashCommandType,
security: cmd.security,
_file: cmd._file
}
});
message.answerCommand(this.main.config.messages.errorWithDatabase
.replace("{0}", cmd.help.name)
.replace("{1}", id));
this._sendErrorMessage(message, error, cmd, id);
return;
}
message.answerCommand(this.main.config.messages.errorWithoutDatabase);
this._sendErrorMessage(message, error, cmd, "No_DB");
}
async _sendErrorMessage(message, error, cmd, errorId) {
if (!this.logChannel) return;
let msg = await this.logChannel.send(new MessageEmbed());
if (this.database) msg.react("📧");
this._updateErrorMessage(message, error, cmd, errorId, false, msg);
if (this.errorMessages) {
this.errorMessages[msg.id] = {
error: errorId,
channel: msg.channel.id,
invite: false
};
this.database.set("error-messages", this.errorMessages);
}
}
async _onOtherError(error, type) {
console.error(type, error);
let id = "";
if (this.database) {
id = this._generateErrorID();
this.database.set(`error-${id}`, {
error,
type,
time: Date.now()
});
id = ` #${id}`;
}
if (!this.logChannel) return;
let msg = await this.logChannel.send(new MessageEmbed());
this._updateOtherErrorMessage(type, error, id, msg);
this.errorMessages[msg.id] = {
error: id.split(" #").join(""),
channel: msg.channel.id
}
this.database.set("error-messages", this.errorMessages);
}
_updateOtherErrorMessage(type, error, errorId, msg) {
let embed = new MessageEmbed()
.setTitle(`Error${errorId}`)
.setColor("#8b0000")
.setDescription(`**Type**: \`${type}\`${(error instanceof Error) ? "" : "\n> _Error isn't an object, location can't be traced!_"}\n\`\`\`${(error.stack || error).toString().substr(0, 1000-6)}\`\`\``)
.setTimestamp()
msg.edit(embed);
}
_updateErrorMessage(message, error, cmd, errorId, invite, toUpdate) {
let embed = new MessageEmbed()
.setTitle(`Error #${errorId}`)
.setColor("#ff0000")
.setAuthor(`${message.author.tag} (${message.author.id})`, message.author.avatarURL(), message.url)
.setTimestamp(message.createdTimestamp)
.setDescription(`Guild: ${message.guild.name} (${message.guild.id})\nChannel: <#${message.channel.id}> (${message.channel.name}, ${message.channel.id})\nAuthor: <@${message.author.id}> (${message.author.tag}, ${message.author.id})\nMessage link: [click](${message.url})`)
.addField("**Message**", `\`\`\`${message.content ? message.content.substr(0, 1000-6) : "_Slash Command_"}\`\`\``)
.addField("**Error**", `\`\`\`${error.stack.toString().substr(0, 1000-6)}\`\`\``)
.addField("**Command**", `Name: **${cmd.help.name}**\nTriggerType: **${message.isSlashCommand ? `Slash (${cmd.slashCommandType})` : "Message"}**\nFile: \`${cmd._file}\``)
if (invite) embed.description += `\nInvite: ${invite}`;
else if (this.database) embed.setFooter(`Click 📧 to generate invite link`);
toUpdate.edit(embed);
}
_generateErrorID() {
let id = randomString({length: 8});
if (this.database.get(id) || id === "messages") return this._generateErrorID();
return id;
}
getErrorList() {
if (!this.database) return [];
return this._getErrorList.all().map((e) => e.key.split("error-")[1]).filter((id) => id !== "messages");
}
async getError(id) {
if (!this.database) return false;
let error = this._getError(id);
if (!error) return false;
if (error.isMessageError) {
let message = new Message(this.client, error.message,
await this.client.channels.fetch(error.channelId)
);
Object.defineProperty(message, "author", {
value: await this.client.users.fetch(error.message.authorID),
writable: false
});
Object.defineProperty(message, "member", {
value: await message.guild.members.fetch(message.author.id),
writable: false
});
return {
message,
error: error.error,
cmd: error.cmd
}
}
return error;
}
async removeError(id, deleteMessage = true) {
if (!this.database || id === "messages") return false;
this._deleteError(id);
if (this.errorMessages && deleteMessage) {
let e = false;
for (let i in this.errorMessages) {
if (this.errorMessages[i].error === id) e = i;
}
if (e === false) return true;
let channel = await this.client.channels.fetch(this.errorMessages[e].channel);
if (!channel) return true;
let msg = await channel.messages.fetch(e);
if (!msg) return true;
msg.delete().catch((_) => {});
delete this.errorMessages[e];
this.database.set("error-messages", this.errorMessages);
}
return true;
}
async clearErrors() {
let errors = this.getErrorList();
for (let error of errors) {
await this.removeError(error);
}
return errors.length;
}
_mountReactionListener() {
this.main.translateRawMessageEvents();
this.client.on("messageReactionAdd", async (reaction, user) => {
if (user.bot || reaction.emoji.name !== "📧" || !this.errorMessages || !this.errorMessages[reaction.message.id] || this.errorMessages[reaction.message.id].invite) return;
let error = await this.getError(this.errorMessages[reaction.message.id].error);
reaction.message.edit(new MessageEmbed());
reaction.message.reactions.removeAll();
let invite = await error.message.channel.createInvite({
maxAge: 0,
reason: "SupaBaseBot | Error resolving"
});
this.errorMessages[reaction.message.id].invite = invite.url;
this.database.set("error-messages", this.errorMessages);
this._updateErrorMessage(error.message, error.error, error.cmd, this.errorMessages[reaction.message.id].error, invite.url, reaction.message);
});
}
_mountErrorMessageDeletionListener() {
this.client.on("messageDelete", (message) => {
if (!message.author.bot || !this.errorMessages || !this.errorMessages[message.id]) return;
this.removeError(this.errorMessages[message.id].error, false);
delete this.errorMessages[message.id];
});
}
_mountProcessErrors() {
process.on("uncaughtException", (e) => {
this._onOtherError(e, "uncaughtException");
});
process.on("unhandledRejection", (e) => {
this._onOtherError(e, "unhandledRejection");
});
}
setLogChannel(id) {
if (this.database) this.errorMessages = this.database.get(`error-messages`) || {};
console.log("ErrorHandler: Setting log channel when bot is ready...");
this.client.on("ready", async () => {
this.logChannel = await this.client.channels.fetch(id);
if (this.logChannel) {
console.log(`ErrorHandler: Bot logging errors in channel ${this.logChannel.name} (${this.logChannel.id}), in guild ${this.logChannel.guild.name}.`);
this._mountReactionListener();
this._mountErrorMessageDeletionListener();
} else console.log(`ErrorHandler: Couldn't find a channel with the id ${id}!`);
});
}
/**
* @method onError
* @description Set a function to call when an error occurred. Return false to stop the default error message to be send.
* @returns {undefined}
* @param {Function} f The function to call when an error occurred
*/
onError(f = () => {}) {
this.errorCallback = f;
}
}