diff --git a/.eslintrc.json b/.eslintrc similarity index 97% rename from .eslintrc.json rename to .eslintrc index 94c48f07..57186d54 100644 --- a/.eslintrc.json +++ b/.eslintrc @@ -14,7 +14,6 @@ "no-shadow": "warn", "no-plusplus": "off", "radix": ["error", "as-needed"], - "max-len": "warn", "import/no-extraneous-dependencies": "off", "import/no-unresolved": "off", "import/no-dynamic-require": "warn", diff --git a/.travis.yml b/.travis.yml index 07720ee5..a5edc928 100644 --- a/.travis.yml +++ b/.travis.yml @@ -1,4 +1,11 @@ language: node_js node_js: - - "6" + - "7" install: npm install +notifications: + webhooks: + urls: + - https://webhooks.gitter.im/e/0926b82fc22ea0760ede + on_success: always # options: [always|never|change] default: always + on_failure: always # options: [always|never|change] default: always + on_start: never # options: [always|never|change] default: always diff --git a/CHANGELOG.md b/CHANGELOG.md index b0ab378a..1a6bcb5f 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -5,6 +5,30 @@ The format is based on [Keep a Changelog](http://keepachangelog.com/) and this project adheres to [Semantic Versioning](http://semver.org/). ## [Unreleased] +## [0.18.1] - 2017-03-17 +### Added +- Sentry Integration for Error Tracking purposes direct to Komada Devs, +- Added the new utils from `Discord.js#master`: escapeMarkdown and splitMessage are now in `client.methods`. +- Added support for silent inhibitors (if `return true`, it won't send a reply). +- Added Environmental Variable support for clientDir. +- Added regExpEscape function. + +### Changed +- Add error.stack to the function log.js to avoid [object Object]. +- Disconnect event should now prints a more human readable error instead of `[object Object]`. +- error and warn event errors are now inspected with depth 0, for better debug. +- loading Functions are removed from Functions folder and moved to a Utils folder. (This folder will be there for future features as well.) + +### Fixed +- Reloading pieces should now return the error stack in a codeblock. +- Fixed function reload event. +- Fixed command reload all. underlying bug since 0.15.x days. +- Fixed typo in validateData function +- Fixed Default Conf initialize. (No longer outputs undefined) +- Fixed invalid regex for prefixes in parseCommand + +### Removed +- client.email redaction from the clean function. ## [0.18.0] - 2017-03-16 ### Added @@ -178,12 +202,13 @@ and this project adheres to [Semantic Versioning](http://semver.org/). - Various Confs fixes from [UnseenFaith] - Usage Addition/ParseUsage fix from [UnseenFaith] -[Unreleased]: https://github.com/eslachance/komada/compare/0.18.0...indev +[Unreleased]: https://github.com/eslachance/komada/compare/0.18.1...indev [0.10.0]: https://github.com/eslachance/komada/compare/1627e6deb1d8c352d83e52ccd590f2330f5f8bb2...0.10.0 [0.11.0]: https://github.com/eslachance/komada/compare/0.10.0...0.11.0 [0.12.0]: https://github.com/eslachance/komada/compare/0.11.0...0.12.0 [0.12.4]: https://github.com/eslachance/komada/compare/0.12.0...0.12.4 [0.18.0]: https://github.com/eslachance/komada/compare/0.12.4...0.18 +[0.18.1]: https://github.com/eslachance/komada/compare/0.12.4...0.18.1 [vzwGrey]: https://github.com/vzwGrey [eslachance]: https://github.com/eslachance diff --git a/app.js b/app.js index 1b160087..dfcbebe9 100644 --- a/app.js +++ b/app.js @@ -1,7 +1,14 @@ const Discord = require("discord.js"); const path = require("path"); +const util = require("util"); + +const loadFunctions = require("./utils/loadFunctions.js"); +const loadEvents = require("./utils/loadEvents.js"); +const loadProviders = require("./utils/loadProviders.js"); +const loadCommands = require("./utils/loadCommands.js"); +const loadCommandInhibitors = require("./utils/loadCommandInhibitors.js"); +const loadMessageMonitors = require("./utils/loadMessageMonitors.js"); -const loadFunctions = require("./functions/loadFunctions.js"); const Config = require("./classes/Config.js"); exports.start = async (config) => { @@ -10,7 +17,7 @@ exports.start = async (config) => { client.config = config; - // Extend client + // Extend client client.funcs = {}; client.helpStructure = new Map(); client.commands = new Discord.Collection(); @@ -19,37 +26,42 @@ exports.start = async (config) => { client.messageMonitors = new Discord.Collection(); client.providers = new Discord.Collection(); - // Extend Client with Native Discord.js Functions for use in our pieces. + // Extend Client with Native Discord.js Functions for use in our pieces. client.methods = {}; client.methods.Collection = Discord.Collection; client.methods.Embed = Discord.RichEmbed; client.methods.MessageCollector = Discord.MessageCollector; client.methods.Webhook = Discord.WebhookClient; + client.methods.escapeMarkdown = Discord.escapeMarkdown; + client.methods.splitMessage = Discord.splitMessage; client.coreBaseDir = `${__dirname}${path.sep}`; - client.clientBaseDir = `${process.cwd()}${path.sep}`; + client.clientBaseDir = `${process.env.clientDir || process.cwd()}${path.sep}`; client.guildConfs = Config.guildConfs; client.configuration = Config; + await loadEvents(client); + client.once("ready", async () => { client.config.prefixMention = new RegExp(`^<@!?${client.user.id}>`); - client.configuration.initialize(client); + await client.configuration.initialize(client); await loadFunctions(client); - await client.funcs.loadProviders(client); - await client.funcs.loadCommands(client); - await client.funcs.loadCommandInhibitors(client); - await client.funcs.loadMessageMonitors(client); - await client.funcs.loadEvents(client); + await loadProviders(client); + await loadCommands(client); + await loadCommandInhibitors(client); + await loadMessageMonitors(client); client.i18n = client.funcs.loadLocalizations; client.i18n.init(client); client.destroy = () => "You cannot use this within Komada, use process.exit() instead."; + client.ready = true; }); - client.on("error", e => client.funcs.log(e, "error")); - client.on("warn", w => client.funcs.log(w, "warning")); - client.on("disconnect", e => client.funcs.log(e, "error")); + client.on("error", e => client.funcs.log(util.inspect(e, { depth: 0 }), "error")); + client.on("warn", w => client.funcs.log(util.inspect(w, { depth: 0 }), "warn")); + client.on("disconnect", e => client.funcs.log(`Disconnected | ${e.code}: ${e.reason}`, "error")); client.on("message", async (msg) => { + if (!client.ready) return; await client.funcs.runMessageMonitors(client, msg); msg.author.permLevel = await client.funcs.permissionLevel(client, msg.author, msg.guild); msg.guildConf = Config.get(msg.guild); @@ -62,7 +74,6 @@ exports.start = async (config) => { client.login(client.config.botToken); return client; }; - process.on("unhandledRejection", (err) => { if (!err) return; console.error(`Uncaught Promise Error: \n${err.stack || err}`); diff --git a/classes/Config.js b/classes/Config.js index 4eb4d779..ab4611c6 100644 --- a/classes/Config.js +++ b/classes/Config.js @@ -354,7 +354,7 @@ class Config { .then((conf) => { if (conf) defaultConf = conf; }) - .catch(() => fs.outputJSONAsync(`${dataDir}${path.sep}${defaultFile}`)); + .catch(() => fs.outputJSONAsync(`${dataDir}${path.sep}${defaultFile}`, defaultConf)); client.guilds.forEach((guild) => { fs.readJSONAsync(path.resolve(`${dataDir}${path.sep}${guild.id}.json`)) .then((thisConf) => { diff --git a/commands/System/download.js b/commands/System/download.js index b125054b..031d0845 100644 --- a/commands/System/download.js +++ b/commands/System/download.js @@ -63,7 +63,7 @@ exports.run = (client, msg, [link, piece, folder = "Downloaded"]) => { if (client.messageMonitors.has(name)) return msg.channel.sendMessage(`<@!${msg.author.id}> | That message monitor already exists in your bot. Aborting the load.`); break; case "providers": - if (client.providers.has(name)) return msg.channel.sendMessage(`<@!${msg.author.id} | That provider already exists in your bot. Aborting the load.`); + if (client.providers.has(name)) return msg.channel.sendMessage(`<@!${msg.author.id}> | That provider already exists in your bot. Aborting the load.`); break; default: return "This will never trigger"; diff --git a/commands/System/info.js b/commands/System/info.js index 5889baa4..9cdf56d5 100644 --- a/commands/System/info.js +++ b/commands/System/info.js @@ -1,5 +1,21 @@ exports.run = (client, msg) => { - msg.channel.sendMessage("This bot is built on the Komada framework, a plug-and-play bot builder made by Dirigeant's team of dedicated developers. For more information visit: "); + const information = ` +Komada is a 'plug-and-play' framework built on top of the Discord.js library. +Most of the code is modularized, which allows developers to edit Komada to suit their needs. + +Some features of Komada include: +• Fast Loading times with ES7 Support (Async/Await) +• Per-server settings for each guild, that can be extended with your own code +• Customizable Command system with automated usage parsing and easy to use reloading and downloading modules +• "Monitors" which can watch messages and act on them, like a normal message event (Swear Filters, Spam Protection, etc) +• "Inhibitors" which can prevent commands from running based on a set of parameters (Permissions, Blacklists, etc) +• "Providers" which allow you to connect with an outside database of your choosing **soon**:tm: +• Internal "Functions" which allow you to use functions anywhere where you have access to a client variable. + +We hope to be a 100% customizable framework that can cater to all audiences. We do frequent updates and bugfixes when available. +If you're interested in us, check us out at https://komada.js.org +`; + msg.channel.send(information); }; exports.conf = { diff --git a/commands/System/reload.js b/commands/System/reload.js index 86259b84..32db4a8a 100644 --- a/commands/System/reload.js +++ b/commands/System/reload.js @@ -50,7 +50,7 @@ exports.run = async (client, msg, [type, name]) => { case "command": switch (name) { case "all": - await client.funcs.loadCommands(client); + await require(`${client.coreBaseDir}utils/loadCommands.js`)(client); msg.channel.sendMessage("✅ Reloaded all commands."); break; default: diff --git a/functions/clean.js b/functions/clean.js index f57ac85e..3f5c5668 100644 --- a/functions/clean.js +++ b/functions/clean.js @@ -3,7 +3,6 @@ function sensitivePattern(client) { let pattern = ""; if (client.token) pattern += client.token; if (client.token) pattern += (pattern.length > 0 ? "|" : "") + client.token; - if (client.email) pattern += (pattern.length > 0 ? "|" : "") + client.email; if (client.user.email) pattern += (pattern.length > 0 ? "|" : "") + client.user.email; if (client.password) pattern += (pattern.length > 0 ? "|" : "") + client.password; this.sensitivePattern = new RegExp(pattern, "gi"); diff --git a/functions/handleCommand.js b/functions/handleCommand.js index 2552fe02..764c3dde 100644 --- a/functions/handleCommand.js +++ b/functions/handleCommand.js @@ -2,7 +2,10 @@ module.exports = async (client, msg, command, args = undefined) => { const validCommand = this.getCommand(client, command); if (!validCommand) return; const response = this.runInhibitors(client, msg, validCommand); - if (response) return msg.reply(response); + if (response) { + if (typeof response === "string") return msg.reply(response); + return; + } try { const params = await client.funcs.usage.run(client, msg, validCommand, args); validCommand.run(client, msg, params); diff --git a/functions/handleMessage.js b/functions/handleMessage.js index 11793b4c..c19de497 100644 --- a/functions/handleMessage.js +++ b/functions/handleMessage.js @@ -5,4 +5,4 @@ module.exports = (client, msg, edited = false) => { if (!client.config.selfbot && msg.author.id === client.user.id) return false; // Ignore other users if selfbot but config option is false if (!client.config.editableCommands && edited) return false; // Ignore message if owner doesn't allow editableCommands return true; -} +}; diff --git a/functions/loadSingleCommand.js b/functions/loadSingleCommand.js index 1d382835..756c97b8 100644 --- a/functions/loadSingleCommand.js +++ b/functions/loadSingleCommand.js @@ -23,7 +23,7 @@ module.exports = (client, command, reload = false, loadPath = null) => new Promi cmd.init(client); } } catch (e) { - reject(`Could not load existing command data: ${e.stack}`); + reject(`Could not load existing command data: \`\`\`js\n${e.stack}\`\`\``); } } else { try { @@ -31,6 +31,7 @@ module.exports = (client, command, reload = false, loadPath = null) => new Promi if (cmd.conf.selfbot && !client.config.selfbot) { return reject(`The command \`${cmd.help.name}\` is only usable in selfbots!`); } + delete require.cache[require.resolve(loadPath)]; if (cmd.init) cmd.init(client); let pathParts = loadPath.split(path.sep); pathParts = pathParts.slice(pathParts.indexOf("commands") + 1); @@ -46,7 +47,7 @@ module.exports = (client, command, reload = false, loadPath = null) => new Promi }); client.funcs.loadSingleCommand(client, command, false, loadPath); } else { - reject(`Could not load new command data: ${e.stack}`); + reject(`Could not load new command data: \`\`\`js\n${e.stack}\`\`\``); } } } diff --git a/functions/log.js b/functions/log.js index 183f8e72..3753019b 100644 --- a/functions/log.js +++ b/functions/log.js @@ -12,7 +12,7 @@ module.exports = (data, type = "log") => { console.warn(`${clk.black.bgYellow(`[${moment().format("YYYY-MM-DD HH:mm:ss")}]`)} ${data}`); break; case "error": - console.error(`${clk.bgRed(`[${moment().format("YYYY-MM-DD HH:mm:ss")}]`)} ${data}`); + console.error(`${clk.bgRed(`[${moment().format("YYYY-MM-DD HH:mm:ss")}]`)} ${data.stack || data}`); break; case "log": console.log(`${clk.bgBlue(`[${moment().format("YYYY-MM-DD HH:mm:ss")}]`)} ${data}`); diff --git a/functions/parseCommand.js b/functions/parseCommand.js index fad054ad..77378bdf 100644 --- a/functions/parseCommand.js +++ b/functions/parseCommand.js @@ -17,14 +17,15 @@ exports.getPrefix = (client, msg) => { if (client.config.prefixMention.test(msg.content)) { return client.config.prefixMention; } - const prefix = msg.guildConf.prefix; + let prefix = msg.guildConf.prefix; + const escape = client.funcs.regExpEsc; if (prefix instanceof Array) { - prefix.forEach((prefix) => { - if (msg.content.startsWith(prefix)) prefix = RegExp(`^${prefix}`); - else prefix = false; + prefix.forEach((pref) => { + if (msg.content.startsWith(pref)) prefix = pref; + else pref = false; }); return prefix; } - if (msg.content.startsWith(prefix)) return new RegExp(`^${prefix}`); // eslint-disable-line + if (prefix && msg.content.startsWith(prefix)) return new RegExp(`^${escape(prefix)}`); // eslint-disable-line return false; }; diff --git a/functions/regExpEsc.js b/functions/regExpEsc.js new file mode 100644 index 00000000..be800b04 --- /dev/null +++ b/functions/regExpEsc.js @@ -0,0 +1 @@ +module.exports = str => str.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); diff --git a/functions/reload.js b/functions/reload.js index 98b3b9eb..c42ab493 100644 --- a/functions/reload.js +++ b/functions/reload.js @@ -14,8 +14,8 @@ exports.function = (client, dir, funcName) => new Promise(async (resolve, reject client.funcs[funcName].init(client); } }); - } catch (error) { - reject(error); + } catch (e) { + reject(`Could not load new function data: \`\`\`js\n${e.stack}\`\`\``); return; } resolve(`Successfully reloaded the function ${funcName}.`); @@ -43,7 +43,7 @@ exports.function = (client, dir, funcName) => new Promise(async (resolve, reject process.exit(); }); } else { - reject(`Could not load new function data: ${error}`); + reject(`Could not load new function data: \`\`\`js\n${error.stack}\`\`\``); } } } else { @@ -67,8 +67,8 @@ exports.inhibitor = (client, dir, inhibName) => new Promise(async (resolve, reje props.init(client); } }); - } catch (error) { - reject(error); + } catch (e) { + reject(`Could not load new inhibitor data: \`\`\`js\n${e.stack}\`\`\``); return; } resolve(`Successfully reloaded the inhibitor ${inhibName}`); @@ -97,7 +97,7 @@ exports.inhibitor = (client, dir, inhibName) => new Promise(async (resolve, reje process.exit(); }); } else { - reject(`Could not load new inhibitor data: ${error}`); + reject(`Could not load new inhibitor data: \`\`\`js\n${error.stack}\`\`\``); } } } else { @@ -121,8 +121,8 @@ exports.monitor = (client, dir, monitName) => new Promise(async (resolve, reject props.init(client); } }); - } catch (error) { - reject(error); + } catch (e) { + reject(`Could not load new monitor data: \`\`\`js\n${e.stack}\`\`\``); return; } resolve(`Succesfully reloaded the monitor ${monitName}.`); @@ -151,7 +151,7 @@ exports.monitor = (client, dir, monitName) => new Promise(async (resolve, reject process.exit(); }); } else { - reject(`Could not load new monitor data: ${error}`); + reject(`Could not load new monitor data: \`\`\`js\n${error.stack}\`\`\``); } } } else { @@ -175,8 +175,8 @@ exports.provider = (client, dir, providerName) => new Promise(async (resolve, re props.init(client); } }); - } catch (error) { - reject(error); + } catch (e) { + reject(`Could not load new provider data: \`\`\`js\n${e.stack}\`\`\``); return; } resolve(`Successfully reloaded the provider ${providerName}.`); @@ -205,7 +205,7 @@ exports.provider = (client, dir, providerName) => new Promise(async (resolve, re process.exit(); }); } else { - reject(`Could not load new provider data: ${error}`); + reject(`Could not load new provider data: \`\`\`js\n${error.stack}\`\`\``); } } } else { @@ -216,27 +216,20 @@ exports.provider = (client, dir, providerName) => new Promise(async (resolve, re exports.event = (client, eventName) => new Promise(async (resolve, reject) => { const files = await client.funcs.getFileListing(client, client.clientBaseDir, "events").catch(err => client.emit("error", client.funcs.newError(err))); - const oldEvent = files.filter(f => f.name === eventName); - if (oldEvent[0] && oldEvent[0].name === eventName) { - if (!client.events[eventName]) { - const file = oldEvent[0]; - client.on(file.name, (...args) => require(`${file.path}${path.sep}${file.base}`).run(client, ...args)); + const file = files.find(f => f.name === eventName); + if (file && file.name === eventName) { + const runEvent = (...args) => require(`${file.path}${path.sep}${file.base}`).run(client, ...args); + if (!client._events[eventName]) { + client.on(file.name, runEvent); + resolve(`Successfully loaded a new event called ${eventName}.`); return; } - let listener; - if (client._events[eventName].length !== 0) { - listener = client._events[eventName][1]; - } else { - listener = client._events[eventName]; - } - client.removeListener(eventName, listener); + client.removeListener(eventName, runEvent); try { - oldEvent.forEach((file) => { - delete require.cache[require.resolve(`${file.path}${path.sep}${file.base}`)]; - client.on(file.name, (...args) => require(`${file.path}${path.sep}${file.base}`).run(client, ...args)); - }); - } catch (error) { - reject(error); + delete require.cache[require.resolve(`${file.path}${path.sep}${file.base}`)]; + client.on(file.name, runEvent); + } catch (e) { + reject(`Could not load new event data: \`\`\`js\n${e.stack}\`\`\``); return; } resolve(`Successfully reloaded the event ${eventName}`); diff --git a/functions/validateData.js b/functions/validateData.js index 4fa7806d..b6190a05 100644 --- a/functions/validateData.js +++ b/functions/validateData.js @@ -67,7 +67,7 @@ module.exports = (data, properties, values) => { case "string": break; default: - throw new Error("unsuported"); + throw new Error("unsupported"); } }); }; diff --git a/package.json b/package.json index f66cb593..662949fd 100644 --- a/package.json +++ b/package.json @@ -1,17 +1,16 @@ { "name": "komada", - "version": "0.18.0", + "version": "0.18.1", "author": "Evelyne Lachance", "description": "Komada: Croatian for 'pieces', is a modular bot system including reloading modules and easy to use custom commands.", "main": "app.js", "scripts": { "start": "node app.js", - "test": "eslint commands functions inhibitors events monitors app.js", + "test": "eslint commands functions inhibitors events monitors utils app.js", "lint": "npm run test -- --fix" }, "repository": { "type": "git", - "url": "https://github.com/dirigeants/komada.git" }, "bugs": { @@ -21,6 +20,7 @@ "dependencies": { "chalk": "^1.1.3", "discord.js": "hydrabolt/discord.js", + "dotenv": "^4.0.0", "fs-extra": "^1.0.0", "fs-extra-promise": "^0.4.1", "moment": "^2.16.0", diff --git a/functions/loadCommandInhibitors.js b/utils/loadCommandInhibitors.js similarity index 100% rename from functions/loadCommandInhibitors.js rename to utils/loadCommandInhibitors.js diff --git a/functions/loadCommands.js b/utils/loadCommands.js similarity index 100% rename from functions/loadCommands.js rename to utils/loadCommands.js diff --git a/functions/loadEvents.js b/utils/loadEvents.js similarity index 78% rename from functions/loadEvents.js rename to utils/loadEvents.js index e2c274e5..0efd5b66 100644 --- a/functions/loadEvents.js +++ b/utils/loadEvents.js @@ -1,5 +1,7 @@ const fs = require("fs-extra-promise"); const path = require("path"); +const getFileListing = require("../functions/getFileListing.js"); +const log = require("../functions/log.js"); let events = require("discord.js/src/util/Constants.js").Events; @@ -8,7 +10,7 @@ events = Object.keys(events).map(k => events[k]); const loadEvents = (client, baseDir, counts) => new Promise(async (resolve) => { const dir = path.resolve(`${baseDir}./events/`); await fs.ensureDirAsync(dir).catch(err => client.emit("error", client.funcs.newError(err))); - let files = await client.funcs.getFileListing(client, baseDir, "events").catch(err => client.emit("error", client.funcs.newError(err))); + let files = await getFileListing(client, baseDir, "events").catch(err => client.emit("error", client.funcs.newError(err))); files = files.filter(f => events.includes(f.name)); files.forEach((f) => { client.on(f.name, (...args) => require(`${f.path}${path.sep}${f.base}`).run(client, ...args)); @@ -24,5 +26,5 @@ module.exports = async (client) => { if (client.coreBaseDir !== client.clientBaseDir) { counts = await loadEvents(client, client.clientBaseDir, counts).catch(err => client.emit("error", client.funcs.newError(err))); } - client.funcs.log(`Loaded ${counts} events`); + log(`Loaded ${counts} events`); }; diff --git a/functions/loadFunctions.js b/utils/loadFunctions.js similarity index 100% rename from functions/loadFunctions.js rename to utils/loadFunctions.js diff --git a/functions/loadMessageMonitors.js b/utils/loadMessageMonitors.js similarity index 100% rename from functions/loadMessageMonitors.js rename to utils/loadMessageMonitors.js diff --git a/functions/loadProviders.js b/utils/loadProviders.js similarity index 100% rename from functions/loadProviders.js rename to utils/loadProviders.js