├── docs ├── CNAME ├── .createFolder ├── tutorials │ ├── .createFolder │ ├── UnderstandingUsageString.md │ ├── PermissionLevels.md │ └── SettingGateway.md ├── img │ ├── favicon.ico │ └── toast-ui.png ├── fonts │ ├── glyphicons-halflings-regular.eot │ ├── glyphicons-halflings-regular.ttf │ ├── glyphicons-halflings-regular.woff │ └── glyphicons-halflings-regular.woff2 ├── scripts │ ├── prettify │ │ └── lang-css.js │ ├── linenumber.js │ └── tui-doc.js └── styles │ ├── prettify-jsdoc.css │ └── prettify-tomorrow.css ├── .npmrc ├── examples ├── .createFolder ├── UnderstandingUsageString.md ├── PermissionLevels.md └── SettingGateway.md ├── src ├── events │ ├── message.js │ ├── error.js │ ├── verbose.js │ ├── warn.js │ ├── log.js │ ├── disconnect.js │ ├── guildDelete.js │ ├── messageUpdate.js │ ├── guildCreate.js │ ├── messageBulkDelete.js │ └── messageDelete.js ├── functions │ ├── regExpEsc.js │ ├── toTitleCase.js │ ├── checkPerms.js │ ├── handleError.js │ ├── newError.js │ ├── getPrefix.js │ ├── clean.js │ └── mergeConfig.js ├── util │ ├── Constants.js │ ├── Timestamp.js │ ├── Duration.js │ └── Stopwatch.js ├── classes │ ├── Storage.js │ ├── PermissionLevels.js │ ├── settingResolver.js │ ├── settingsCache.js │ ├── settings │ │ ├── Schema.js │ │ ├── Settings.js │ │ └── Gateway.js │ ├── console │ │ ├── Colors.js │ │ └── Console.js │ ├── sql.js │ ├── Resolver.js │ └── parsedUsage.js ├── extendables │ ├── get_settings.js │ ├── get_guildSettings.js │ ├── sendFiles.js │ ├── get_readable.js │ ├── sendCode.js │ ├── sendFile.js │ ├── get_attachable.js │ ├── get_embedable.js │ ├── get_postable.js │ ├── sendEmbed.js │ └── sendMessage.js ├── inhibitors │ ├── disable.js │ ├── permissions.js │ ├── runIn.js │ ├── requiredSettings.js │ ├── requiredFuncs.js │ ├── commandCooldown.js │ └── missingBotPermissions.js ├── finalizers │ ├── commandCooldown.js │ └── commandlogging.js ├── commands │ └── System │ │ ├── reboot.js │ │ ├── ping.js │ │ ├── invite.js │ │ ├── stats.js │ │ ├── eval.js │ │ ├── disable.js │ │ ├── enable.js │ │ ├── transfer.js │ │ ├── info.js │ │ ├── reload.js │ │ ├── conf.js │ │ └── help.js ├── providers │ ├── collection.js │ └── json.js ├── documentation │ ├── EventStructure.js │ ├── FunctionStructure.js │ ├── Finalizer.js │ ├── ExtendableStructure.js │ ├── MonitorStructure.js │ ├── InhibitorStructure.js │ ├── CommandStructure.js │ └── ProviderStructure.js ├── monitors │ └── commandHandler.js └── index.js ├── .npmignore ├── .github ├── ISSUE_TEMPLATE.md └── PULL_REQUEST_TEMPLATE.md ├── .travis.yml ├── .gitignore ├── .travis-deploy.sh ├── .docstrap.json ├── LICENSE ├── package.json ├── CONTRIBUTING.md ├── .eslintrc.json └── README.md /docs/CNAME: -------------------------------------------------------------------------------- 1 | komada.js.org -------------------------------------------------------------------------------- /docs/.createFolder: -------------------------------------------------------------------------------- 1 | -- 2 | -------------------------------------------------------------------------------- /.npmrc: -------------------------------------------------------------------------------- 1 | package-lock=false 2 | -------------------------------------------------------------------------------- /examples/.createFolder: -------------------------------------------------------------------------------- 1 | -- 2 | -------------------------------------------------------------------------------- /docs/tutorials/.createFolder: -------------------------------------------------------------------------------- 1 | -- 2 | -------------------------------------------------------------------------------- /src/events/message.js: -------------------------------------------------------------------------------- 1 | exports.run = (client, msg) => msg.runMonitors(); 2 | -------------------------------------------------------------------------------- /src/events/error.js: -------------------------------------------------------------------------------- 1 | exports.run = (client, err) => client.emit("log", err, "error"); 2 | -------------------------------------------------------------------------------- /src/events/verbose.js: -------------------------------------------------------------------------------- 1 | exports.run = (client, log) => client.emit("log", log, "verbose"); 2 | -------------------------------------------------------------------------------- /docs/img/favicon.ico: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnseenFaith/komada/HEAD/docs/img/favicon.ico -------------------------------------------------------------------------------- /docs/img/toast-ui.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnseenFaith/komada/HEAD/docs/img/toast-ui.png -------------------------------------------------------------------------------- /src/events/warn.js: -------------------------------------------------------------------------------- 1 | exports.run = (client, warning) => client.emit("log", warning, "warn"); 2 | -------------------------------------------------------------------------------- /src/events/log.js: -------------------------------------------------------------------------------- 1 | exports.run = (client, data, type = "log") => client.console.log(data, type); 2 | -------------------------------------------------------------------------------- /src/functions/regExpEsc.js: -------------------------------------------------------------------------------- 1 | module.exports = str => str.replace(/[-/\\^$*+?.()|[\]{}]/g, "\\$&"); 2 | -------------------------------------------------------------------------------- /src/events/disconnect.js: -------------------------------------------------------------------------------- 1 | exports.run = (client, err) => client.emit("log", `Disconnected | ${err.code}: ${err.reason}`, "error"); 2 | -------------------------------------------------------------------------------- /docs/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnseenFaith/komada/HEAD/docs/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /docs/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnseenFaith/komada/HEAD/docs/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /docs/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnseenFaith/komada/HEAD/docs/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /docs/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/UnseenFaith/komada/HEAD/docs/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /src/functions/toTitleCase.js: -------------------------------------------------------------------------------- 1 | module.exports = str => str.replace(/\w\S*/g, txt => txt.charAt(0).toUpperCase() + txt.substr(1).toLowerCase()); 2 | -------------------------------------------------------------------------------- /src/util/Constants.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | OPTIONS: { 3 | // options 4 | }, 5 | COMMAND: { 6 | // command stuff 7 | }, 8 | }; 9 | -------------------------------------------------------------------------------- /src/events/guildDelete.js: -------------------------------------------------------------------------------- 1 | exports.run = (client, guild) => { 2 | if (guild.available) client.settings.guilds.destroy(guild.id).catch(() => null); 3 | }; 4 | -------------------------------------------------------------------------------- /src/events/messageUpdate.js: -------------------------------------------------------------------------------- 1 | exports.run = (client, old, msg) => { 2 | if (old.content !== msg.content && client.config.cmdEditing) client.emit("message", msg); 3 | }; 4 | -------------------------------------------------------------------------------- /src/events/guildCreate.js: -------------------------------------------------------------------------------- 1 | exports.run = (client, guild) => { 2 | if (guild.available) client.settings.guilds.create(guild).catch(e => client.emit("log", e, "error")); 3 | }; 4 | -------------------------------------------------------------------------------- /src/events/messageBulkDelete.js: -------------------------------------------------------------------------------- 1 | exports.run = (client, msgs) => { 2 | for (const msg of msgs.values()) client.emit("messageDelete", msg); // eslint-disable-line no-restricted-syntax 3 | }; 4 | -------------------------------------------------------------------------------- /src/classes/Storage.js: -------------------------------------------------------------------------------- 1 | class Storage { 2 | 3 | constructor(client) { 4 | Object.defineProperty(this, "client", { value: client }); 5 | } 6 | 7 | } 8 | 9 | module.exports = Storage; 10 | -------------------------------------------------------------------------------- /.npmignore: -------------------------------------------------------------------------------- 1 | .c9 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | 7 | # Dependency directories 8 | node_modules 9 | jspm_packages 10 | 11 | # Optional npm cache directory 12 | .npm 13 | 14 | # Optional REPL history 15 | .node_repl_history -------------------------------------------------------------------------------- /.github/ISSUE_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Version of Komada 2 | 3 | 4 | ### Are core files modified? 5 | 6 | 7 | ### Describe the problem 8 | 9 | 10 | ### Expected Behaviour 11 | 12 | 13 | ### Actual Behaviour 14 | 15 | 16 | ### Steps to Reproduce. 17 | -------------------------------------------------------------------------------- /src/extendables/get_settings.js: -------------------------------------------------------------------------------- 1 | exports.conf = { 2 | type: "get", 3 | method: "settings", 4 | appliesTo: ["Guild"], 5 | }; 6 | 7 | // eslint-disable-next-line func-names 8 | exports.extend = function () { 9 | return this.client.settings.guilds.get(this.id); 10 | }; 11 | -------------------------------------------------------------------------------- /.github/PULL_REQUEST_TEMPLATE.md: -------------------------------------------------------------------------------- 1 | ### Proposed Semver Increment Bump: [MAJOR/MINOR/PATCH] 2 | 3 | ### Changes Proposed in this Pull Request (List new items in CHANGELOG.MD) 4 | 5 | - 6 | - 7 | - 8 | 9 | ### (If Applicable) What Issue does it fix? 10 | 11 | Fixes... 12 | -------------------------------------------------------------------------------- /.travis.yml: -------------------------------------------------------------------------------- 1 | language: node_js 2 | node_js: 3 | - "8" 4 | install: npm install 5 | jobs: 6 | include: 7 | - stage: test 8 | script: npm test 9 | - stage: deploy 10 | script: bash ./.travis-deploy.sh 11 | cache: 12 | directories: 13 | - node_modules 14 | -------------------------------------------------------------------------------- /src/functions/checkPerms.js: -------------------------------------------------------------------------------- 1 | module.exports = (client, msg, min) => { 2 | for (let i = min; i <= client.permStructure.size; i++) { 3 | if (client.permStructure.levels[i].check(client, msg)) return true; 4 | if (client.permStructure.levels[i].break) return false; 5 | } 6 | return null; 7 | }; 8 | -------------------------------------------------------------------------------- /.gitignore: -------------------------------------------------------------------------------- 1 | .c9 2 | # Logs 3 | logs 4 | *.log 5 | npm-debug.log* 6 | 7 | # Dependency directories 8 | node_modules 9 | jspm_packages 10 | 11 | # Optional npm cache directory 12 | .npm 13 | 14 | # Optional REPL history 15 | .node_repl_history 16 | 17 | server\.js 18 | 19 | \.vscode/ 20 | 21 | bwd/ 22 | /.vs 23 | -------------------------------------------------------------------------------- /src/extendables/get_guildSettings.js: -------------------------------------------------------------------------------- 1 | exports.conf = { 2 | type: "get", 3 | method: "guildSettings", 4 | appliesTo: ["Message"], 5 | }; 6 | 7 | // eslint-disable-next-line func-names 8 | exports.extend = function () { 9 | return this.guild ? this.guild.settings : this.client.settings.guilds.schema.defaults; 10 | }; 11 | -------------------------------------------------------------------------------- /src/inhibitors/disable.js: -------------------------------------------------------------------------------- 1 | exports.conf = { 2 | enabled: true, 3 | spamProtection: false, 4 | priority: 9, 5 | }; 6 | 7 | exports.run = (client, msg, cmd) => { 8 | if (cmd.conf.enabled && !msg.guildSettings.disabledCommands.includes(cmd.help.name)) return false; 9 | return "This command is currently disabled"; 10 | }; 11 | -------------------------------------------------------------------------------- /src/events/messageDelete.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-syntax */ 2 | exports.run = (client, msg) => { 3 | for (const [key, value] of client.commandMessages) { 4 | if (key === msg.id) return client.commandMessages.delete(key); 5 | if (msg.id === value.response.id) return client.commandMessages.delete(key); 6 | } 7 | return false; 8 | }; 9 | -------------------------------------------------------------------------------- /src/extendables/sendFiles.js: -------------------------------------------------------------------------------- 1 | exports.conf = { 2 | type: "method", 3 | method: "sendFiles", 4 | appliesTo: ["TextChannel", "DMChannel", "GroupDMChannel"], 5 | }; 6 | 7 | // eslint-disable-next-line func-names 8 | exports.extend = function (files, content, options = {}) { 9 | return this.send(content, Object.assign(options, { files })); 10 | }; 11 | -------------------------------------------------------------------------------- /src/extendables/get_readable.js: -------------------------------------------------------------------------------- 1 | exports.conf = { 2 | type: "get", 3 | method: "readable", 4 | appliesTo: ["GroupDMChannel", "DMChannel", "TextChannel"], 5 | }; 6 | 7 | // eslint-disable-next-line func-names 8 | exports.extend = function () { 9 | if (!this.guild) return true; 10 | return this.permissionsFor(this.guild.me).has("VIEW_CHANNEL"); 11 | }; 12 | -------------------------------------------------------------------------------- /src/extendables/sendCode.js: -------------------------------------------------------------------------------- 1 | exports.conf = { 2 | type: "method", 3 | method: "sendCode", 4 | appliesTo: ["TextChannel", "DMChannel", "GroupDMChannel"], 5 | }; 6 | 7 | // eslint-disable-next-line func-names 8 | exports.extend = function (lang, content, options = {}) { 9 | return this.sendMessage(content, Object.assign(options, { code: lang })); 10 | }; 11 | -------------------------------------------------------------------------------- /src/functions/handleError.js: -------------------------------------------------------------------------------- 1 | module.exports = (client, msg, error) => { 2 | if (error.stack) { 3 | client.emit("error", error.stack); 4 | } else if (error.message) { 5 | msg.sendCode("JSON", error.message).catch(err => client.emit("error", err)); 6 | } else { 7 | msg.sendMessage(error).catch(err => client.emit("error", err)); 8 | } 9 | }; 10 | -------------------------------------------------------------------------------- /src/extendables/sendFile.js: -------------------------------------------------------------------------------- 1 | exports.conf = { 2 | type: "method", 3 | method: "sendFile", 4 | appliesTo: ["TextChannel", "DMChannel", "GroupDMChannel"], 5 | }; 6 | 7 | // eslint-disable-next-line func-names 8 | exports.extend = function (attachment, name, content, options = {}) { 9 | return this.send({ files: [{ attachment, name }], content, options }); 10 | }; 11 | -------------------------------------------------------------------------------- /src/extendables/get_attachable.js: -------------------------------------------------------------------------------- 1 | exports.conf = { 2 | type: "get", 3 | method: "attachable", 4 | appliesTo: ["GroupDMChannel", "DMChannel", "TextChannel"], 5 | }; 6 | 7 | // eslint-disable-next-line func-names 8 | exports.extend = function () { 9 | if (!this.guild) return true; 10 | return this.postable && this.permissionsFor(this.guild.me).has("ATTACH_FILES"); 11 | }; 12 | -------------------------------------------------------------------------------- /src/extendables/get_embedable.js: -------------------------------------------------------------------------------- 1 | exports.conf = { 2 | type: "get", 3 | method: "embedable", 4 | appliesTo: ["GroupDMChannel", "DMChannel", "TextChannel"], 5 | }; 6 | 7 | // eslint-disable-next-line func-names 8 | exports.extend = function () { 9 | if (!this.guild) return true; 10 | return this.postable && this.permissionsFor(this.guild.me).has("EMBED_LINKS"); 11 | }; 12 | -------------------------------------------------------------------------------- /src/extendables/get_postable.js: -------------------------------------------------------------------------------- 1 | exports.conf = { 2 | type: "get", 3 | method: "postable", 4 | appliesTo: ["GroupDMChannel", "DMChannel", "TextChannel"], 5 | }; 6 | 7 | // eslint-disable-next-line func-names 8 | exports.extend = function () { 9 | if (!this.guild) return true; 10 | return this.readable && this.permissionsFor(this.guild.me).has("SEND_MESSAGES"); 11 | }; 12 | -------------------------------------------------------------------------------- /src/inhibitors/permissions.js: -------------------------------------------------------------------------------- 1 | exports.conf = { 2 | enabled: true, 3 | spamProtection: false, 4 | priority: 10, 5 | }; 6 | 7 | exports.run = (client, msg, cmd) => { 8 | const res = client.funcs.checkPerms(client, msg, cmd.conf.permLevel); 9 | if (res === null) return true; 10 | else if (!res) return "You do not have permission to use this command."; 11 | return false; 12 | }; 13 | -------------------------------------------------------------------------------- /src/finalizers/commandCooldown.js: -------------------------------------------------------------------------------- 1 | exports.conf = { 2 | enabled: true, 3 | }; 4 | 5 | exports.run = (client, msg) => { 6 | if (msg.author.id === client.config.ownerID) return; 7 | if (!msg.command.conf.cooldown || msg.command.conf.cooldown <= 0) return; 8 | 9 | msg.command.cooldown.set(msg.author.id, Date.now()); 10 | setTimeout(() => msg.command.cooldown.delete(msg.author.id), msg.command.conf.cooldown * 1000); 11 | }; 12 | -------------------------------------------------------------------------------- /src/functions/newError.js: -------------------------------------------------------------------------------- 1 | module.exports = (error, code) => { 2 | if (error.status) { 3 | this.statusCode = error.response.res.statusCode; 4 | this.statusMessage = error.response.res.statusMessage; 5 | this.code = error.response.body.code; 6 | this.message = error.response.body.message; 7 | return this; 8 | } 9 | this.code = code || null; 10 | this.message = error; 11 | this.stack = error.stack || null; 12 | return this; 13 | }; 14 | -------------------------------------------------------------------------------- /src/inhibitors/runIn.js: -------------------------------------------------------------------------------- 1 | exports.conf = { 2 | enabled: true, 3 | spamProtection: false, 4 | priority: 8, 5 | }; 6 | 7 | exports.run = (client, msg, cmd) => { 8 | if (!cmd.conf.runIn || cmd.conf.runIn.length <= 0) return `The ${cmd.help.name} command is not configured to run in any channel.`; 9 | if (cmd.conf.runIn.includes(msg.channel.type)) return false; 10 | return `This command is only available in ${cmd.conf.runIn.join(" ")} channels`; 11 | }; 12 | -------------------------------------------------------------------------------- /src/extendables/sendEmbed.js: -------------------------------------------------------------------------------- 1 | exports.conf = { 2 | type: "method", 3 | method: "sendEmbed", 4 | appliesTo: ["TextChannel", "DMChannel", "GroupDMChannel"], 5 | }; 6 | 7 | // eslint-disable-next-line func-names 8 | exports.extend = function (embed, content, options) { 9 | if (!options && typeof content === "object") { 10 | options = content; 11 | content = ""; 12 | } else if (!options) { 13 | options = {}; 14 | } 15 | return this.sendMessage(content, Object.assign(options, { embed })); 16 | }; 17 | -------------------------------------------------------------------------------- /src/commands/System/reboot.js: -------------------------------------------------------------------------------- 1 | exports.run = async (client, msg) => { 2 | await msg.sendMessage("Rebooting...").catch(err => client.emit("error", err)); 3 | process.exit(); 4 | }; 5 | 6 | exports.conf = { 7 | enabled: true, 8 | runIn: ["text", "dm", "group"], 9 | aliases: [], 10 | permLevel: 10, 11 | botPerms: ["SEND_MESSAGES"], 12 | requiredFuncs: [], 13 | requiredSettings: [], 14 | }; 15 | 16 | exports.help = { 17 | name: "reboot", 18 | description: "Reboots the bot.", 19 | usage: "", 20 | usageDelim: "", 21 | }; 22 | -------------------------------------------------------------------------------- /src/inhibitors/requiredSettings.js: -------------------------------------------------------------------------------- 1 | exports.conf = { 2 | enabled: true, 3 | spamProtection: false, 4 | priority: 6, 5 | }; 6 | 7 | exports.run = (client, msg, cmd) => { 8 | if (!cmd.conf.requiredSettings || cmd.conf.requiredSettings.length === 0) return false; 9 | const settings = cmd.conf.requiredSettings.filter(setting => !msg.guildSettings[setting]); 10 | if (settings.length > 0) return `The guild is missing the **${settings.join(", ")}** guild setting${settings.length > 1 ? "s" : ""} and cannot run.`; 11 | return false; 12 | }; 13 | -------------------------------------------------------------------------------- /src/inhibitors/requiredFuncs.js: -------------------------------------------------------------------------------- 1 | exports.conf = { 2 | enabled: true, 3 | spamProtection: false, 4 | priority: 6, 5 | }; 6 | 7 | /* eslint-disable no-prototype-builtins */ 8 | exports.run = (client, msg, cmd) => { 9 | if (!cmd.conf.requiredFuncs || cmd.conf.requiredFuncs.length === 0) return false; 10 | const funcs = cmd.conf.requiredFuncs.filter(func => !client.funcs.hasOwnProperty(func)); 11 | if (funcs.length > 0) return `The client is missing the **${funcs.join(", ")}** function${funcs.length > 1 ? "s" : ""} and cannot run.`; 12 | return false; 13 | }; 14 | -------------------------------------------------------------------------------- /src/functions/getPrefix.js: -------------------------------------------------------------------------------- 1 | module.exports = (client, msg) => { 2 | if (client.config.prefixMention.test(msg.content)) return client.config.prefixMention; 3 | const prefix = msg.guildSettings.prefix || client.config.prefix; 4 | const { regExpEsc } = client.funcs; 5 | if (prefix instanceof Array) { 6 | for (let i = prefix.length - 1; i >= 0; i--) { 7 | if (msg.content.startsWith(prefix[i])) return new RegExp(`^${regExpEsc(prefix[i])}`); 8 | } 9 | } else if (prefix && msg.content.startsWith(prefix)) return new RegExp(`^${regExpEsc(prefix)}`); 10 | return false; 11 | }; 12 | -------------------------------------------------------------------------------- /src/functions/clean.js: -------------------------------------------------------------------------------- 1 | const zws = String.fromCharCode(8203); 2 | let sensitivePattern; 3 | 4 | module.exports = (client, text) => { 5 | if (typeof text === "string") { 6 | return text.replace(sensitivePattern, "「redacted」").replace(/`/g, `\`${zws}`).replace(/@/g, `@${zws}`); 7 | } 8 | return text; 9 | }; 10 | 11 | module.exports.init = (client) => { 12 | const patterns = []; 13 | if (client.token) patterns.push(client.token); 14 | if (client.user.email) patterns.push(client.user.email); 15 | if (client.password) patterns.push(client.password); 16 | sensitivePattern = new RegExp(patterns.join("|"), "gi"); 17 | }; 18 | -------------------------------------------------------------------------------- /src/commands/System/ping.js: -------------------------------------------------------------------------------- 1 | exports.run = async (client, msg) => { 2 | const message = await msg.sendMessage("Ping?"); 3 | return msg.sendMessage(`Pong! (Roundtrip took: ${message.createdTimestamp - msg.createdTimestamp}ms. Heartbeat: ${Math.round(client.ping)}ms.)`); 4 | }; 5 | 6 | exports.conf = { 7 | enabled: true, 8 | runIn: ["text", "dm", "group"], 9 | aliases: [], 10 | permLevel: 0, 11 | botPerms: ["SEND_MESSAGES"], 12 | requiredFuncs: [], 13 | requiredSettings: [], 14 | }; 15 | 16 | exports.help = { 17 | name: "ping", 18 | description: "Ping/Pong command. I wonder what this does? /sarcasm", 19 | usage: "", 20 | usageDelim: "", 21 | }; 22 | -------------------------------------------------------------------------------- /src/inhibitors/commandCooldown.js: -------------------------------------------------------------------------------- 1 | exports.conf = { 2 | enabled: true, 3 | spamProtection: true, 4 | priority: 11, 5 | }; 6 | 7 | exports.run = (client, msg, cmd) => { 8 | if (msg.author.id === client.config.ownerID) return false; 9 | if (!cmd.conf.cooldown || cmd.conf.cooldown <= 0) return false; 10 | 11 | const instance = cmd.cooldown.get(msg.author.id); 12 | 13 | if (!instance) return false; 14 | 15 | const remaining = ((cmd.conf.cooldown * 1000) - (Date.now() - instance)) / 1000; 16 | 17 | if (remaining < 0) { 18 | cmd.cooldown.delete(msg.author.id); 19 | return false; 20 | } 21 | 22 | return `You have just used this command. You can use this command again in ${Math.ceil(remaining)} seconds.`; 23 | }; 24 | -------------------------------------------------------------------------------- /src/extendables/sendMessage.js: -------------------------------------------------------------------------------- 1 | exports.conf = { 2 | type: "method", 3 | method: "sendMessage", 4 | appliesTo: ["TextChannel", "DMChannel", "GroupDMChannel"], 5 | }; 6 | 7 | // eslint-disable-next-line func-names 8 | exports.extend = function (content, options) { 9 | if (!this.channel) return this.send(content, options); 10 | const commandMessage = this.client.commandMessages.get(this.id); 11 | if (commandMessage && (!options || !("files" in options))) return commandMessage.response.edit(content, options); 12 | return this.channel.send(content, options) 13 | .then((mes) => { 14 | if (mes.constructor.name === "Message" && (!options || !("files" in options))) this.client.commandMessages.set(this.id, { trigger: this, response: mes }); 15 | return mes; 16 | }); 17 | }; 18 | -------------------------------------------------------------------------------- /docs/scripts/prettify/lang-css.js: -------------------------------------------------------------------------------- 1 | PR.registerLangHandler(PR.createSimpleLexer([["pln",/^[\t\n\f\r ]+/,null," \t\r\n "]],[["str",/^"(?:[^\n\f\r"\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*"/,null],["str",/^'(?:[^\n\f\r'\\]|\\(?:\r\n?|\n|\f)|\\[\S\s])*'/,null],["lang-css-str",/^url\(([^"')]*)\)/i],["kwd",/^(?:url|rgb|!important|@import|@page|@media|@charset|inherit)(?=[^\w-]|$)/i,null],["lang-css-kw",/^(-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*)\s*:/i],["com",/^\/\*[^*]*\*+(?:[^*/][^*]*\*+)*\//],["com", 2 | /^(?:<\!--|--\>)/],["lit",/^(?:\d+|\d*\.\d+)(?:%|[a-z]+)?/i],["lit",/^#[\da-f]{3,6}/i],["pln",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i],["pun",/^[^\s\w"']+/]]),["css"]);PR.registerLangHandler(PR.createSimpleLexer([],[["kwd",/^-?(?:[_a-z]|\\[\da-f]+ ?)(?:[\w-]|\\\\[\da-f]+ ?)*/i]]),["css-kw"]);PR.registerLangHandler(PR.createSimpleLexer([],[["str",/^[^"')]+/]]),["css-str"]); 3 | -------------------------------------------------------------------------------- /.travis-deploy.sh: -------------------------------------------------------------------------------- 1 | #!/bin/bash 2 | # Based on https://github.com/hydrabolt/discord.js-site/blob/master/deploy/deploy.sh 3 | 4 | set -e 5 | 6 | if [ "$TRAVIS_BRANCH" != "master" -o -n "$TRAVIS_TAG" -o "$TRAVIS_PULL_REQUEST" != "false" ]; then 7 | echo -e "Not building for a non indev branch push - building without deploying." 8 | npm run docs 9 | exit 0 10 | fi 11 | 12 | echo -e "Building for a indev branch push - building and deploying." 13 | 14 | REPO=$(git config remote.origin.url) 15 | SHA=$(git rev-parse --verify HEAD) 16 | 17 | TARGET_BRANCH="master" 18 | git clone $REPO dist -b $TARGET_BRANCH 19 | 20 | npm run docs 21 | 22 | rsync -vau docs/ dist/docs/ 23 | 24 | cd dist 25 | git add --all . 26 | git config user.name "Travis CI" 27 | git config user.email "${COMMIT_EMAIL}" 28 | git commit -m "Docs build: ${SHA}" || true 29 | git push "https://${GH_TOKEN}@${GH_REF}" $TARGET_BRANCH 30 | -------------------------------------------------------------------------------- /src/providers/collection.js: -------------------------------------------------------------------------------- 1 | const { Collection } = require("discord.js"); 2 | 3 | exports.database = new Collection(); 4 | 5 | exports.getTable = table => this.database.get(table) || this.database.set(table, new Collection()).get(table); 6 | 7 | exports.getAll = table => Array.from(this.getTable(table).values()); 8 | 9 | exports.get = (table, id) => { 10 | const collection = this.getTable(table); 11 | return collection.get(id) || null; 12 | }; 13 | 14 | exports.has = (table, id) => !!this.get(table, id); 15 | 16 | exports.set = (table, id, data) => { 17 | const collection = this.getTable(table); 18 | return collection.set(id, data); 19 | }; 20 | 21 | exports.delete = (table, id) => { 22 | const collection = this.getTable(table); 23 | return collection.delete(id); 24 | }; 25 | 26 | exports.conf = { 27 | moduleName: "collection", 28 | enabled: true, 29 | requiredModules: [], 30 | }; 31 | -------------------------------------------------------------------------------- /src/documentation/EventStructure.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Events are the same as they are in discord.js with the excecption of one thing: we pass client to every event. 3 | * @module Event 4 | * @example They will always follow this structure. You will always name the file the event you are trying to use. ex: ready event : ready.js 5 | * exports.run = (client, ...args) => { // code here }; 6 | */ 7 | 8 | /** 9 | * The part of the extendable that is added to the class. 10 | * @param {KomadaClient} client The Komada client 11 | * @param {Array} args The arguments normally given to the Discord.js event. 12 | * @example This will create a ready event that sets the status to the below string. You would name this "ready.js" 13 | * exports.run = (client) => { 14 | * client.user.setStatus(`Komada | ${require("komada").version} `); 15 | * } 16 | */ 17 | exports.run = (client, ...args) => {}; // eslint-disable-line 18 | -------------------------------------------------------------------------------- /docs/scripts/linenumber.js: -------------------------------------------------------------------------------- 1 | /*global document */ 2 | (function() { 3 | var source = document.getElementsByClassName('prettyprint source linenums'); 4 | var i = 0; 5 | var lineNumber = 0; 6 | var lineId; 7 | var lines; 8 | var totalLines; 9 | var anchorHash; 10 | var lineNumberHTML = ''; 11 | 12 | if (source && source[0]) { 13 | anchorHash = document.location.hash.substring(1); 14 | lines = source[0].getElementsByTagName('li'); 15 | totalLines = lines.length; 16 | 17 | for (; i < totalLines; i++) { 18 | lineNumber++; 19 | lineId = 'line' + lineNumber; 20 | lines[i].id = lineId; 21 | 22 | lineNumberHTML = '' + (i + 1) + ' : '; 23 | 24 | lines[i].insertAdjacentHTML('afterBegin', lineNumberHTML); 25 | if (lineId === anchorHash) { 26 | lines[i].className += ' selected'; 27 | } 28 | } 29 | } 30 | })(); 31 | -------------------------------------------------------------------------------- /src/documentation/FunctionStructure.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Function pieces are exactly what they are... they're functions. This can range from a single function, to a group of functions. 3 | * Their structure can be almost anything, the only requirement is that you have to assign it as a module for Komada to understand what it is. 4 | * @module Functions 5 | * @example An example of a single function saved as "add.js". 6 | * module.exports = (var, var2) => var + var2; // accessed via 'client.funcs.add' 7 | * @example An example of multiple functions in one file, named math.js, and accessed via 'client.funcs.group["exportName"]' 8 | * exports.add = (var, var2) => var + var2; // accessed via 'client.funcs.math.add' 9 | * exports.subtract = (var, var2) => var - var2; // accessed via 'client.funcs.math.subtract' 10 | * exports.mutliply = (var, var2) => var * var2; // accessed via 'client.funcs.math.multiply' 11 | * exports.divide = (var, var2) => var / var2; // accessed via 'client.funcs.math.divide' 12 | */ 13 | -------------------------------------------------------------------------------- /.docstrap.json: -------------------------------------------------------------------------------- 1 | { 2 | "templates": { 3 | "name" : "Komada", 4 | "footerText" : "Welcome to Komada's Documentation! Discord: https://discord.gg/FpEFSyY", 5 | "tabNames": { 6 | "api" : "Documentation", 7 | "tutorials" : "Examples" 8 | }, 9 | "logo": { 10 | "url" : "https://cdn.discordapp.com/avatars/275809850422722560/4e85b97ec4d79f709bbedd849c0e6ad9.png?size=256", 11 | "width" : "100px", 12 | "height" : "100px", 13 | "link" : "https://github.com/dirigeants/komada" 14 | } 15 | }, 16 | "source": { 17 | "include" : [ "./src"] 18 | }, 19 | "opts": { 20 | "template" : "node_modules/tui-jsdoc-template", 21 | "encoding" : "utf8", 22 | "destination" : "./docs/", 23 | "tutorials" : "./examples", 24 | "recurse" : true 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/inhibitors/missingBotPermissions.js: -------------------------------------------------------------------------------- 1 | const { Permissions } = require("discord.js"); 2 | 3 | const impliedPermissions = new Permissions([ 4 | "VIEW_CHANNEL", 5 | "SEND_MESSAGES", 6 | "SEND_TTS_MESSAGES", 7 | "EMBED_LINKS", 8 | "ATTACH_FILES", 9 | "READ_MESSAGE_HISTORY", 10 | "MENTION_EVERYONE", 11 | "USE_EXTERNAL_EMOJIS", 12 | "ADD_REACTIONS", 13 | ]); 14 | 15 | exports.conf = { 16 | enabled: true, 17 | spamProtection: false, 18 | priority: 7, 19 | }; 20 | 21 | exports.run = (client, msg, cmd) => { 22 | const missing = msg.channel.type === "text" ? msg.channel.permissionsFor(client.user).missing(cmd.conf.botPerms) : impliedPermissions.missing(cmd.conf.botPerms); 23 | if (missing.length > 0) { 24 | if (missing.includes("SEND_MESSAGES")) { 25 | client.emit("log", `[Channel: ${msg.channel.id}] Insufficient permissions, missing: **${client.funcs.toTitleCase(missing.join(", ").split("_").join(" "))}**`, "error"); 26 | return true; 27 | } 28 | return `Insufficient permissions, missing: **${client.funcs.toTitleCase(missing.join(", ").split("_").join(" "))}**`; 29 | } 30 | return false; 31 | }; 32 | -------------------------------------------------------------------------------- /src/commands/System/invite.js: -------------------------------------------------------------------------------- 1 | exports.run = async (client, msg) => { 2 | if (!client.user.bot) return msg.reply("Why would you need an invite link for a selfbot..."); 3 | 4 | return msg.sendMessage([ 5 | `To add ${client.user.username} to your discord guild:`, 6 | client.invite, 7 | [ 8 | "```The above link is generated requesting the minimum permissions required to use every command currently.", 9 | "I know not all permissions are right for every server, so don't be afraid to uncheck any of the boxes.", 10 | "If you try to use a command that requires more permissions than the bot is granted, it will let you know.```", 11 | ].join(" "), 12 | "Please file an issue at if you find any bugs.", 13 | ]); 14 | }; 15 | 16 | exports.conf = { 17 | enabled: true, 18 | runIn: ["text"], 19 | aliases: [], 20 | permLevel: 0, 21 | botPerms: ["SEND_MESSAGES"], 22 | requiredFuncs: [], 23 | requiredSettings: [], 24 | }; 25 | 26 | exports.help = { 27 | name: "invite", 28 | description: "Displays the join server link of the bot.", 29 | usage: "", 30 | usageDelim: "", 31 | }; 32 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | MIT License 2 | 3 | Copyright (c) 2016 Évelyne Lachance 4 | 5 | Permission is hereby granted, free of charge, to any person obtaining a copy 6 | of this software and associated documentation files (the "Software"), to deal 7 | in the Software without restriction, including without limitation the rights 8 | to use, copy, modify, merge, publish, distribute, sublicense, and/or sell 9 | copies of the Software, and to permit persons to whom the Software is 10 | furnished to do so, subject to the following conditions: 11 | 12 | The above copyright notice and this permission notice shall be included in all 13 | copies or substantial portions of the Software. 14 | 15 | THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR 16 | IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, 17 | FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE 18 | AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER 19 | LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, 20 | OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE 21 | SOFTWARE. 22 | -------------------------------------------------------------------------------- /src/commands/System/stats.js: -------------------------------------------------------------------------------- 1 | const { version: discordVersion } = require("discord.js"); 2 | const { version: komadaVersion, Duration } = require("komada"); 3 | 4 | exports.run = async (client, msg) => msg.sendCode("asciidoc", [ 5 | "= STATISTICS =", 6 | "", 7 | `• Mem Usage :: ${(process.memoryUsage().heapUsed / 1024 / 1024).toFixed(2)} MB`, 8 | `• Uptime :: ${Duration.format(client.uptime)}`, 9 | `• Users :: ${client.guilds.reduce((a, b) => a + b.memberCount, 0).toLocaleString()}`, 10 | `• Servers :: ${client.guilds.size.toLocaleString()}`, 11 | `• Channels :: ${client.channels.size.toLocaleString()}`, 12 | `• Komada :: v${komadaVersion}`, 13 | `• Discord.js :: v${discordVersion}`, 14 | `• Node.js :: ${process.version}`, 15 | ]); 16 | 17 | exports.conf = { 18 | enabled: true, 19 | runIn: ["text", "dm", "group"], 20 | aliases: ["details", "what"], 21 | permLevel: 0, 22 | botPerms: ["SEND_MESSAGES"], 23 | requiredFuncs: [], 24 | requiredSettings: [], 25 | }; 26 | 27 | exports.help = { 28 | name: "stats", 29 | description: "Provides some details about the bot and stats.", 30 | usage: "", 31 | usageDelim: "", 32 | }; 33 | -------------------------------------------------------------------------------- /src/util/Timestamp.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Simple class for formatting epoch milliseconds into a short-handed format. 3 | */ 4 | 5 | class Timestamp { 6 | 7 | /** 8 | * Formats a Date Object into a shorthand date/time format. 9 | * @param {Date} date A Javascript Date Objected, created by doing new Date() 10 | * @return {string} The newly created titmestamp for this date object. 11 | */ 12 | static format(date) { 13 | if (!(date instanceof Date)) throw "Date object not passed."; 14 | const year = date.getYear(); 15 | const month = date.getMonth(); 16 | const day = date.getDate(); 17 | const hours = date.getHours(); 18 | const minutes = date.getMinutes(); 19 | const seconds = date.getSeconds(); 20 | const output = []; 21 | output.push([ 22 | year.toString().length < 4 ? year + 1900 : year, 23 | month < 10 ? `0${month}` : month, 24 | day < 10 ? `0${day}` : day, 25 | ].join("-")); 26 | output.push(" "); 27 | output.push([ 28 | hours < 10 ? `0${hours}` : hours, 29 | minutes < 10 ? `0${minutes}` : minutes, 30 | seconds < 10 ? `0${seconds}` : seconds, 31 | ].join(":")); 32 | return output.join(""); 33 | } 34 | 35 | } 36 | 37 | module.exports = Timestamp; 38 | -------------------------------------------------------------------------------- /package.json: -------------------------------------------------------------------------------- 1 | { 2 | "name": "komada", 3 | "version": "0.22.0", 4 | "description": "Komada: Croatian for 'pieces', is a modular bot system including reloading modules and easy to use custom commands.", 5 | "homepage": "https://github.com/dirigeants/komada#readme", 6 | "bugs": { 7 | "url": "https://github.com/dirigeants/komada/issues" 8 | }, 9 | "license": "MIT", 10 | "author": "Evelyne Lachance", 11 | "main": "src/index.js", 12 | "repository": { 13 | "type": "git", 14 | "url": "https://github.com/dirigeants/komada.git" 15 | }, 16 | "scripts": { 17 | "lint": "npm run test -- --fix", 18 | "test": "npx eslint src", 19 | "docs": "npx jsdoc -c ./.docstrap.json -R README.md" 20 | }, 21 | "dependencies": { 22 | "fs-nextra": "^0.3.0" 23 | }, 24 | "peerDependencies": { 25 | "discord.js": "github:discordjs/discord.js" 26 | }, 27 | "devDependencies": { 28 | "eslint": "^4.9.0", 29 | "eslint-config-airbnb-base": "^12.1.0", 30 | "eslint-plugin-import": "^2.7.0", 31 | "ink-docstrap": "^1.3.2", 32 | "jsdoc": "github:jsdoc3/jsdoc#master", 33 | "tui-jsdoc-template": "github:dirigeants/tui.jsdoc-template" 34 | }, 35 | "engines": { 36 | "node": ">=8.9.1" 37 | } 38 | } 39 | -------------------------------------------------------------------------------- /src/commands/System/eval.js: -------------------------------------------------------------------------------- 1 | const { inspect } = require("util"); 2 | const { MessageAttachment } = require("discord.js"); 3 | 4 | /* eslint-disable no-eval, consistent-return */ 5 | exports.run = async (client, msg, [code]) => { 6 | try { 7 | let evaled = eval(code); 8 | if (evaled instanceof Promise) evaled = await evaled; 9 | if (typeof evaled !== "string") evaled = inspect(evaled, { depth: 0 }); 10 | const output = client.funcs.clean(client, evaled); 11 | if (output.length > 1992) { 12 | return msg.channel.send(new MessageAttachment(Buffer.from(output), "output.txt")); 13 | } 14 | return msg.sendCode("js", output); 15 | } catch (err) { 16 | msg.sendMessage(`\`ERROR\` \`\`\`js\n${client.funcs.clean(client, err)}\n\`\`\``); 17 | if (err.stack) client.emit("error", err.stack); 18 | } 19 | }; 20 | 21 | exports.conf = { 22 | enabled: true, 23 | runIn: ["text", "dm", "group"], 24 | aliases: ["ev"], 25 | permLevel: 10, 26 | botPerms: ["SEND_MESSAGES"], 27 | requiredFuncs: [], 28 | requiredSettings: [], 29 | }; 30 | 31 | exports.help = { 32 | name: "eval", 33 | description: "Evaluates arbitrary Javascript. Reserved for bot owner.", 34 | usage: "", 35 | usageDelim: "", 36 | }; 37 | -------------------------------------------------------------------------------- /src/documentation/Finalizer.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Finalizers are functions that are ran after a Command has successfully been ran. Examples of these are 3 | * cooldown setting after a command is ran and command logging. 4 | * @module Finalizer 5 | * @example They will always follow this structure. 6 | * exports.run = (client, msg, mes) => { // code here }; 7 | * exports.conf = {}; 8 | */ 9 | 10 | /** 11 | * The part of the monitor that will run on the message. 12 | * @param {KomadaClient} client The Komada Client 13 | * @param {Message} msg A Message object obtained from discord.js 14 | * @param {?} [mes] Something returned from the command for use in finalizers, like a message. 15 | * @example This will create a finalizer that logs commands. 16 | * exports.run = (client, msg, cmd) => { 17 | * console.log(`Message ${msg.id} contained the command ${cmd.help.name} and was ran with the arguments ${msg.args.join(",")}`); 18 | * } 19 | */ 20 | exports.run = (client, msg, mes) => {}; // eslint-disable-line 21 | 22 | 23 | /** 24 | * An Object containing configuration values that will configure a monitor. 25 | * @typedef {Object} Conf 26 | * @property {Boolean} enabled Whether or not this monitor should be enabled for use. 27 | */ 28 | 29 | 30 | /** 31 | * An object that configures the monitor. 32 | * @type {Conf} 33 | * @example 34 | * exports.conf = { 35 | enabled: true, 36 | }; 37 | */ 38 | exports.conf = {}; 39 | -------------------------------------------------------------------------------- /src/commands/System/disable.js: -------------------------------------------------------------------------------- 1 | const longTypes = { command: "commands", inhibitor: "commandInhibitors", monitor: "messageMonitors", finalizer: "commandFinalizers" }; 2 | const { resolve } = require("path"); 3 | const fs = require("fs-nextra"); 4 | 5 | 6 | exports.init = async (client) => { 7 | this._path = resolve(client.clientBaseDir, "bwd", "disabled.json"); 8 | this.disable = client.funcs._disabled; 9 | }; 10 | 11 | exports.run = async (client, msg, [type, name]) => { 12 | let toDisable = client[longTypes[type]].get(name); 13 | if (!toDisable && type === "command") toDisable = client.commands.get(client.aliases.get(name)); 14 | if (!toDisable) return msg.sendCode("diff", `- I cannot find the ${type}: ${name}`); 15 | if (this.disable[type].includes(name)) return msg.sendCode("diff", "- That piece is already currently disabled."); 16 | toDisable.conf.enabled = false; 17 | this.disable[type].push(name); 18 | fs.outputJSONAtomic(this._path, this.disable); 19 | return msg.sendCode("diff", `+ Successfully disabled ${type}: ${name}`); 20 | }; 21 | 22 | exports.conf = { 23 | enabled: true, 24 | runIn: ["text", "dm", "group"], 25 | aliases: [], 26 | permLevel: 10, 27 | botPerms: ["SEND_MESSAGES"], 28 | requiredFuncs: [], 29 | requiredSettings: [], 30 | }; 31 | 32 | exports.help = { 33 | name: "disable", 34 | description: "Permanently disables a command/inhibitor/monitor/finalizer. Saved through bot reboots.", 35 | usage: " ", 36 | usageDelim: " ", 37 | }; 38 | -------------------------------------------------------------------------------- /src/commands/System/enable.js: -------------------------------------------------------------------------------- 1 | const longTypes = { command: "commands", inhibitor: "commandInhibitors", monitor: "messageMonitors", finalizer: "commandFinalizers" }; 2 | const fs = require("fs-nextra"); 3 | const { resolve } = require("path"); 4 | 5 | 6 | exports.init = async (client) => { 7 | this._path = resolve(client.clientBaseDir, "bwd", "disabled.json"); 8 | this.disable = client.funcs._disabled; 9 | }; 10 | 11 | exports.run = async (client, msg, [type, name]) => { 12 | let toEnable = client[longTypes[type]].get(name); 13 | if (!toEnable && type === "command") toEnable = client.commands.get(client.aliases.get(name)); 14 | if (!toEnable) return msg.sendCode("diff", `- I cannot find the ${type}: ${name}`); 15 | if (!this.disable[type].includes(name)) return msg.sendCode("diff", "- That piece isn't currently disabled."); 16 | toEnable.conf.enabled = true; 17 | this.disable[type].splice(this.disable[type].indexOf(name), 1); 18 | fs.outputJSONAtomic(this._path, this.disable); 19 | return msg.sendCode("diff", `+ Successfully enabled ${type}: ${name}`); 20 | }; 21 | 22 | exports.conf = { 23 | enabled: true, 24 | runIn: ["text", "dm", "group"], 25 | aliases: [], 26 | permLevel: 10, 27 | botPerms: ["SEND_MESSAGES"], 28 | requiredFuncs: [], 29 | requiredSettings: [], 30 | }; 31 | 32 | exports.help = { 33 | name: "enable", 34 | description: "Permanently enables a command/inhibitor/monitor/finalizer. Saved through bot reboots.", 35 | usage: " ", 36 | usageDelim: " ", 37 | }; 38 | -------------------------------------------------------------------------------- /src/documentation/ExtendableStructure.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Extendables allow you to "extend" native Discord.js classes with functions that you can use. 3 | * @module Extendable 4 | * @example They will always follow this structure. 5 | * exports.extend = function() { // code here }; 6 | * exports.conf = {}; 7 | */ 8 | 9 | /** 10 | * The part of the extendable that is added to the class. 11 | * @example This will create an extendable that replies to the author of the message with "Pong!" 12 | * exports.extend = function() { 13 | * return this.reply("Pong!"); // The keyword 'this' refers to a Message Object. 14 | * } 15 | */ 16 | exports.extend = function() {}; // eslint-disable-line 17 | 18 | 19 | /** 20 | * An Object containing configuration values that will configure an extendable. 21 | * @typedef {Object} Conf 22 | * @property {String} type Type of extendable. This will be one of the three: "method", "set", or "get". 23 | * @property {String} method The name of this extendable. 24 | * @property {Array} appliesTo An array of Discord.js classes that this extendable will apply to. 25 | * @property {boolean} komada Whether or not this extendable should apply to Komada or Discord.js 26 | */ 27 | 28 | 29 | /** 30 | * An object that configures the extendable. 31 | * @type {Conf} 32 | * @example When applied to Message, this would be accessed via .ping(); 33 | * exports.conf = { 34 | type: "method", 35 | method: "ping", 36 | appliesTo: ["Message"], 37 | komada: false 38 | }; 39 | */ 40 | exports.conf = {}; 41 | -------------------------------------------------------------------------------- /src/documentation/MonitorStructure.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Monitor are functions that are designed to watch incoming messages. These can range from 3 | * checking a message for specific words, or logging all mentions to a channel. 4 | * @module Monitor 5 | * @example They will always follow this structure. 6 | * exports.run = (client, msg) => { // code here }; 7 | * exports.conf = {}; 8 | */ 9 | 10 | /** 11 | * The part of the monitor that will run on the message. 12 | * @param {KomadaClient} client The Komada Client 13 | * @param {Message} msg A Message object obtained from discord.js 14 | * @example This will create a monitor that logs every message that mentions the bot. 15 | * exports.run = (client, msg, cmd) => { 16 | * if (msg.mentions.users.has(client.user.id)) console.log(`Message ${msg.id} contained the bots mention: ${msg.cleanContent}`); 17 | * } 18 | */ 19 | exports.run = (client, msg) => {}; // eslint-disable-line 20 | 21 | 22 | /** 23 | * An Object containing configuration values that will configure a monitor. 24 | * @typedef {Object} Conf 25 | * @property {Boolean} enabled Whether or not this monitor should be enabled for use. 26 | * @property {Boolean} ignoreBots Whether or not this monitor should ignore other bots. 27 | * @property {Boolean} ignoreSelf Whether or not this monitor should ignore messages from the ClientUser. 28 | */ 29 | 30 | 31 | /** 32 | * An object that configures the monitor. 33 | * @type {Conf} 34 | * @example 35 | * exports.conf = { 36 | enabled: true, 37 | ignoreBots: true, 38 | ignoreSelf: true 39 | }; 40 | */ 41 | exports.conf = {}; 42 | -------------------------------------------------------------------------------- /src/finalizers/commandlogging.js: -------------------------------------------------------------------------------- 1 | const { performance: { now } } = require("perf_hooks"); 2 | 3 | const colors = { 4 | prompted: { message: { background: "red" } }, 5 | notprompted: { message: { background: "blue" } }, 6 | user: { message: { background: "yellow", text: "black" } }, 7 | channel: { 8 | text: { message: { background: "green" } }, 9 | dm: { message: { background: "magenta" } }, 10 | group: { message: { background: "cyan" } }, 11 | }, 12 | }; 13 | 14 | exports.conf = { 15 | enabled: true, 16 | }; 17 | 18 | exports.run = (client, msg, mes, start) => { 19 | if (client.config.cmdLogging) { 20 | client.emit("log", [ 21 | `${msg.command.help.name}(${msg.args.join(", ")})`, 22 | msg.reprompted ? `${client.console.messages((`[${(now() - start).toFixed(2)}ms]`), colors.prompted.message)}` : `${client.console.messages(`[${(now() - start).toFixed(2)}ms]`, colors.notprompted.message)}`, 23 | `${client.console.messages(`${msg.author.username}[${msg.author.id}]`, colors.user.message)}`, 24 | this.channel(msg), 25 | ].join(" "), "log"); 26 | } 27 | }; 28 | 29 | exports.channel = (msg) => { 30 | switch (msg.channel.type) { 31 | case "text": 32 | return `${msg.client.console.messages(`${msg.guild.name}[${msg.guild.id}]`, colors.channel.text.message)}`; 33 | case "dm": 34 | return `${msg.client.console.messages("Direct Messages", colors.channel.dm.message)}`; 35 | case "group": 36 | return `${msg.client.console.messages(`Group DM => ${msg.channel.owner.username}[${msg.channel.owner.id}]`, colors.channel.group.message)}`; 37 | default: 38 | return "not going to happen"; 39 | } 40 | }; 41 | -------------------------------------------------------------------------------- /CONTRIBUTING.md: -------------------------------------------------------------------------------- 1 | ## Issues & Pull Requests 2 | - Issues and Pull Requests now have a template. 3 | Ensure you follow these templates or your Issue/PR may be auto-denied. 4 | 5 | - In the subject title of a PR place a [TYPE] at the front of the title. 6 | TYPE can be BUGFIX, FEATURE, REMOVAL or anything that is suitable for the PR 7 | 8 | - When making changes to the source code, ensure the changes are based on the **Indev** branch of the repo. 9 | This is to ensure that any changes are not outdated. 10 | 11 | ## Commits 12 | - Whilst other Contribs have thier own commit message styles, having a commit message template for the Git command-line can streamline this. 13 | An example of this is below: 14 | 15 | ``` 16 | # If this is the first commit in a PR 17 | # Start the first commit with a Category Header 18 | # All subsequent commits are to have the [PR] header 19 | 20 | # Subject Line: [BUG-FIX/FEATURE/REMOVAL] 21 | 22 | # Updated Semver Proposal [MAJOR/MINOR/PATCH] 23 | SEMVER: 24 | 25 | # What is being committed. 26 | Why: 27 | 28 | * 29 | 30 | # What does it fix? 31 | 32 | This change addresses the need by: 33 | 34 | * 35 | 36 | # Issue Tracker References go here, Use github # 37 | # Example, Fixes #123 (Refers to Issue #123) 38 | 39 | 40 | # Commit Character Length limits are below 41 | # 50-character subject line 42 | # 43 | # 72-character wrapped longer description. 44 | 45 | ``` 46 | 47 | - The above example is saved in a `.gitmessage` file and stored in git config via `git config commit.template /path/to/.gitmessage` 48 | 49 | ## Questions 50 | - If there are any Questions regarding the Contribution Guidelines, Contact CyberiumShadow on the evie.codes Discord Server 51 | -------------------------------------------------------------------------------- /src/commands/System/transfer.js: -------------------------------------------------------------------------------- 1 | const fs = require("fs-nextra"); 2 | const { resolve } = require("path"); 3 | 4 | exports.run = async (client, msg, [type, name]) => { 5 | const coreDir = client.coreBaseDir; 6 | const clientDir = client.clientBaseDir; 7 | const reload = { 8 | command: client.funcs._reloadCommand, 9 | function: client.funcs._reloadFunction, 10 | inhibitor: client.funcs._reloadInhibitor, 11 | finalizer: client.funcs._reloadFinalizer, 12 | event: client.funcs._reloadEvent, 13 | monitor: client.funcs._reloadMonitor, 14 | }; 15 | if (type === "command") name = `System/${name}`; 16 | const fileLocation = resolve(coreDir, `${type}s`, `${name}.js`); 17 | await fs.access(fileLocation).catch(() => { throw "❌ That file has been transfered already or never existed."; }); 18 | return fs.copy(fileLocation, resolve(clientDir, `${type}s`, `${name}.js`)) 19 | .then(() => { 20 | reload[type].call(client.funcs, `${name}`).catch((response) => { throw `❌ ${response}`; }); 21 | return msg.sendMessage(`✅ Successfully Transferred ${type}: ${name}`); 22 | }) 23 | .catch((err) => { 24 | client.emit("error", err.stack); 25 | return msg.sendMessage(`Transfer of ${type}: ${name} to Client has failed. Please check your Console.`); 26 | }); 27 | }; 28 | 29 | exports.conf = { 30 | enabled: true, 31 | runIn: ["text", "dm", "group"], 32 | aliases: [], 33 | permLevel: 10, 34 | botPerms: ["SEND_MESSAGES"], 35 | requiredFuncs: [], 36 | requiredSettings: [], 37 | }; 38 | 39 | exports.help = { 40 | name: "transfer", 41 | description: "Transfers a core piece to its respective folder", 42 | usage: " ", 43 | usageDelim: " ", 44 | }; 45 | -------------------------------------------------------------------------------- /.eslintrc.json: -------------------------------------------------------------------------------- 1 | { 2 | "parserOptions": { 3 | "ecmaVersion": 8 4 | }, 5 | "env": { 6 | "es6": true, 7 | "node": true 8 | }, 9 | "extends": "airbnb-base", 10 | "rules": { 11 | "no-console": "off", 12 | "object-curly-newline": "off", 13 | "function-paren-newline": "off", 14 | "indent": ["error", 2, { "SwitchCase": 1 }], 15 | "linebreak-style": "off", 16 | "quotes": ["warn", "double"], 17 | "semi": ["warn", "always"], 18 | "no-param-reassign": "off", 19 | "no-shadow": "warn", 20 | "no-plusplus": "off", 21 | "no-continue": "off", 22 | "radix": ["error", "as-needed"], 23 | "import/no-extraneous-dependencies": "off", 24 | "import/no-unresolved": "off", 25 | "import/no-dynamic-require": "warn", 26 | "no-prototype-builtins": "warn", 27 | "no-restricted-syntax": "warn", 28 | "no-throw-literal": "off", 29 | "guard-for-in": "warn", 30 | "consistent-return": ["warn", { "treatUndefinedAsUnspecified": true }], 31 | "no-use-before-define": ["warn", { "functions": true, "classes": true }], 32 | "no-eval": "warn", 33 | "max-len": "off", 34 | "no-underscore-dangle": "off", 35 | "global-require": "off", 36 | "no-nested-ternary": "warn", 37 | "padded-blocks": ["error", { "classes": "always", "blocks": "never", "switches": "never" }], 38 | "valid-jsdoc": ["warn", { 39 | "requireReturn": false, 40 | "requireReturnDescription": false, 41 | "preferType": { 42 | "String": "string", 43 | "Number": "number", 44 | "Boolean": "boolean", 45 | "Symbol": "symbol", 46 | "function": "Function", 47 | "object": "Object", 48 | "date": "Date", 49 | "error": "Error" 50 | } 51 | }] 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/commands/System/info.js: -------------------------------------------------------------------------------- 1 | exports.run = async (client, msg) => { 2 | const information = [ 3 | "Komada is a 'plug-and-play' framework built on top of the Discord.js library.", 4 | "Most of the code is modularized, which allows developers to edit Komada to suit their needs.", 5 | "", 6 | "Some features of Komada include:", 7 | "• 🐇💨 Fast loading times with ES2017 support (`async`/`await`)", 8 | "• 🎚🎛 Per-server settings that can be extended with your own fields", 9 | "• 💬 Customizable command system with automated parameter resolving and the ability to reload commands and download new modules on-the-fly", 10 | "• 👀 \"Monitors\", which can watch messages and edits (for swear filters, spam protection, etc.)", 11 | "• ⛔ \"Inhibitors\", which can prevent commands from running based on any condition you wish to apply (for permissions, blacklists, etc.)", 12 | "• 🗄 \"Providers\", which simplify usage of any database of your choosing", 13 | "• ✅ \"Finalizers\", which run after successful commands (for logging, collecting stats, cleaning up responses, etc.)", 14 | "• ➕ \"Extendables\", which passively add methods, getters/setters, or static properties to existing Discord.js or Komada classes", 15 | "• 🇫 Internal \"Functions\", which allow you to use functions anywhere you have access to a client variable", 16 | "", 17 | "We hope to be a 100% customizable framework that can cater to all audiences. We do frequent updates and bugfixes when available.", 18 | "If you're interested in us, check us out at https://komada.js.org", 19 | ]; 20 | return msg.sendMessage(information); 21 | }; 22 | 23 | exports.conf = { 24 | enabled: true, 25 | runIn: ["text", "dm", "group"], 26 | aliases: ["details", "what"], 27 | permLevel: 0, 28 | botPerms: ["SEND_MESSAGES"], 29 | requiredFuncs: [], 30 | requiredSettings: [], 31 | }; 32 | 33 | exports.help = { 34 | name: "info", 35 | description: "Provides some information about this bot.", 36 | usage: "", 37 | usageDelim: "", 38 | }; 39 | -------------------------------------------------------------------------------- /docs/styles/prettify-jsdoc.css: -------------------------------------------------------------------------------- 1 | /* JSDoc prettify.js theme */ 2 | 3 | /* plain text */ 4 | .pln { 5 | color: #000000; 6 | font-weight: normal; 7 | font-style: normal; 8 | } 9 | 10 | /* string content */ 11 | .str { 12 | color: #006400; 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | 17 | /* a keyword */ 18 | .kwd { 19 | color: #000000; 20 | font-weight: bold; 21 | font-style: normal; 22 | } 23 | 24 | /* a comment */ 25 | .com { 26 | font-weight: normal; 27 | font-style: italic; 28 | } 29 | 30 | /* a type name */ 31 | .typ { 32 | color: #000000; 33 | font-weight: normal; 34 | font-style: normal; 35 | } 36 | 37 | /* a literal value */ 38 | .lit { 39 | color: #006400; 40 | font-weight: normal; 41 | font-style: normal; 42 | } 43 | 44 | /* punctuation */ 45 | .pun { 46 | color: #000000; 47 | font-weight: bold; 48 | font-style: normal; 49 | } 50 | 51 | /* lisp open bracket */ 52 | .opn { 53 | color: #000000; 54 | font-weight: bold; 55 | font-style: normal; 56 | } 57 | 58 | /* lisp close bracket */ 59 | .clo { 60 | color: #000000; 61 | font-weight: bold; 62 | font-style: normal; 63 | } 64 | 65 | /* a markup tag name */ 66 | .tag { 67 | color: #006400; 68 | font-weight: normal; 69 | font-style: normal; 70 | } 71 | 72 | /* a markup attribute name */ 73 | .atn { 74 | color: #006400; 75 | font-weight: normal; 76 | font-style: normal; 77 | } 78 | 79 | /* a markup attribute value */ 80 | .atv { 81 | color: #006400; 82 | font-weight: normal; 83 | font-style: normal; 84 | } 85 | 86 | /* a declaration */ 87 | .dec { 88 | color: #000000; 89 | font-weight: bold; 90 | font-style: normal; 91 | } 92 | 93 | /* a variable name */ 94 | .var { 95 | color: #000000; 96 | font-weight: normal; 97 | font-style: normal; 98 | } 99 | 100 | /* a function name */ 101 | .fun { 102 | color: #000000; 103 | font-weight: bold; 104 | font-style: normal; 105 | } 106 | 107 | /* Specify class=linenums on a pre to get line numbering */ 108 | ol.linenums { 109 | margin-top: 0; 110 | margin-bottom: 0; 111 | } 112 | -------------------------------------------------------------------------------- /src/util/Duration.js: -------------------------------------------------------------------------------- 1 | 2 | /** 3 | * Simple class used to turn durations into a human readable format. 4 | */ 5 | class Duration { 6 | 7 | /** 8 | * Constructs our duration class so that we can start formatting. 9 | */ 10 | constructor() { 11 | /** 12 | * The number of milliseconds in one second. 13 | * @type {number} 14 | */ 15 | this.second = 1000; 16 | 17 | /** 18 | * The number of milliseconds in one minute. 19 | * @type {number} 20 | */ 21 | this.minute = this.second * 60; 22 | 23 | /** 24 | * The number of milliseconds in one hour. 25 | * @type {number} 26 | */ 27 | this.hour = this.minute * 60; 28 | 29 | /** 30 | * The number of milliseconds in one day. 31 | * @type {number} 32 | */ 33 | this.day = this.hour * 24; 34 | 35 | /** 36 | * The number of milliseconds in one week. 37 | * @type {number} 38 | */ 39 | this.week = this.day * 7; 40 | } 41 | 42 | /** 43 | * Formats a time that is given in milliseconds. 44 | * @param {number} time The number of milliseconds we want to convert to a readable time. 45 | * @return {string} A human readable string of the time. 46 | */ 47 | static format(time) { 48 | const output = []; 49 | const weeks = `${Math.floor(time / this.week)}`; 50 | const days = `${Math.floor((time - (weeks * this.week)) / this.day)}`; 51 | const hours = `${Math.floor((time - (weeks * this.week) - (days * this.day)) / this.hour)}`; 52 | const minutes = `${Math.floor((time - (weeks * this.week) - (days * this.day) - (hours * this.hour)) / this.minute)}`; 53 | const seconds = `${Math.floor((time - (weeks * this.week) - (days * this.day) - (hours * this.hour) - (minutes * this.minute)) / this.second)}`; 54 | if (weeks > 0) output.push(`${weeks} weeks`); 55 | if (days > 0) output.push(`${days.substr(-2)} days`); 56 | if (hours > 0) output.push(`${hours.substr(-2)} hours`); 57 | if (minutes > 0) output.push(`${minutes.substr(-2)} minutes`); 58 | if (seconds > 0) output.push(`${seconds.substr(-2)} seconds`); 59 | return output.join(", "); 60 | } 61 | 62 | } 63 | 64 | module.exports = Duration; 65 | -------------------------------------------------------------------------------- /src/providers/json.js: -------------------------------------------------------------------------------- 1 | const { resolve } = require("path"); 2 | const fs = require("fs-nextra"); 3 | 4 | let baseDir; 5 | 6 | exports.init = (client) => { 7 | if (baseDir) return null; 8 | baseDir = resolve(client.clientBaseDir, "bwd", "provider", "json"); 9 | return fs.ensureDir(baseDir).catch(err => client.emit("log", err, "error")); 10 | }; 11 | 12 | exports.hasTable = table => fs.pathExists(resolve(baseDir, table)); 13 | 14 | exports.createTable = table => fs.mkdir(resolve(baseDir, table)); 15 | 16 | exports.deleteTable = table => this.hasTable(table) 17 | .then(exists => (exists ? fs.emptyDir(resolve(baseDir, table)).then(() => fs.remove(resolve(baseDir, table))) : null)); 18 | 19 | exports.getAll = (table) => { 20 | const dir = resolve(baseDir, table); 21 | return fs.readdir(dir) 22 | .then(files => Promise.all(files.map(file => fs.readJSON(resolve(dir, file))))); 23 | }; 24 | 25 | exports.get = (table, document) => fs.readJSON(resolve(baseDir, table, `${document}.json`)).catch(() => null); 26 | 27 | exports.has = (table, document) => fs.pathExists(resolve(baseDir, table, `${document}.json`)); 28 | 29 | exports.getRandom = table => this.getAll(table).then(data => data[Math.floor(Math.random() * data.length)]); 30 | 31 | exports.create = (table, document, data) => fs.outputJSONAtomic(resolve(baseDir, table, `${document}.json`), Object.assign(data, { id: document })); 32 | exports.set = (...args) => this.create(...args); 33 | exports.insert = (...args) => this.create(...args); 34 | 35 | exports.update = (table, document, data) => this.get(table, document) 36 | .then(current => fs.outputJSONAtomic(resolve(baseDir, table, `${document}.json`), Object.assign(current, data))); 37 | 38 | exports.replace = (table, document, data) => fs.outputJSONAtomic(resolve(baseDir, table, `${document}.json`), data); 39 | 40 | exports.delete = (table, document) => fs.unlink(resolve(baseDir, table, `${document}.json`)); 41 | 42 | exports.conf = { 43 | moduleName: "json", 44 | enabled: true, 45 | requiredModules: ["fs-nextra"], 46 | }; 47 | 48 | exports.help = { 49 | name: "json", 50 | type: "providers", 51 | description: "Allows you to use JSON functionality throught Komada", 52 | }; 53 | -------------------------------------------------------------------------------- /src/commands/System/reload.js: -------------------------------------------------------------------------------- 1 | exports.run = async (client, msg, [type, name]) => { 2 | type = client.funcs.toTitleCase(type); 3 | if (name === "all") { 4 | await client.funcs[`_load${type}s`](); 5 | switch (type) { 6 | case "Function": 7 | await Promise.all(Object.keys(client.funcs).map((key) => { 8 | if (client.funcs[key].init) return client.funcs[key].init(client); 9 | return true; 10 | })); 11 | break; 12 | 13 | case "Inhibitor": 14 | await Promise.all(client.commandInhibitors.map((piece) => { 15 | if (piece.init) return piece.init(client); 16 | return true; 17 | })); 18 | break; 19 | 20 | case "Finalizer": 21 | await Promise.all(client.commandFinalizers.map((piece) => { 22 | if (piece.init) return piece.init(client); 23 | return true; 24 | })); 25 | break; 26 | 27 | case "Monitor": 28 | await Promise.all(client.messageMonitors.map((piece) => { 29 | if (piece.init) return piece.init(client); 30 | return true; 31 | })); 32 | break; 33 | 34 | case "Provider": 35 | await Promise.all(client.providers.map((piece) => { 36 | if (piece.init) return piece.init(client); 37 | return true; 38 | })); 39 | break; 40 | 41 | case "Command": 42 | await Promise.all(client.commands.map((piece) => { 43 | if (piece.init) return piece.init(client); 44 | return true; 45 | })); 46 | break; 47 | // no default 48 | } 49 | return msg.sendMessage(`✅ Reloaded all ${type}s`); 50 | } 51 | const mes = await msg.sendMessage(`Attemping to reload ${type} ${name}`); 52 | return client.funcs[`_reload${type}`](name) 53 | .then(mess => mes.edit(`✅ ${mess}`)) 54 | .catch(err => mes.edit(`❌ ${err}`)); 55 | }; 56 | 57 | exports.conf = { 58 | enabled: true, 59 | runIn: ["text", "dm", "group"], 60 | aliases: ["r", "load"], 61 | permLevel: 10, 62 | botPerms: ["SEND_MESSAGES"], 63 | requiredFuncs: [], 64 | requiredSettings: [], 65 | }; 66 | 67 | exports.help = { 68 | name: "reload", 69 | description: "Reloads the command file, if it's been updated or modified.", 70 | usage: " ", 71 | usageDelim: " ", 72 | }; 73 | -------------------------------------------------------------------------------- /src/monitors/commandHandler.js: -------------------------------------------------------------------------------- 1 | const { performance: { now } } = require("perf_hooks"); 2 | 3 | exports.conf = { 4 | enabled: true, 5 | ignoreBots: true, 6 | ignoreSelf: true, 7 | }; 8 | 9 | exports.run = (client, msg) => { 10 | if (!client.ready) return; 11 | if (!msg._handle) return; 12 | const res = this.parseCommand(client, msg); 13 | if (!res.command) return; 14 | this.handleCommand(client, msg, res); 15 | }; 16 | 17 | exports.parseCommand = (client, msg, usage = false) => { 18 | const prefix = client.funcs.getPrefix(client, msg); 19 | if (!prefix) return false; 20 | const prefixLength = this.getLength(client, msg, prefix); 21 | if (usage) return prefixLength; 22 | return { 23 | command: msg.content.slice(prefixLength).trim().split(" ")[0].toLowerCase(), 24 | prefix, 25 | length: prefixLength, 26 | }; 27 | }; 28 | 29 | exports.getLength = (client, msg, prefix) => { 30 | if (client.config.prefixMention === prefix) return prefix.exec(msg.content)[0].length + 1; 31 | return prefix.exec(msg.content)[0].length; 32 | }; 33 | 34 | exports.handleCommand = (client, msg, { command, prefix, length }) => { 35 | command = client.commands.get(command) || client.commands.get(client.aliases.get(command)); 36 | if (!command) return; 37 | msg._registerCommand({ command, prefix, length }); 38 | const start = now(); 39 | const response = this.runInhibitors(client, msg, msg.command); 40 | if (response) { 41 | if (typeof response === "string") msg.reply(response); 42 | return; 43 | } 44 | this.runCommand(client, msg, start); 45 | }; 46 | 47 | exports.runCommand = async (client, msg, start) => { 48 | await msg.validateArgs() 49 | .catch((error) => { throw client.funcs.handleError(client, msg, error); }); 50 | msg.command.run(client, msg, msg.params) 51 | .then(mes => this.runFinalizers(client, msg, mes, start)) 52 | .catch(err => client.funcs.handleError(client, msg, err)); 53 | }; 54 | 55 | exports.runInhibitors = (client, msg, command) => { 56 | let response; 57 | client.commandInhibitors.some((inhibitor) => { 58 | if (inhibitor.conf.enabled) { 59 | response = inhibitor.run(client, msg, command); 60 | if (response) return true; 61 | } 62 | return false; 63 | }); 64 | return response; 65 | }; 66 | 67 | exports.runFinalizers = (client, msg, mes, start) => { 68 | Promise.all(client.commandFinalizers.filter(i => i.conf.enabled).map(item => item.run(client, msg, mes, start))); 69 | }; 70 | -------------------------------------------------------------------------------- /src/documentation/InhibitorStructure.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Inhibitors are functions that are designed to prevent or allow a user to run a command. These can range from 3 | * checking a users specific permission level to checking if the user is on cooldown. 4 | * @module Inhibitor 5 | * @example They will always follow this structure. 6 | * exports.run = (client, msg, cmd) => { // code here }; 7 | * exports.conf = {}; 8 | */ 9 | 10 | /** 11 | * The part of the inhibitor that will determine if the user can use the command or not. This function must return one of three things.
12 | * If the user should be allowed to use the command, you will return false.
13 | * If the user should prevented from using the command, you will return true. If you do this, the command will be silent.
14 | * However, if you would like to give the user a message as to why they couldn't use the command, you can also return a String, such as `return "Not enough permissions to use this command."`. 15 | * @param {KomadaClient} client The Komada Client 16 | * @param {Message} msg A Message object obtained from discord.js 17 | * @param {Command} cmd The command that the user is trying to run. 18 | * @example This will create an inhibitor that only runs when the commands "requiredUser" configuration property has the message authors id in it. 19 | * exports.run = (client, msg, cmd) => { 20 | * if (!cmd.conf.requiredUser || !(cmd.conf.requiredUser instanceof Array) || cmd.conf.requiredUser.length === 0) return false; 21 | * if (cmd.conf.requiredUser.includes(message.author.id)) return false; 22 | * return "You are not allowed to use this command."; 23 | * } 24 | * @return {string|boolean} 25 | */ 26 | exports.run = (client, msg, cmd) => ({}); // eslint-disable-line 27 | 28 | 29 | /** 30 | * An Object containing configuration values that will configure a inhibitor. 31 | * @typedef {Object} Conf 32 | * @property {Boolean} enabled Whether or not this inhibitor should be enabled for use. 33 | * @property {Number} priority The priority of this inhibitor. **This will probably be removed in the future** 34 | * @property {Boolean} spamProtection Whether or not we should run this inhibitor in other places, like the help menu. 35 | */ 36 | 37 | 38 | /** 39 | * An object that configures the inhibitor. 40 | * @type {Conf} 41 | * @example 42 | * exports.conf = { 43 | enabled: true, 44 | priority: 0, 45 | spamProtection: false 46 | }; 47 | */ 48 | exports.conf = {}; 49 | -------------------------------------------------------------------------------- /docs/styles/prettify-tomorrow.css: -------------------------------------------------------------------------------- 1 | /* Tomorrow Theme */ 2 | /* Original theme - https://github.com/chriskempson/tomorrow-theme */ 3 | /* Pretty printing styles. Used with prettify.js. */ 4 | /* SPAN elements with the classes below are added by prettyprint. */ 5 | /* plain text */ 6 | .pln { 7 | color: #4d4d4c; } 8 | 9 | @media screen { 10 | /* string content */ 11 | .str { 12 | color: #718c00; } 13 | 14 | /* a keyword */ 15 | .kwd { 16 | color: #8959a8; } 17 | 18 | /* a comment */ 19 | .com { 20 | color: #8e908c; } 21 | 22 | /* a type name */ 23 | .typ { 24 | color: #4271ae; } 25 | 26 | /* a literal value */ 27 | .lit { 28 | color: #f5871f; } 29 | 30 | /* punctuation */ 31 | .pun { 32 | color: #4d4d4c; } 33 | 34 | /* lisp open bracket */ 35 | .opn { 36 | color: #4d4d4c; } 37 | 38 | /* lisp close bracket */ 39 | .clo { 40 | color: #4d4d4c; } 41 | 42 | /* a markup tag name */ 43 | .tag { 44 | color: #c82829; } 45 | 46 | /* a markup attribute name */ 47 | .atn { 48 | color: #f5871f; } 49 | 50 | /* a markup attribute value */ 51 | .atv { 52 | color: #3e999f; } 53 | 54 | /* a declaration */ 55 | .dec { 56 | color: #f5871f; } 57 | 58 | /* a variable name */ 59 | .var { 60 | color: #c82829; } 61 | 62 | /* a function name */ 63 | .fun { 64 | color: #4271ae; } } 65 | /* Use higher contrast and text-weight for printable form. */ 66 | @media print, projection { 67 | .str { 68 | color: #060; } 69 | 70 | .kwd { 71 | color: #006; 72 | font-weight: bold; } 73 | 74 | .com { 75 | color: #600; 76 | font-style: italic; } 77 | 78 | .typ { 79 | color: #404; 80 | font-weight: bold; } 81 | 82 | .lit { 83 | color: #044; } 84 | 85 | .pun, .opn, .clo { 86 | color: #440; } 87 | 88 | .tag { 89 | color: #006; 90 | font-weight: bold; } 91 | 92 | .atn { 93 | color: #404; } 94 | 95 | .atv { 96 | color: #060; } } 97 | /* Style */ 98 | /* 99 | pre.prettyprint { 100 | background: white; 101 | font-family: Consolas, Monaco, 'Andale Mono', monospace; 102 | font-size: 12px; 103 | line-height: 1.5; 104 | border: 1px solid #ccc; 105 | padding: 10px; } 106 | */ 107 | 108 | /* Specify class=linenums on a pre to get line numbering */ 109 | ol.linenums { 110 | margin-top: 0; 111 | margin-bottom: 0; } 112 | 113 | /* IE indents via margin-left */ 114 | li.L0, 115 | li.L1, 116 | li.L2, 117 | li.L3, 118 | li.L4, 119 | li.L5, 120 | li.L6, 121 | li.L7, 122 | li.L8, 123 | li.L9 { 124 | /* */ } 125 | 126 | /* Alternate shading for lines */ 127 | li.L1, 128 | li.L3, 129 | li.L5, 130 | li.L7, 131 | li.L9 { 132 | /* */ } 133 | -------------------------------------------------------------------------------- /src/classes/PermissionLevels.js: -------------------------------------------------------------------------------- 1 | /** 2 | * A helper class for building valid permission Structures 3 | */ 4 | class PermissionLevels { 5 | 6 | /** 7 | * @typedef {object} permLevel 8 | * @memberof PermissionLevels 9 | * @property {boolean} break Whether the level should break (stop processing higher levels, and inhibit a no permission error) 10 | * @property {Function} check The permission checking function 11 | */ 12 | 13 | /** 14 | * Creates a new PermissionLevels instance. 15 | * Note that constructing any PermissionLevel instance will always insert a "Owner only" level for you 16 | * at the highest number you specify automatically. You can still however remove this and change it after 17 | * constructing. 18 | * @param {number} [size=10] The number of levels you want to allocate for this PermissionLevels instance 19 | */ 20 | constructor(size = 10) { 21 | const s = parseInt(size); 22 | if (typeof s !== "number" || size < 0) throw new Error("Size must be a valid integer above zero."); 23 | 24 | /** 25 | * The number of permission levels allowed in this instance. 26 | * Technically this will be the size you input + 1 since Array[0] would be Level 0 27 | * @type {number} 28 | */ 29 | this.size = s; 30 | 31 | /** 32 | * Cached array of levels that get used for determining permissions. 33 | * @type {Array} 34 | */ 35 | this.levels = new Array(s).fill().map(() => ({ break: false, check: () => false })); 36 | this.levels[s] = { break: false, check: (client, msg) => client.user === msg.author }; 37 | } 38 | 39 | /** 40 | * Adds levels to the levels array to be used in your bot. 41 | * @param {number} level The permission number for the level you are defining 42 | * @param {boolean} brk Whether the level should break (stop processing higher levels, and inhibit a no permission error) 43 | * @param {Function} check The permission checking function 44 | * @returns {PermissionLevels} This permission levels 45 | */ 46 | add(level, brk, check) { 47 | if (level > this.size) throw new Error(`Level ${level} is higher then the allocated amount (${this.size}) of levels.`); 48 | if (typeof brk !== "boolean") throw new Error("Break must be a boolean value."); 49 | if (typeof check !== "function") throw new Error("Check must be a function."); 50 | this.levels[level] = { break: brk, check }; 51 | return this; 52 | } 53 | 54 | /** 55 | * Resets a level back to the default permission object. 56 | * @param {number} level The level you want to reset. 57 | * @returns {PermissionLevels} This permission levels. 58 | */ 59 | reset(level) { 60 | this.levels[level] = { break: false, check: () => false }; 61 | return this; 62 | } 63 | 64 | } 65 | 66 | module.exports = PermissionLevels; 67 | -------------------------------------------------------------------------------- /src/util/Stopwatch.js: -------------------------------------------------------------------------------- 1 | const { performance } = require("perf_hooks"); 2 | 3 | /** 4 | * Stopwatch class, uses native node to replicate/extend previous performance now dependancy. 5 | */ 6 | class Stopwatch { 7 | 8 | /** 9 | * Starts a new Stopwatch 10 | * @since 0.4.0 11 | * @param {number} [digits = 2] The number of digits to appear after the decimal point when returning the friendly duration 12 | */ 13 | constructor(digits = 2) { 14 | /** 15 | * The start time of this stopwatch 16 | * @private 17 | * @type {number} 18 | */ 19 | this._start = performance.now(); 20 | 21 | /** 22 | * The end time of this stopwatch 23 | * @private 24 | * @type {?number} 25 | */ 26 | this._end = null; 27 | 28 | /** 29 | * The number of digits to appear after the decimal point when returning the friendly duration. 30 | * @type {number} 31 | */ 32 | this.digits = digits; 33 | } 34 | 35 | /** 36 | * The duration of this stopwatch since start or start to end if this stopwatch has stopped. 37 | * @readonly 38 | * @type {number} 39 | */ 40 | get duration() { 41 | return this._end ? this._end - this._start : performance.now() - this._start; 42 | } 43 | 44 | /** 45 | * The duration formatted in a friendly string 46 | * @readonly 47 | * @type {string} 48 | */ 49 | get friendlyDuration() { 50 | const time = this.duration; 51 | if (time >= 1000) return `${(time / 1000).toFixed(this.digits)}s`; 52 | if (time >= 1) return `${time.toFixed(this.digits)}ms`; 53 | return `${(time * 1000).toFixed(this.digits)}μs`; 54 | } 55 | 56 | /** 57 | * If the stopwatch is running or not 58 | * @readonly 59 | * @type {boolean} 60 | */ 61 | get running() { 62 | return Boolean(!this._end); 63 | } 64 | 65 | /** 66 | * Restarts the Stopwatch (Returns a running state) 67 | * @returns {Stopwatch} 68 | */ 69 | restart() { 70 | this._start = performance.now(); 71 | this._end = null; 72 | return this; 73 | } 74 | 75 | /** 76 | * Resets the Stopwatch to 0 duration (Returns a stopped state) 77 | * @returns {Stopwatch} 78 | */ 79 | reset() { 80 | this._start = performance.now(); 81 | this._end = this._start; 82 | return this; 83 | } 84 | 85 | /** 86 | * Starts the Stopwatch 87 | * @returns {Stopwatch} 88 | */ 89 | start() { 90 | if (!this.running) { 91 | this._start = performance.now() - this.duration; 92 | this._end = null; 93 | } 94 | return this; 95 | } 96 | 97 | /** 98 | * Stops the Stopwatch, freezing the duration 99 | * @returns {Stopwatch} 100 | */ 101 | stop() { 102 | if (this.running) this._end = performance.now(); 103 | return this; 104 | } 105 | 106 | /** 107 | * Defines toString behavior o return the friendlyDuration 108 | * @since 0.4.0 109 | * @returns {string} 110 | */ 111 | toString() { 112 | return this.friendlyDuration; 113 | } 114 | 115 | } 116 | 117 | module.exports = Stopwatch; 118 | -------------------------------------------------------------------------------- /src/commands/System/conf.js: -------------------------------------------------------------------------------- 1 | const { inspect } = require("util"); 2 | 3 | const handle = (value) => { 4 | if (typeof value !== "object") return value; 5 | if (value === null) return "Not set"; 6 | if (value instanceof Array) return value[0] ? `[ ${value.join(" | ")} ]` : "None"; 7 | return value; 8 | }; 9 | 10 | exports.run = async (client, msg, [action, key, ...value]) => { 11 | const configs = msg.guild.settings; 12 | switch (action) { 13 | case "set": { 14 | if (!key) return msg.sendMessage("You must provide a key"); 15 | if (!value[0]) return msg.sendMessage("You must provide a value"); 16 | if (client.settings.guilds.schema[key].array) { 17 | await client.settings.guilds.updateArray(msg.guild, "add", key, value.join(" ")); 18 | return msg.sendMessage(`Successfully added the value \`${value.join(" ")}\` to the key: **${key}**`); 19 | } 20 | const response = await client.settings.guilds.update(msg.guild, { [key]: value.join(" ") }); 21 | return msg.sendMessage(`Successfully updated the key **${key}**: \`${response[key]}\``); 22 | } 23 | case "remove": { 24 | if (!key) return msg.sendMessage("You must provide a key"); 25 | if (!value[0]) return msg.sendMessage("You must provide a value"); 26 | if (!client.settings.guilds.schema[key].array) return msg.sendMessage("This key is not array type. Use the action 'reset' instead."); 27 | return client.settings.guilds.updateArray(msg.guild, "remove", key, value.join(" ")) 28 | .then(() => msg.sendMessage(`Successfully removed the value \`${value.join(" ")}\` from the key: **${key}**`)) 29 | .catch(e => msg.sendMessage(e)); 30 | } 31 | case "get": { 32 | if (!key) return msg.sendMessage("You must provide a key"); 33 | if (!(key in configs)) return msg.sendMessage(`The key **${key}** does not seem to exist.`); 34 | return msg.sendMessage(`The value for the key **${key}** is: \`${inspect(configs[key])}\``); 35 | } 36 | case "reset": { 37 | if (!key) return msg.sendMessage("You must provide a key"); 38 | const response = await client.settings.guilds.reset(msg.guild, key); 39 | return msg.sendMessage(`The key **${key}** has been reset to: \`${response}\``); 40 | } 41 | case "list": { 42 | const longest = Object.keys(configs).sort((a, b) => a.length < b.length)[0].length; 43 | const output = ["= Guild Settings ="]; 44 | const entries = Object.entries(configs); 45 | for (let i = 0; i < entries.length; i++) { 46 | if (entries[i][0] === "id") continue; 47 | output.push(`${entries[i][0].padEnd(longest)} :: ${handle(entries[i][1])}`); 48 | } 49 | return msg.sendCode("asciidoc", output); 50 | } 51 | // no default 52 | } 53 | 54 | return null; 55 | }; 56 | 57 | exports.conf = { 58 | enabled: true, 59 | runIn: ["text"], 60 | aliases: [], 61 | permLevel: 3, 62 | botPerms: ["SEND_MESSAGES"], 63 | requiredFuncs: [], 64 | requiredSettings: [], 65 | }; 66 | 67 | exports.help = { 68 | name: "conf", 69 | description: "Define per-server configuration.", 70 | usage: " [key:string] [value:string] [...]", 71 | usageDelim: " ", 72 | }; 73 | -------------------------------------------------------------------------------- /src/commands/System/help.js: -------------------------------------------------------------------------------- 1 | exports.run = async (client, msg, [cmd]) => { 2 | const method = client.user.bot ? "author" : "channel"; 3 | if (cmd) { 4 | cmd = client.commands.get(cmd) || client.commands.get(client.aliases.get(cmd)); 5 | if (!cmd) return msg.sendMessage("❌ | Unknown command, please run the help command with no arguments to get a list of them all."); 6 | if (!this.runCommandInhibitors(client, msg, cmd)) return; // eslint-disable-line 7 | const info = [ 8 | `= ${cmd.help.name} = `, 9 | cmd.help.description, 10 | `usage :: ${cmd.usage.fullUsage(msg)}`, 11 | "Extended Help ::", 12 | cmd.help.extendedHelp || "No extended help available.", 13 | ].join("\n"); 14 | return msg.sendMessage(info, { code: "asciidoc" }); 15 | } 16 | const help = this.buildHelp(client, msg); 17 | const categories = Object.keys(help); 18 | const helpMessage = []; 19 | for (let cat = 0; cat < categories.length; cat++) { 20 | helpMessage.push(`**${categories[cat]} Commands**: \`\`\`asciidoc`); 21 | const subCategories = Object.keys(help[categories[cat]]); 22 | for (let subCat = 0; subCat < subCategories.length; subCat++) helpMessage.push(`= ${subCategories[subCat]} =`, `${help[categories[cat]][subCategories[subCat]].join("\n")}\n`); 23 | helpMessage.push("```\n\u200b"); 24 | } 25 | return msg[method].send(helpMessage, { split: { char: "\u200b" } }) 26 | .then(() => { if (msg.channel.type !== "dm" && client.user.bot) msg.sendMessage("📥 | Commands have been sent to your DMs."); }) 27 | .catch(() => { if (msg.channel.type !== "dm" && client.user.bot) msg.sendMessage("❌ | You have DMs disabled, I couldn't send you the commands in DMs."); }); 28 | }; 29 | 30 | exports.conf = { 31 | enabled: true, 32 | runIn: ["text", "dm", "group"], 33 | aliases: ["commands"], 34 | permLevel: 0, 35 | botPerms: ["SEND_MESSAGES"], 36 | requiredFuncs: [], 37 | requiredSettings: [], 38 | }; 39 | 40 | exports.help = { 41 | name: "help", 42 | description: "Display help for a command.", 43 | usage: "[command:str]", 44 | usageDelim: "", 45 | }; 46 | 47 | /* eslint-disable no-restricted-syntax, no-prototype-builtins */ 48 | exports.buildHelp = (client, msg) => { 49 | const help = {}; 50 | const prefix = msg.guildSettings.prefix || client.config.prefix; 51 | 52 | const commandNames = Array.from(client.commands.keys()); 53 | const longest = commandNames.reduce((long, str) => Math.max(long, str.length), 0); 54 | 55 | for (const command of client.commands.values()) { 56 | if (this.runCommandInhibitors(client, msg, command)) { 57 | const cat = command.help.category; 58 | const subcat = command.help.subCategory; 59 | if (!help.hasOwnProperty(cat)) help[cat] = {}; 60 | if (!help[cat].hasOwnProperty(subcat)) help[cat][subcat] = []; 61 | help[cat][subcat].push(`\u00A0${prefix}${command.help.name.padEnd(longest)} :: ${command.help.description}`); 62 | } 63 | } 64 | 65 | return help; 66 | }; 67 | 68 | exports.runCommandInhibitors = (client, msg, command) => !client.commandInhibitors.some((inhibitor) => { 69 | if (!inhibitor.conf.spamProtection && inhibitor.conf.enabled) return inhibitor.run(client, msg, command); 70 | return false; 71 | }); 72 | -------------------------------------------------------------------------------- /src/documentation/CommandStructure.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Commands are pieces of code ran when messages contain the bots prefix. Along with these, we have a full argument parsing system for use in commands. 3 | * @module Command 4 | * @example They will always follow this structure 5 | * exports.run = async (client, msg, ...args) => { // code here }; 6 | * exports.help = {}; 7 | * exports.conf = {}; 8 | */ 9 | 10 | /** 11 | * The part of the command that will run. This should always return a Promise to prevent issues in Komada. The easy way to do this is to add the async keyword. 12 | * @param {KomadaClient} client The Komada Client 13 | * @param {Message} msg A Message object obtained from discord.js 14 | * @param {Array} args An array of arguments passed through by our argument parser. 15 | * @example 16 | * exports.run = (client, msg) => msg.reply("Hello Komada!"); 17 | * @example 18 | * exports.run = async (client, msg) => { 19 | * const message = await msg.channel.send("Hello!"); 20 | * return message.edit("Hello from Komada!"); 21 | * } 22 | * @example 23 | * exports.run = async function run(client, msg) { 24 | * const message = await msg.channel.send("Hello!"); 25 | * return message.edit("Hello from Komada!"); 26 | * } 27 | * @return {Promise} 28 | */ 29 | exports.run = async (client, msg, ...args) => ({}); // eslint-disable-line 30 | 31 | /** 32 | * An object containing help information that will help identify and use a command. 33 | * @typedef {Object} Help 34 | * @property {String} name The name of the command 35 | * @property {String} description The description displayed in the help 36 | * @property {String} usage A usage string that denotes how the command should be used. 37 | * @property {String} usageDelim A character(s) to split the message content by and determine arguments. 38 | */ 39 | 40 | /** 41 | * The help object used throughout komada 42 | * @type {Help} 43 | * @example 44 | * exports.help = { 45 | name: "ping", 46 | description: "Ping/Pong command. I wonder what this does? /sarcasm", 47 | usage: "", 48 | usageDelim: "", 49 | }; 50 | */ 51 | exports.help = {}; 52 | 53 | /** 54 | * An Object containing configuration values that will configure a command. 55 | * @typedef {Object} Conf 56 | * @property {Boolean} enabled Whether or not this command should be enabled for use. 57 | * @property {Array} runIn What type of text channels this command should run in. 58 | * @property {Array} aliases An array of names that will also trigger this command. 59 | * @property {Number} permLevel What permission level this command should be limited to. 60 | * @property {Array} botPerms What permissions the bot must have to run this command. 61 | * @property {Array} requiredFuncs What functions are required in the bot to run this command. 62 | * @property {Array} requiredSettings What settings are required in the default schema to run this command. 63 | */ 64 | 65 | 66 | /** 67 | * An object that configures the command. 68 | * @type {Conf} 69 | * @example 70 | * exports.conf = { 71 | enabled: true, 72 | runIn: ["text", "dm", "group"], 73 | aliases: [], 74 | permLevel: 0, 75 | botPerms: [], 76 | requiredFuncs: [], 77 | requiredSettings: [], 78 | }; 79 | */ 80 | exports.conf = {}; 81 | -------------------------------------------------------------------------------- /src/classes/settingResolver.js: -------------------------------------------------------------------------------- 1 | const Resolver = require("./Resolver"); 2 | /* eslint-disable class-methods-use-this */ 3 | 4 | /** 5 | * SettingResolver class for SettingGateway argument parsing. 6 | * @class SettingResolver 7 | * @extends {Resolver} 8 | */ 9 | class SettingResolver extends Resolver { 10 | 11 | async user(data) { 12 | const result = await super.user(data); 13 | if (!result) throw "This key expects a User Object or ID."; 14 | return result; 15 | } 16 | 17 | async channel(data) { 18 | const result = await super.channel(data); 19 | if (!result) throw "This key expects a Channel Object or ID."; 20 | return result; 21 | } 22 | 23 | async textchannel(data) { 24 | const result = await this.channel(data); 25 | if (result.type !== "text") throw "This key expects a TextChannel Object or ID."; 26 | return result; 27 | } 28 | 29 | async voicechannel(data) { 30 | const result = await this.channel(data); 31 | if (result.type !== "voice") throw "This key expects a VoiceChannel Object or ID."; 32 | return result; 33 | } 34 | 35 | async guild(data) { 36 | const result = await super.guild(data); 37 | if (!result) throw "This key expects a Guild ID."; 38 | return result; 39 | } 40 | 41 | async role(data, guild) { 42 | const result = await super.role(data, guild) || guild.roles.find("name", data); 43 | if (!result) throw "This key expects a Role Object or ID."; 44 | return result; 45 | } 46 | 47 | async boolean(data) { 48 | const result = await super.boolean(data); 49 | if (typeof result !== "boolean") throw "This key expects a Boolean."; 50 | return result; 51 | } 52 | 53 | async string(data, guild, { min, max }) { 54 | const result = await super.string(data); 55 | SettingResolver.maxOrMin(result.length, min, max).catch((e) => { throw `The string length must be ${e} characters.`; }); 56 | return result; 57 | } 58 | 59 | async integer(data, guild, { min, max }) { 60 | const result = await super.integer(data); 61 | if (!result) throw "This key expects an Integer value."; 62 | SettingResolver.maxOrMin(result, min, max).catch((e) => { throw `The integer value must be ${e}.`; }); 63 | return result; 64 | } 65 | 66 | async float(data, guild, { min, max }) { 67 | const result = await super.float(data); 68 | if (!result) throw "This key expects a Float value."; 69 | SettingResolver.maxOrMin(result, min, max).catch((e) => { throw `The float value must be ${e}.`; }); 70 | return result; 71 | } 72 | 73 | async url(data) { 74 | const result = await super.url(data); 75 | if (!result) throw "This key expects an URL (Uniform Resource Locator)."; 76 | return result; 77 | } 78 | 79 | async command(data) { 80 | const command = this.client.commands.get(data.toLowerCase()) || this.client.commands.get(this.client.aliases.get(data.toLowerCase())); 81 | if (!command) throw "This key expects a Command."; 82 | return command.help.name; 83 | } 84 | 85 | /** 86 | * Check if the input is valid with min and/or max values. 87 | * @static 88 | * @param {any} value The value to check. 89 | * @param {?number} min Min value. 90 | * @param {?number} max Max value. 91 | * @returns {?boolean} 92 | */ 93 | static async maxOrMin(value, min, max) { 94 | if (min && max) { 95 | if (value >= min && value <= max) return true; 96 | if (min === max) throw `exactly ${min}`; 97 | throw `between ${min} and ${max}`; 98 | } else if (min) { 99 | if (value >= min) return true; 100 | throw `longer than ${min}`; 101 | } else if (max) { 102 | if (value <= max) return true; 103 | throw `shorter than ${max}`; 104 | } 105 | return null; 106 | } 107 | 108 | } 109 | 110 | module.exports = SettingResolver; 111 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Komada Framework Documentation 2 | 3 | [![Discord](https://discordapp.com/api/guilds/339942739275677727/embed.png)](https://discord.gg/FpEFSyY) 4 | [![npm](https://img.shields.io/npm/v/komada.svg?maxAge=3600)](https://www.npmjs.com/package/komada) 5 | [![npm](https://img.shields.io/npm/dt/komada.svg?maxAge=3600)](https://www.npmjs.com/package/komada) 6 | [![Greenkeeper badge](https://badges.greenkeeper.io/dirigeants/komada.svg)](https://greenkeeper.io/) 7 | [![Build Status](https://travis-ci.org/dirigeants/komada.svg?branch=master)](https://travis-ci.org/dirigeants/komada) 8 | [![David](https://img.shields.io/david/dirigeants/komada.svg?maxAge=3600)](https://david-dm.org/dirigeants/komada) 9 | [![Codacy Badge](https://api.codacy.com/project/badge/Grade/b78090f6d2614660ac58328645a2616d)](https://www.codacy.com/app/dirigeants/komada_repo?utm_source=github.com&utm_medium=referral&utm_content=dirigeants/komada&utm_campaign=Badge_Grade) 10 | 11 | Komada is a modular framework for bots built on top of [Discord.js](https://github.com/discordjs/discord.js). It offers an extremely easy installation, downloadable commands, and a framework to build your own commands, modules, and functions. 12 | 13 | ## What's with the name? 14 | 15 | Komada is the Croatian word for "pieces", such as puzzle pieces. As usual to find my software name I just shove english words in a translator and see what comes out. But why "pieces"? Because Komada is modular, meaning each "piece" is a standalone part that can be easily replaced, enhanced, reloaded, removed. 16 | 17 | ## Installing Komada 18 | 19 | Time to take the plunge! Komada is on NPM and can be easily installed. 20 | 21 | > I assume you know how to open a command prompt in a folder where you want to install this. Please don't prove me wrong. 22 | 23 | ``` 24 | npm install komada discordjs/discord.js 25 | ``` 26 | 27 | Create a file called `app.js` (or whatever you prefer) which will initiate and configure Komada. 28 | 29 | ```js 30 | const komada = require("komada"); 31 | 32 | const client = new komada.Client({ 33 | ownerID : "your-user-id", 34 | prefix: "+", 35 | clientOptions: { 36 | fetchAllMembers: false, 37 | }, 38 | cmdLogging: true, 39 | }); 40 | 41 | client.login("your-bot-token"); 42 | ``` 43 | 44 | ### Configuration Options 45 | 46 | - **botToken**: The MFA token for your bot. To get this, please see [This discord.js Getting Started Guide](https://anidiotsguide.gitbooks.io/discord-js-bot-guide/getting-started/the-long-version.html), which explains how to create the bot and get the token. 47 | - **ownerID**: The User ID of the bot owner - you. This gives you the highest possible access to the bot's default commands, including eval! To obtain it, enable Developer Mode in Discord, right-click your name and do "Copy ID". 48 | 49 | > As of Komada 0.20.4, If you do not set this option, the ownerID will default the creator of the application on the discord developer website. This only works if your bot is not a self/user bot. 50 | 51 | - **prefix**: The default prefix when the bot first boots up. This option becomes useless after first boot, since the prefix is written to the default configuration system. 52 | - **clientOptions**: These are passed directly to the discord.js library. They are optional. For more information on which options are available, see [ClientOptions in the discord.js docs](https://discord.js.org/#/docs/main/stable/typedef/ClientOptions). 53 | - **permStructure**: It allows you to configure the permission levels from Komada, with a range of 0-10. You can also use `Komada.PermLevels` constructor. 54 | - **cmdLogging**: If set to true, it console.logs EVERY *successful* command run, where, the user who ran it, and the time it took to process the command with a sexy color format. 55 | 56 | > Komada automatically detects selfbot mode, and takes appropriate precautions, such as not responding to anyone but yourself. 57 | 58 | ## Running the bot 59 | 60 | Then, run the following in your folder: 61 | 62 | ``` 63 | node app.js 64 | ``` 65 | 66 | > **Requirements**: This version of Komada requires Node 8.1.0 or higher to run. Depends on Discord.js v12.0.0-dev or higher (the appropriate version is automatically installed). 67 | 68 | ## Documentation 69 | 70 | Please check [Komada Docs](https://dirigeants.github.io/komada/) to learn more about Komada Framework and its usage. Any doubts? Ask us [here](https://discord.gg/FpEFSyY). 71 | -------------------------------------------------------------------------------- /src/classes/settingsCache.js: -------------------------------------------------------------------------------- 1 | const Settings = require("./settings/Settings"); 2 | const Resolver = require("./settingResolver"); 3 | 4 | /** 5 | * SettingGateway's driver to make new instances of it, with the purpose to handle different databases simultaneously. 6 | * @class SettingsCache 7 | */ 8 | class SettingsCache { 9 | 10 | /** 11 | * @param {KomadaClient} client The Komada client 12 | */ 13 | constructor(client) { 14 | /** 15 | * The client this SettingsCache was created with. 16 | * @name SettingsCache#client 17 | * @type {KomadaClient} 18 | * @readonly 19 | */ 20 | Object.defineProperty(this, "client", { value: client }); 21 | 22 | this.resolver = new Resolver(client); 23 | 24 | /** 25 | * The SettingGateway instance created to handle guild settings. 26 | * @name SettingsCache#guilds 27 | * @type {SettingGateway} 28 | */ 29 | this.guilds = new Settings(client, "guilds", this.validate.bind(null, this.resolver), this.defaultDataSchema, this.resolver); 30 | } 31 | 32 | /** 33 | * Add a new instance of SettingGateway, with its own validateFunction and schema. 34 | * @param {string} name The name for the new instance. 35 | * @param {Function} validateFunction The function that validates the input. 36 | * @param {Object} [schema={}] The schema. 37 | * @returns {SettingGateway} 38 | * @example 39 | * // Add a new SettingGateway instance, called 'users', which input takes users, and stores a quote which is a string between 2 and 140 characters. 40 | * const validate = async function(resolver, user) { 41 | * const result = await resolver.user(user); 42 | * if (!result) throw "The parameter expects either a User ID or a User Object."; 43 | * return result; 44 | * }; 45 | * const schema = { 46 | * quote: { 47 | * type: "String", 48 | * default: null, 49 | * array: false, 50 | * min: 2, 51 | * max: 140, 52 | * }, 53 | * }; 54 | * SettingsCache.add("users", validate, schema); 55 | */ 56 | async add(name, validateFunction, schema = {}) { 57 | if (!name || typeof name !== "string") throw "You must pass a name for your new gateway and it must be a string."; 58 | if (name in this) throw "There is already a Gateway with that name."; 59 | if (typeof validateFunction !== "function") throw "You must pass a validate function."; 60 | validateFunction = validateFunction.bind(null, this.resolver); 61 | if (schema.constructor.name !== "Object") throw "Schema must be a valid object or left undefined for an empty object."; 62 | this[name] = new Settings(this, name, validateFunction, schema, this.resolver); 63 | return this[name]; 64 | } 65 | 66 | /** 67 | * The validator function Komada uses for guild settings. 68 | * @param {SettingResolver} resolver The resolver instance this SettingGateway uses to parse the data. 69 | * @param {(Object|string)} guild The data to validate. 70 | * @returns {Promise} 71 | */ 72 | async validate(resolver, guild) { // eslint-disable-line class-methods-use-this 73 | const result = await resolver.guild(guild); 74 | if (!result) throw "The parameter expects either a Guild ID or a Guild Object."; 75 | return result; 76 | } 77 | 78 | /** 79 | * The data schema Komada uses for guild settings. 80 | * @readonly 81 | * @returns {Object} 82 | */ 83 | get defaultDataSchema() { 84 | return { 85 | prefix: { 86 | type: "String", 87 | default: this.client.config.prefix, 88 | array: this.client.config.prefix.constructor.name === "Array", 89 | sql: `TEXT NOT NULL DEFAULT '${this.client.config.prefix.constructor.name === "Array" ? JSON.stringify(this.client.config.prefix) : this.client.config.prefix}'`, 90 | }, 91 | modRole: { 92 | type: "Role", 93 | default: null, 94 | array: false, 95 | sql: "TEXT", 96 | }, 97 | adminRole: { 98 | type: "Role", 99 | default: null, 100 | array: false, 101 | sql: "TEXT", 102 | }, 103 | disabledCommands: { 104 | type: "Command", 105 | default: [], 106 | array: true, 107 | sql: "TEXT DEFAULT '[]'", 108 | }, 109 | }; 110 | } 111 | 112 | } 113 | 114 | module.exports = SettingsCache; 115 | -------------------------------------------------------------------------------- /src/classes/settings/Schema.js: -------------------------------------------------------------------------------- 1 | const Resolver = require("../settingResolver"); 2 | 3 | /* eslint-disable class-methods-use-this, no-underscore-dangle, no-restricted-syntax */ 4 | const types = Object.getOwnPropertyNames(Resolver.prototype).slice(1); 5 | 6 | /** 7 | * Schema constructor that creates schemas for use in Komada. 8 | * @type {Object} 9 | */ 10 | class Schema { 11 | 12 | /** 13 | * Constructs our schema that we can add into. 14 | * @param {Object} [schema] An object containing key:value pairs 15 | */ 16 | constructor(schema) { 17 | if (schema) { 18 | for (const [key, value] of Object.entries(schema)) { 19 | this[key] = value; 20 | } 21 | } 22 | } 23 | 24 | /** 25 | * All of the valid key types in Komada at the present moment. 26 | * @typedef {string} Types 27 | * @property {string} user 28 | * @property {string} channel 29 | * @property {string} textChannel 30 | * @property {string} voiceChannel 31 | * @property {string} guild 32 | * @property {string} role 33 | * @property {string} boolean 34 | * @property {string} string 35 | * @property {string} integer 36 | * @property {string} float 37 | * @property {string} url 38 | * @property {string} command 39 | * @memberof Schema 40 | */ 41 | 42 | /** 43 | * @typedef {object} Options 44 | * @property {Schema.Types} type The type of key you want to add 45 | * @property {?} value The default value to set the key to. 46 | * @property {number} min The minimum number, used for determining string length and amount for number types. 47 | * @property {number} max The maximum number, used for determining string length and amount for number types. 48 | * @property {boolean} Array A boolean indicating whether or not this key should be created and stored as an array. 49 | * @memberof Schema 50 | */ 51 | 52 | /** 53 | * Add function that adds a new key into the schema. Only requires that you pass a name and type for the key. 54 | * @param {string} name The name of the key you want to add. 55 | * @param {Schema.Options} [options] An object containing extra options for a key. 56 | * @returns {Schema} Returns the schema you are creating so you can chain add calls. 57 | */ 58 | add(name, { type, value, min, max, array } = {}) { 59 | [name, value, min, max, array, type] = this._validateInput(name, value, min, max, array, type.toLowerCase()); 60 | if (["float", "integer", "string"].includes(type)) { 61 | this[name] = { type, default: value, min, max, array }; 62 | return this; 63 | } 64 | this[name] = { type, default: value, array }; 65 | return this; 66 | } 67 | 68 | /** 69 | * Returns an object containing the keys mapped to their default values. 70 | * @return {Object} 71 | */ 72 | get defaults() { 73 | const defaults = {}; 74 | for (const key of Object.keys(this)) { 75 | defaults[key] = this[key].default; 76 | } 77 | return defaults; 78 | } 79 | 80 | _validateInput(name, value, min, max, array, type) { 81 | if (!name) throw "You must provide a name for this new key."; 82 | if (!types.includes(type)) throw `Invalid type provided. Valid types are: ${types.join(", ")}`; 83 | if (array) { 84 | if (array.constructor.name !== "Boolean") throw "The array parameter must be a boolean."; 85 | value = value || []; 86 | if (!Array.isArray(value)) throw "The default value must be an array if you set array to true."; 87 | } else { 88 | array = false; 89 | value = value || null; 90 | } 91 | if (min || max) [min, max] = this._minOrMax(value, min, max); 92 | return [name, value, min, max, array, type]; 93 | } 94 | 95 | _minOrMax(value, min, max) { 96 | if (!value) return [min, max]; 97 | if (!Number.isNaN(min) && !Number.isNaN(max)) { 98 | if (value >= min && value <= max) return [min, max]; 99 | if (min === max) throw `Value must be exactly ${min}`; 100 | throw `Value must be between ${min} and ${max}`; 101 | } else if (!Number.isNaN(min)) { 102 | if (value >= min) return [min, null]; 103 | throw `Value must be longer than ${min}`; 104 | } else if (!Number.isNaN(max)) { 105 | if (value <= max) return [null, max]; 106 | throw `Value must be shorter than ${max}`; 107 | } 108 | return [null, null]; 109 | } 110 | 111 | } 112 | 113 | module.exports = Schema; 114 | -------------------------------------------------------------------------------- /src/classes/console/Colors.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-unused-expressions, no-bitwise */ 2 | 3 | class Colors { 4 | 5 | constructor() { 6 | this.CLOSE = { 7 | normal: 0, 8 | bold: 22, 9 | dim: 22, 10 | italic: 23, 11 | underline: 24, 12 | inverse: 27, 13 | hidden: 28, 14 | strikethrough: 29, 15 | text: 39, 16 | background: 49, 17 | }; 18 | 19 | this.STYLES = { 20 | normal: 0, 21 | bold: 1, 22 | dim: 2, 23 | italic: 3, 24 | underline: 4, 25 | inverse: 7, 26 | hidden: 8, 27 | strikethrough: 9, 28 | }; 29 | 30 | this.TEXTS = { 31 | black: 30, 32 | red: 31, 33 | green: 32, 34 | yellow: 33, 35 | blue: 34, 36 | magenta: 35, 37 | cyan: 36, 38 | lightgray: 37, 39 | lightgrey: 37, 40 | gray: 90, 41 | grey: 90, 42 | lightred: 91, 43 | lightgreen: 92, 44 | lightyellow: 93, 45 | lightblue: 94, 46 | lightmagenta: 95, 47 | lightcyan: 96, 48 | white: 97, 49 | }; 50 | 51 | this.BACKGROUNDS = { 52 | black: 40, 53 | red: 41, 54 | green: 42, 55 | yellow: 43, 56 | blue: 44, 57 | magenta: 45, 58 | cyan: 46, 59 | gray: 47, 60 | grey: 47, 61 | lightgray: 100, 62 | lightgrey: 100, 63 | lightred: 101, 64 | lightgreen: 102, 65 | lightyellow: 103, 66 | lightblue: 104, 67 | lightmagenta: 105, 68 | lightcyan: 106, 69 | white: 107, 70 | }; 71 | } 72 | 73 | static hexToRGB(hex) { 74 | let string = hex[0]; 75 | if (string.length === 3) string = string.split("").map(char => char + char).join(""); 76 | const integer = parseInt(string, 16); 77 | return [(integer >> 16) & 0xFF, (integer >> 8) & 0xFF, integer & 0xFF]; 78 | } 79 | 80 | static hslToRGB([h, s, l]) { 81 | if (s === "0%") return [l, l, l]; 82 | const q = l < 0.5 ? l * (1 + s) : l + s - l * s; // eslint-disable-line 83 | const p = 2 * l - q; // eslint-disable-line 84 | return [Colors.hueToRGB(p, q, h + (1 / 3)), Colors.hueToRGB(p, q, h), Colors.hueToRGB(p, q, h - (1 / 3))]; 85 | } 86 | 87 | static hueToRGB(p, q, t) { 88 | if (t < 0) t += 1; 89 | if (t > 1) t -= 1; 90 | if (t < 1 / 6) return p + (q - p) * 6 * t; // eslint-disable-line 91 | if (t < 1 / 2) return q; 92 | if (t < 2 / 3) return p + (q - p) * (2 / 3 - t) * 6; // eslint-disable-line 93 | return p; 94 | } 95 | 96 | static formatArray(array) { 97 | if (array[2].endsWith("%") && array[3].endsWith("%")) { 98 | return Colors.hslToRGB(array); 99 | } 100 | return `38;2;${array[0]};${array[1]};${array[2]}`; 101 | } 102 | 103 | 104 | format(string, { style, background, text } = {}) { 105 | const opening = []; 106 | const closing = []; 107 | const backgroundMatch = background ? background.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i) : null; 108 | const textMatch = text ? text.toString(16).match(/[a-f0-9]{6}|[a-f0-9]{3}/i) : null; 109 | if (backgroundMatch) background = Colors.hexToRGB(backgroundMatch); 110 | if (textMatch) text = Colors.hexToRGB(textMatch); 111 | if (style) { 112 | if (Array.isArray(style)) style.forEach(sty => (sty in this.STYLES ? opening.push(`${this.STYLES[sty.toLowerCase()]}`) && closing.push(`${this.CLOSE[sty.toLowerCase()]}`) : null)); 113 | else if (style in this.STYLES) opening.push(`${this.STYLES[style.toLowerCase()]}`) && closing.push(`${this.CLOSE[style.toLowerCase()]}`); 114 | } 115 | if (background) { 116 | if (Number.isInteger(background)) opening.push(`48;5;${background}`) && closing.push(`${this.CLOSE.background}`); 117 | if (Array.isArray(background)) opening.push(Colors.formatArray(background)) && closing.push(`\u001B[${this.CLOSE.background}`); 118 | else if (background.toString().toLowerCase() in this.BACKGROUNDS) opening.push(`${this.BACKGROUNDS[background.toLowerCase()]}`) && closing.push(`${this.CLOSE.background}`); 119 | } 120 | if (text) { 121 | if (Number.isInteger(text)) opening.push(`38;5;${text}`) && closing.push(`${this.CLOSE.text}`); 122 | if (Array.isArray(text)) opening.push(Colors.formatArray(text)) && closing.push(`${this.CLOSE.text}`); 123 | else if (text.toString().toLowerCase() in this.TEXTS) opening.push(`${this.TEXTS[text.toLowerCase()]}`) && closing.push(`${this.CLOSE.text}`); 124 | } 125 | return `\u001B[${opening.join(";")}m${string}\u001B[${closing.join(";")}m`; 126 | } 127 | 128 | } 129 | 130 | module.exports = new Colors(); 131 | -------------------------------------------------------------------------------- /src/classes/sql.js: -------------------------------------------------------------------------------- 1 | const tuplify = s => [s.split(" ")[0], s.split(" ").slice(1).join(" ")]; 2 | const DefaultDataTypes = { 3 | String: "TEXT", 4 | Integer: "INTEGER", 5 | Float: "INTEGER", 6 | AutoID: "INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE", 7 | Timestamp: "DATETIME", 8 | AutoTS: "DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL", 9 | }; 10 | 11 | /* eslint-disable no-restricted-syntax */ 12 | /** 13 | * SQL driver for compatibility with SQL providers. Do NOT use this directly. 14 | * @class SQL 15 | */ 16 | class SQL { 17 | 18 | /** 19 | * Creates an instance of SQL. 20 | * @param {KomadaClient} client The Komada Client. 21 | * @param {SettingGateway} gateway The SettingGateway instance which initialized this instance. 22 | */ 23 | constructor(client, gateway) { 24 | /** 25 | * The client this SettingsCache was created with. 26 | * @name SQL#client 27 | * @type {KomadaClient} 28 | * @readonly 29 | */ 30 | Object.defineProperty(this, "client", { value: client }); 31 | 32 | /** 33 | * The gateway which initiated this instance. 34 | * @name SQL#gateway 35 | * @type {SettingGateway} 36 | * @readonly 37 | */ 38 | Object.defineProperty(this, "gateway", { value: gateway }); 39 | } 40 | 41 | /** 42 | * Generate an automatic SQL schema for a single row. 43 | * @param {Object} value The Schema object. 44 | * @returns {string} 45 | */ 46 | buildSingleSQLSchema(value) { 47 | const selectType = schemaKey => this.constants[schemaKey] || "TEXT"; 48 | const type = value.sql || value.default ? ` DEFAULT ${this.sanitizer(value.default)}` : ""; 49 | return `${selectType(value.type)}${type}`; 50 | } 51 | 52 | /** 53 | * Generate an automatic SQL schema for all rows. 54 | * @param {any} schema The Schema Object. 55 | * @returns {string[]} 56 | */ 57 | buildSQLSchema(schema) { 58 | const output = ["id TEXT NOT NULL UNIQUE"]; 59 | for (const [key, value] of Object.entries(schema)) { 60 | output.push(`${key} ${this.buildSingleSQLSchema(key, value)}`); 61 | } 62 | return output; 63 | } 64 | 65 | /** 66 | * Init the deserialization keys for SQL providers. 67 | */ 68 | initDeserialize() { 69 | this.deserializeKeys = []; 70 | for (const [key, value] of Object.entries(this.schema)) { 71 | if (value.array === true) this.deserializeKeys.push(key); 72 | } 73 | } 74 | 75 | /** 76 | * Deserialize stringified objects. 77 | * @param {Object} data The GuildSettings object. 78 | */ 79 | deserializer(data) { 80 | const deserialize = this.deserializeKeys; 81 | for (let i = 0; i < deserialize.length; i++) data[deserialize[i]] = JSON.parse(data[deserialize[i]]); 82 | } 83 | 84 | /** 85 | * Create/Remove columns from a SQL database, by the current Schema. 86 | * @param {Object} schema The Schema object. 87 | * @param {Object} defaults The Schema object. 88 | * @param {string} key The key which is updated. 89 | * @returns {Promise} 90 | */ 91 | async updateColumns(schema, defaults, key) { 92 | if (!this.provider.updateColumns) { 93 | this.client.emit("log", "This SQL Provider does not seem to have a updateColumns exports. Force action cancelled.", "error"); 94 | return false; 95 | } 96 | const newSQLSchema = this.buildSQLSchema(schema).map(tuplify); 97 | const keys = Object.keys(defaults); 98 | if (!keys.includes("id")) keys.push("id"); 99 | const columns = keys.filter(k => k !== key); 100 | await this.provider.updateColumns(this.gateway.type, columns, newSQLSchema); 101 | this.initDeserialize(); 102 | 103 | return true; 104 | } 105 | 106 | /** 107 | * The constants this instance will use to build the SQL schemas. 108 | * @name SQL#constants 109 | * @type {Object} 110 | * @readonly 111 | */ 112 | get constants() { 113 | return this.provider.CONSTANTS || DefaultDataTypes; 114 | } 115 | 116 | /** 117 | * Sanitize and prepare the strings for SQL input. 118 | * @name SQL#sanitizer 119 | * @type {Function} 120 | * @readonly 121 | */ 122 | get sanitizer() { 123 | return this.provider.sanitize || (value => `'${value}'`); 124 | } 125 | 126 | /** 127 | * Shortcut for Schema. 128 | * @name SQL#schema 129 | * @type {Object} 130 | * @readonly 131 | */ 132 | get schema() { 133 | return this.gateway.schema; 134 | } 135 | 136 | /** 137 | * Shortcut for Schema 138 | * @name SQL#defaults 139 | * @type {Object} 140 | * @readonly 141 | */ 142 | get defaults() { 143 | return this.gateway.defaults; 144 | } 145 | 146 | /** 147 | * The provider this SettingGateway instance uses for the persistent data operations. 148 | * @name SQL#provider 149 | * @type {Resolver} 150 | * @readonly 151 | */ 152 | get provider() { 153 | return this.gateway.provider; 154 | } 155 | 156 | } 157 | 158 | module.exports = SQL; 159 | -------------------------------------------------------------------------------- /src/functions/mergeConfig.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-restricted-syntax */ 2 | 3 | const PermLevels = require("../classes/PermissionLevels"); 4 | 5 | const defaultPermStructure = new PermLevels() 6 | .add(0, false, () => true) 7 | .add(2, false, (client, msg) => { 8 | if (!msg.guild || !msg.guild.settings.modRole) return false; 9 | const modRole = msg.guild.roles.get(msg.guild.settings.modRole); 10 | return modRole && msg.member.roles.has(modRole.id); 11 | }) 12 | .add(3, false, (client, msg) => { 13 | if (!msg.guild || !msg.guild.settings.adminRole) return false; 14 | const adminRole = msg.guild.roles.get(msg.guild.settings.adminRole); 15 | return adminRole && msg.member.roles.has(adminRole.id); 16 | }) 17 | .add(4, false, (client, msg) => msg.guild && msg.author.id === msg.guild.owner.id) 18 | .add(9, true, (client, msg) => msg.author.id === client.config.ownerID) 19 | .add(10, false, (client, msg) => msg.author.id === client.config.ownerID); 20 | 21 | 22 | module.exports = (options) => { 23 | for (const key in this.DEFAULT_OPTIONS) { 24 | if (!(key in options)) options[key] = this.DEFAULT_OPTIONS[key]; 25 | if (["provider", "disabled", "console"].includes(key)) { 26 | for (const property in this.DEFAULT_OPTIONS[key]) { 27 | if (!(property in options[key])) options[key][property] = this.DEFAULT_OPTIONS[key][property]; 28 | } 29 | } 30 | } 31 | this.validate(options); 32 | return options; 33 | }; 34 | 35 | exports.DEFAULT_OPTIONS = { 36 | prefix: "?", 37 | ownerID: null, 38 | disabled: { 39 | commands: [], 40 | events: [], 41 | functions: [], 42 | inhibitors: [], 43 | finalizers: [], 44 | monitors: [], 45 | providers: [], 46 | extendables: [], 47 | }, 48 | permStructure: defaultPermStructure, 49 | selfbot: false, 50 | readyMessage: client => `Successfully initialized. Ready to serve ${client.guilds.size} guilds.`, 51 | commandMessageLifetime: 1800, 52 | commandMessageSweep: 900, 53 | cmdEditing: false, 54 | cmdPrompt: false, 55 | provider: { 56 | engine: "json", 57 | cache: "js", 58 | }, 59 | console: { 60 | useColors: true, 61 | colors: {}, 62 | timestamps: true, 63 | stdout: process.stdout, 64 | stderr: process.stderr, 65 | }, 66 | }; 67 | 68 | exports.validate = (options) => { 69 | const pieces = Object.keys(this.DEFAULT_OPTIONS.disabled); 70 | if ("prefix" in options && typeof options.prefix !== "string") throw new TypeError("Prefix must be a string value."); 71 | if ("ownerID" in options && typeof options.ownerID !== "string" && options.ownerID !== null) throw new TypeError("OwnerID must be a string (user id) if provided."); 72 | if ("disabled" in options) { 73 | if (typeof options.disabled !== "object" || Array.isArray(options.disabled)) throw new TypeError("Disabled must be a valid object"); 74 | for (const key of Object.keys(options.disabled)) { // eslint-disable-line 75 | if (!pieces.includes(key)) throw new Error("Invalid piece name in the disabled array"); 76 | if (!Array.isArray(options.disabled[key])) throw new TypeError(`${key} must be an array.`); 77 | } 78 | } 79 | if ("permStructure" in options) { 80 | if (options.permStructure.constructor.name !== "PermissionLevels" && !Array.isArray(options.permStructure)) throw new TypeError("PermStructure must be a valid array with 11 entries, or a instance of Komada.PermLevels"); 81 | } 82 | if ("selfbot" in options && typeof options.selfbot !== "boolean") throw new TypeError("Selfbot must be true or false."); 83 | if ("readyMessage" in options && typeof options.readyMessage !== "function") throw new TypeError("ReadyMessage must be a function."); 84 | if ("commandMessageLifetime" in options && typeof options.commandMessageLifetime !== "number") throw new TypeError("CommandMessageLifetime must be a number."); 85 | if ("commandMessageSweep" in options && typeof options.commandMessageSweep !== "number") throw new TypeError("CommandMessageSweep must be a number."); 86 | if ("cmdEditing" in options && typeof options.cmdEditing !== "boolean") throw new TypeError("CmdEditing must be true or false."); 87 | if ("cmdPrompt" in options && typeof options.cmdPrompt !== "boolean") throw new TypeError("CmdPrompt must be true or false."); 88 | if ("provider" in options) { 89 | if ("engine" in options.provider && typeof options.provider.engine !== "string") throw new TypeError("Engine must be a string."); 90 | if ("cache" in options.provider && typeof options.provider.cache !== "string") throw new TypeError("Cache must be a string."); 91 | } 92 | if ("console" in options) { 93 | if ("timestamps" in options.console && !(typeof options.console.timestamps === "boolean" || typeof options.console.timestamps === "string")) throw new TypeError("Timestamps must be true or false"); 94 | if ("colors" in options.console && typeof options.console.colors !== "object") throw new TypeError("Colors must be an object with message and time objects"); 95 | if ("useColors" in options.console && typeof options.console.useColors !== "boolean") throw new TypeError("Colors must be true or false."); 96 | } 97 | }; 98 | -------------------------------------------------------------------------------- /src/index.js: -------------------------------------------------------------------------------- 1 | module.exports = { 2 | ArgResolver: require("./classes/argResolver"), 3 | Client: require("./classes/client"), 4 | Colors: require("./classes/console/Colors"), 5 | Console: require("./classes/console/Console"), 6 | Duration: require("./util/Duration"), 7 | KomadaMessage: require("./structures/KomadaMessage"), 8 | Loader: require("./classes/loader"), 9 | ParsedUsage: require("./classes/parsedUsage"), 10 | PermissionLevels: require("./classes/PermissionLevels"), 11 | Resolver: require("./classes/Resolver"), 12 | Schema: require("./classes/settings/Schema"), 13 | Stopwatch: require("./util/Stopwatch"), 14 | Gateway: require("./classes/settings/Gateway"), 15 | SettingResolver: require("./classes/settingResolver"), 16 | SettingsCache: require("./classes/settingsCache"), 17 | SQL: require("./classes/sql"), 18 | version: require("../package.json").version, 19 | }; 20 | 21 | 22 | /** 23 | * @name Discord.js 24 | */ 25 | 26 | /** 27 | * @external Collection 28 | * @memberof Discord.js 29 | * @see {@link https://discord.js.org/#/docs/main/master/class/Collection} 30 | */ 31 | 32 | /** 33 | * @external Channel 34 | * @memberof Discord.js 35 | * @see {@link https://discord.js.org/#/docs/main/master/class/Channel} 36 | */ 37 | 38 | /** 39 | * @external Client 40 | * @memberof Discord.js 41 | * @see {@link https://discord.js.org/#/docs/main/master/class/Client} 42 | */ 43 | 44 | /** 45 | * @external DiscordJSConfig 46 | * @memberof Discord.js 47 | * @see {@link https://discord.js.org/#/docs/main/master/typedef/ClientOptions} 48 | */ 49 | 50 | /** 51 | * @external DMChannel 52 | * @memberof Discord.js 53 | * @see {@link https://discord.js.org/#/docs/main/master/class/DMChannel} 54 | */ 55 | 56 | /** 57 | * @external GroupDMChannel 58 | * @memberof Discord.js 59 | * @see {@link https://discord.js.org/#/docs/main/master/class/GroupDMChannel} 60 | */ 61 | 62 | /** 63 | * @external Guild 64 | * @memberof Discord.js 65 | * @see {@link https://discord.js.org/#/docs/main/master/class/Guild} 66 | */ 67 | 68 | /** 69 | * @external GuildMember 70 | * @memberof Discord.js 71 | * @see {@link https://discord.js.org/#/docs/main/master/class/GuildMember} 72 | */ 73 | 74 | /** 75 | * @external GuildResolvable 76 | * @memberof Discord.js 77 | * @see {@link https://discord.js.org/#/docs/main/master/typedef/GuildResolvable} 78 | */ 79 | 80 | /** 81 | * @external Message 82 | * @memberof Discord.js 83 | * @see {@link https://discord.js.org/#/docs/main/master/class/Message} 84 | */ 85 | 86 | /** 87 | * @external MessageAttachment 88 | * @memberof Discord.js 89 | * @see {@link https://discord.js.org/#/docs/main/master/class/MessageAttachment} 90 | */ 91 | 92 | /** 93 | * @external MessageEmbed 94 | * @memberof Discord.js 95 | * @see {@link https://discord.js.org/#/docs/main/master/class/MessageEmbed} 96 | */ 97 | 98 | /** 99 | * @external MessageReaction 100 | * @memberof Discord.js 101 | * @see {@link https://discord.js.org/#/docs/main/master/class/MessageReaction} 102 | */ 103 | 104 | /** 105 | * @external MessageOptions 106 | * @memberof Discord.js 107 | * @see {@link https://discord.js.org/#/docs/main/master/typedef/MessageOptions} 108 | */ 109 | 110 | /** 111 | * @external Role 112 | * @memberof Discord.js 113 | * @see {@link https://discord.js.org/#/docs/main/master/class/Role} 114 | */ 115 | 116 | /** 117 | * @external StringResolvable 118 | * @memberof Discord.js 119 | * @see {@link https://discord.js.org/#/docs/main/master/typedef/StringResolvable} 120 | */ 121 | 122 | /** 123 | * @external TextChannel 124 | * @memberof Discord.js 125 | * @see {@link https://discord.js.org/#/docs/main/master/class/TextChannel} 126 | */ 127 | 128 | /** 129 | * @external VoiceChannel 130 | * @memberof Discord.js 131 | * @see {@link https://discord.js.org/#/docs/main/master/class/VoiceChannel} 132 | */ 133 | 134 | /** 135 | * @external User 136 | * @memberof Discord.js 137 | * @see {@link https://discord.js.org/#/docs/main/master/class/User} 138 | */ 139 | 140 | /** 141 | * @external UserResolvable 142 | * @memberof Discord.js 143 | * @see {@link https://discord.js.org/#/docs/main/master/class/UserResolvable} 144 | */ 145 | 146 | /** 147 | * @external Emoji 148 | * @memberof Discord.js 149 | * @see {@link https://discord.js.org/#/docs/main/master/class/Emoji} 150 | */ 151 | 152 | /** 153 | * @external ReactionEmoji 154 | * @memberof Discord.js 155 | * @see {@link https://discord.js.org/#/docs/main/master/class/ReactionEmoji} 156 | */ 157 | 158 | /** 159 | * @external Webhook 160 | * @memberof Discord.js 161 | * @see {@link https://discord.js.org/#/docs/main/master/class/Webhook} 162 | */ 163 | 164 | /** 165 | * @external MessageEmbed 166 | * @memberof Discord.js 167 | * @see {@link https://discord.js.org/#/docs/main/master/class/MessageEmbed} 168 | */ 169 | 170 | /** 171 | * @external ShardingManager 172 | * @memberof Discord.js 173 | * @see {@link https://discord.js.org/#/docs/main/master/class/ShardingManager} 174 | */ 175 | -------------------------------------------------------------------------------- /src/documentation/ProviderStructure.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Providers are special pieces that give you easy access to various data storage systems 3 | * using a consistent and predictable set of methods 4 | * @module Provider 5 | * @example The following structure is needed for SettingGateway compatibility. 6 | * exports.hasTable = (table) => { // code here }; 7 | * exports.createTable = (table) => { // code here }; 8 | * exports.getAll = (table) => { // code here }; 9 | * exports.get = (table, id) => { // code here }; 10 | * exports.create = (table, id, document) => { // code here }; 11 | * exports.delete = (table, id) => { // code here }; 12 | * exports.update = (table, id, document) => { // code here }; 13 | * exports.replace = (table, id, document) => { // code here }; 14 | * exports.conf = {}; 15 | */ 16 | 17 | /** 18 | * Checks if a table exists. 19 | * @param {string} table The name of the table you want to check. 20 | * @return {Promise} 21 | */ 22 | exports.hasTable = (table) => ({}); // eslint-disable-line 23 | 24 | /** 25 | * Create a new table. 26 | * @param {string} table The name for the new table. 27 | * @return {Promise<*>} 28 | */ 29 | exports.createTable = (table) => ({}); // eslint-disable-line 30 | 31 | /** 32 | * Get all entries from a table. 33 | * @param {string} table The name of the table to fetch from. 34 | * @return {Promise>} 35 | */ 36 | exports.getAll = (table) => ({}); // eslint-disable-line 37 | 38 | /** 39 | * Get an entry from a table. 40 | * @param {string} table The name of the table to fetch from. 41 | * @param {string} id The ID of the entry to get. 42 | * @return {Promise>} 43 | */ 44 | exports.get = (table, id) => ({}); // eslint-disable-line 45 | 46 | /** 47 | * Create a new entry into a table. 48 | * @param {string} table The name of the table to update. 49 | * @param {string} id The ID for the new entry. 50 | * @param {Object} document A JSON object. 51 | * @return {Promise<*>} 52 | */ 53 | exports.create = (table, id, document) => ({}); // eslint-disable-line 54 | 55 | /** 56 | * Delete an entry from a table. 57 | * @param {string} table The name of the table to update. 58 | * @param {string} id The ID of the entry to delete. 59 | * @return {Promise>} 60 | */ 61 | exports.delete = (table, id) => ({}); // eslint-disable-line 62 | 63 | /** 64 | * Update an entry from a table. 65 | * @param {string} table The name of the table to update. 66 | * @param {string} id The ID of the entry to update. 67 | * @param {Object} document A JSON object. 68 | * @return {Promise<*>} 69 | */ 70 | exports.update = (table, id, document) => ({}); // eslint-disable-line 71 | 72 | /** 73 | * Replace an entry from a table. 74 | * @param {string} table The name of the table to update. 75 | * @param {string} id The ID of the entry to update. 76 | * @param {Object} document The new JSON object for the document. 77 | * @return {Promise<*>} 78 | */ 79 | exports.replace = (table, id, document) => ({}); // eslint-disable-line 80 | 81 | /** 82 | * An object that configures the provider. 83 | * @type {Conf} 84 | * @example 85 | * exports.conf = { 86 | * enabled: true, 87 | * moduleName: "json", 88 | * priority: 0 89 | * }; 90 | */ 91 | exports.conf = {}; 92 | 93 | /** 94 | * Some providers are SQL, and due to the No-SQL environment that exists in SettingGateway, 95 | * they require extra methods/properties to work. All the previous methods are required to work. 96 | * @module ProviderSQL 97 | * @example SQL Compatibility 98 | * exports.updateColumns = (table, columns, schema) => { // code here }; 99 | * exports.serialize = (data) => { // code here }; 100 | * exports.sanitize = (string) => { // code here }; 101 | * exports.CONSTANTS = {}; 102 | */ 103 | 104 | /** 105 | * Update the columns from a table (All the data is provided by the SQL class). 106 | * @param {string} table The name of the table. 107 | * @param {string[]} columns Array of columns. 108 | * @param {array[]} schema Tuples of keys/values from the schema. 109 | * @returns {boolean} 110 | */ 111 | exports.updateColumns = (table, columns, schema) => ({}); // eslint-disable-line 112 | 113 | /** 114 | * Transform NoSQL queries into SQL. 115 | * @param {Object} data The object. 116 | * @returns {Object} 117 | */ 118 | exports.serialize = (data) => ({}); // eslint-disable-line 119 | 120 | /** 121 | * Sanitize strings to be storable into the SQL database. 122 | * @param {*} string An object or string. 123 | * @returns {string} 124 | */ 125 | exports.sanitize = (string) => ({}); // eslint-disable-line 126 | 127 | /** 128 | * An object that helps the SQL class creating compatible schemas for the provider. 129 | * @property {string} String The SQL compatible string datatype. 130 | * @property {string} Integer The SQL compatible integer datatype. 131 | * @property {string} Float The SQL compatible float datatype. 132 | * @example 133 | * exports.CONSTANTS = { 134 | * String: "TEXT", 135 | * Integer: "INTEGER", 136 | * Float: "INTEGER", 137 | * AutoID: "INTEGER PRIMARY KEY AUTOINCREMENT UNIQUE", 138 | * Timestamp: "DATETIME", 139 | * AutoTS: "DATETIME DEFAULT CURRENT_TIMESTAMP NOT NULL", 140 | * }; 141 | */ 142 | exports.CONSTANTS = {}; 143 | -------------------------------------------------------------------------------- /examples/UnderstandingUsageString.md: -------------------------------------------------------------------------------- 1 | ## Usage Structure 2 | 3 | `<>` required argument, `[]` optional argument `` 4 | 5 | - **Name** Mostly used for debugging message, unless the type is Literal in which it compares the argument to the name. 6 | - **Type** The type of variable you are expecting. 7 | - **Min, Max** Minimum or Maximum for a giving variable (works on strings in terms of length, and on all types of numbers in terms of value) You are allowed to define any combination of min and max. Omit for none, `{min}` for min, `{,max}` for max. If you set `min` and `max` with the same integer, then the provided string must have equal length. 8 | - **Special Repeat Tag** `[...]` will repeat the last usage optionally until you run out of arguments. Useful for doing something like ` [...]` which will allow you to take as many search terms as you want, per your Usage Delimiter. 9 | 10 | > Note: You can set multiple options in an argument by writting `|`. For example: `` will work when you provide a message ID or a string with a length between 4 and 16 (including both limits). 11 | 12 | ### Usage Types 13 | 14 | | Type | Description 15 | | ---------------------------: | ----------- 16 | | `literal` | Literally equal to the Name. This is the default type if none is defined. 17 | | `str` \| `string` | A [String](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/String). 18 | | `int` \| `integer` | An [Integer](https://en.wikipedia.org/wiki/Integer). 19 | | `num` \| `number` \| `float` | A [Floating Point Number](https://en.wikipedia.org/wiki/Floating-point_arithmetic). 20 | | `boolean` | A [Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean). 21 | | `url` | A [URL](https://en.wikipedia.org/wiki/URL). 22 | | `msg` \| `message` | A [Message](https://discord.js.org/#/docs/main/master/class/Message) instance returned from the message ID. 23 | | `role` | A [Role](https://discord.js.org/#/docs/main/master/class/Role) instance returned from the role ID or mention. 24 | | `channel` | A [TextChannel](https://discord.js.org/#/docs/main/master/class/TextChannel) instance returned from the channel ID or channel tag. 25 | | `guild` | A [Guild](https://discord.js.org/#/docs/main/master/class/Guild) instance returned from the guild ID. 26 | | `user` \| `mention` | A [User](https://discord.js.org/#/docs/main/master/class/User) instance returned from the user ID or mention. 27 | | `member` | A [GuildMember](https://discord.js.org/#/docs/main/master/class/GuildMember) instance returned from the member ID or mention. 28 | 29 | > Note: `Literal` is very useful in arguments with multiple options. 30 | 31 | ___ 32 | 33 | # Using arguments in your command. 34 | 35 | Now, after we understand how to configurate the command, we'll start writting it: 36 | 37 | ```javascript 38 | exports.run = async (client, msg, [...args]) => { 39 | // Place Code Here 40 | }; 41 | ``` 42 | 43 | `[...args]` represents a variable number of arguments give when the command is run. The name of the arguments in the array (and their count) is determined by the `usage` property and its given arguments. 44 | 45 | > Note that the commands' arguments are an array. This is a trick called [Destructuring assignment](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment). 46 | 47 | For example, when you have: 48 | 49 | ```javascript 50 | exports.help = { 51 | name: "messager", 52 | description: "Deletes a message, or edits it.", 53 | usage: " [newContent:string]", 54 | usageDelim: "|", 55 | extendedHelp: "", 56 | }; 57 | ``` 58 | 59 | Then, we have to define all the arguments from *help.usage*, the following code block is an example of how it would look like, and how we would work with them. 60 | 61 | ```javascript 62 | exports.run = async (client, msg, [message, action, newContent]) => { 63 | // code 64 | }; 65 | ``` 66 | 67 | In which `message` is the argument assigned to the message object as provided in `` argument from usage. Same does `action` for `` and respectively. 68 | 69 | > Keep in mind that we declared `newContent` as an optional argument, if it's not provided, it'll return undefined. 70 | 71 | Keep in mind that arguments are delimited by the character or combination of characters written in *help.usageDelim*. In this case, we have assigned the character `|` for it. How do we use this command? Easy: 72 | 73 | `komessager 293107496191655936|delete` 74 | 75 | The line above will execute the command with the name `messager` (or a command with `messager` as an alias), it'll use [Channel.messages.fetch](https://discord.js.org/#/docs/main/master/class/MessageStore?scrollTo=fetch) if the bot is a userbot). If the message is not found (you mistyped it or the message is in another channel) it'll warn you that the message hasn't been found. The next argument is a literal, in which must be either `delete` or `edit`. Keep in mind that Komada does *String.toLowerCase()*, if you write `DELETE` or any other variation of [font case](https://techterms.com/definition/font_case), it'll work too. 76 | 77 | We come back to the `exports.run`, remember that we have: 78 | 79 | ```javascript 80 | exports.run = async (client, msg, [message, action, newContent]) => { 81 | // code 82 | }; 83 | ``` 84 | 85 | As I explained before, `message` will return a message object, it'll be the message found with the ID `293107496191655936`, and `action` is defined as the string `delete`. 86 | `newContent` is undefined because we didn't write that argument. The code inside the curly brackets (`{}`) will be executed when the usage is valid. 87 | -------------------------------------------------------------------------------- /docs/tutorials/UnderstandingUsageString.md: -------------------------------------------------------------------------------- 1 | ## Usage Structure 2 | 3 | `<>` required argument, `[]` optional argument `` 4 | 5 | - **Name** Mostly used for debugging message, unless the type is Literal in which it compares the argument to the name. 6 | - **Type** The type of variable you are expecting. 7 | - **Min, Max** Minimum or Maximum for a giving variable (works on strings in terms of length, and on all types of numbers in terms of value) You are allowed to define any combination of min and max. Omit for none, `{min}` for min, `{,max}` for max. If you set `min` and `max` with the same integer, then the provided string must have equal length. 8 | - **Special Repeat Tag** `[...]` will repeat the last usage optionally until you run out of arguments. Useful for doing something like ` [...]` which will allow you to take as many search terms as you want, per your Usage Delimiter. 9 | 10 | > Note: You can set multiple options in an argument by writting `|`. For example: `` will work when you provide a message ID or a string with a length between 4 and 16 (including both limits). 11 | 12 | ### Usage Types 13 | 14 | | Type | Description 15 | | ---------------------------: | ----------- 16 | | `literal` | Literally equal to the Name. This is the default type if none is defined. 17 | | `str` \| `string` | A [String](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Global_Objects/String). 18 | | `int` \| `integer` | An [Integer](https://en.wikipedia.org/wiki/Integer). 19 | | `num` \| `number` \| `float` | A [Floating Point Number](https://en.wikipedia.org/wiki/Floating-point_arithmetic). 20 | | `boolean` | A [Boolean](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Boolean). 21 | | `url` | A [URL](https://en.wikipedia.org/wiki/URL). 22 | | `msg` \| `message` | A [Message](https://discord.js.org/#/docs/main/master/class/Message) instance returned from the message ID. 23 | | `role` | A [Role](https://discord.js.org/#/docs/main/master/class/Role) instance returned from the role ID or mention. 24 | | `channel` | A [TextChannel](https://discord.js.org/#/docs/main/master/class/TextChannel) instance returned from the channel ID or channel tag. 25 | | `guild` | A [Guild](https://discord.js.org/#/docs/main/master/class/Guild) instance returned from the guild ID. 26 | | `user` \| `mention` | A [User](https://discord.js.org/#/docs/main/master/class/User) instance returned from the user ID or mention. 27 | | `member` | A [GuildMember](https://discord.js.org/#/docs/main/master/class/GuildMember) instance returned from the member ID or mention. 28 | 29 | > Note: `Literal` is very useful in arguments with multiple options. 30 | 31 | ___ 32 | 33 | # Using arguments in your command. 34 | 35 | Now, after we understand how to configurate the command, we'll start writting it: 36 | 37 | ```javascript 38 | exports.run = async (client, msg, [...args]) => { 39 | // Place Code Here 40 | }; 41 | ``` 42 | 43 | `[...args]` represents a variable number of arguments give when the command is run. The name of the arguments in the array (and their count) is determined by the `usage` property and its given arguments. 44 | 45 | > Note that the commands' arguments are an array. This is a trick called [Destructuring assignment](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Operators/Destructuring_assignment). 46 | 47 | For example, when you have: 48 | 49 | ```javascript 50 | exports.help = { 51 | name: "messager", 52 | description: "Deletes a message, or edits it.", 53 | usage: " [newContent:string]", 54 | usageDelim: "|", 55 | extendedHelp: "", 56 | }; 57 | ``` 58 | 59 | Then, we have to define all the arguments from *help.usage*, the following code block is an example of how it would look like, and how we would work with them. 60 | 61 | ```javascript 62 | exports.run = async (client, msg, [message, action, newContent]) => { 63 | // code 64 | }; 65 | ``` 66 | 67 | In which `message` is the argument assigned to the message object as provided in `` argument from usage. Same does `action` for `` and respectively. 68 | 69 | > Keep in mind that we declared `newContent` as an optional argument, if it's not provided, it'll return undefined. 70 | 71 | Keep in mind that arguments are delimited by the character or combination of characters written in *help.usageDelim*. In this case, we have assigned the character `|` for it. How do we use this command? Easy: 72 | 73 | `komessager 293107496191655936|delete` 74 | 75 | The line above will execute the command with the name `messager` (or a command with `messager` as an alias), it'll use [Channel.messages.fetch](https://discord.js.org/#/docs/main/master/class/MessageStore?scrollTo=fetch) if the bot is a userbot). If the message is not found (you mistyped it or the message is in another channel) it'll warn you that the message hasn't been found. The next argument is a literal, in which must be either `delete` or `edit`. Keep in mind that Komada does *String.toLowerCase()*, if you write `DELETE` or any other variation of [font case](https://techterms.com/definition/font_case), it'll work too. 76 | 77 | We come back to the `exports.run`, remember that we have: 78 | 79 | ```javascript 80 | exports.run = async (client, msg, [message, action, newContent]) => { 81 | // code 82 | }; 83 | ``` 84 | 85 | As I explained before, `message` will return a message object, it'll be the message found with the ID `293107496191655936`, and `action` is defined as the string `delete`. 86 | `newContent` is undefined because we didn't write that argument. The code inside the curly brackets (`{}`) will be executed when the usage is valid. 87 | -------------------------------------------------------------------------------- /docs/scripts/tui-doc.js: -------------------------------------------------------------------------------- 1 | "use strict"; 2 | 3 | /*************** API-EXAMPLES TAB ***************/ 4 | var $apiTab = $('#api-tab'); 5 | var $examplesTab = $('#examples-tab'); 6 | 7 | function showLnbExamples() { 8 | $apiTab.removeClass('selected'); 9 | $examplesTab.addClass('selected'); 10 | $('.lnb-api').addClass('hidden'); 11 | $('.lnb-examples').removeClass('hidden'); 12 | } 13 | 14 | function showLnbApi() { 15 | $examplesTab.removeClass('selected'); 16 | $apiTab.addClass('selected'); 17 | $('.lnb-api').removeClass('hidden'); 18 | $('.lnb-examples').addClass('hidden'); 19 | } 20 | 21 | $apiTab.click(showLnbApi); 22 | $examplesTab.click(showLnbExamples); 23 | 24 | /*************** RESIZE ***************/ 25 | var $resizer = $('#resizer'); 26 | var $lnb = $('#lnb'); 27 | var $main = $('#main'); 28 | 29 | function resize(event) { 30 | var clientX = event.clientX; 31 | 32 | clientX = Math.max(200, clientX); 33 | clientX = Math.min(500, clientX); 34 | 35 | $lnb.css('width', clientX); 36 | $resizer.css('left', clientX); 37 | $main.css('left', clientX + $resizer.width()); 38 | } 39 | 40 | function detachResize() { 41 | $(window).off({ 42 | mousemove: resize, 43 | mouseup: detachResize 44 | }); 45 | } 46 | 47 | $resizer.on('mousedown', function() { 48 | $(window).on({ 49 | mousemove: resize, 50 | mouseup: detachResize 51 | }); 52 | }); 53 | 54 | /*************** SEARCH - AUTOCOMPLETE ***************/ 55 | var $searchContainer = $('#search-container'); 56 | var $searchInput = $searchContainer.find('input'); 57 | var $searchedList = $searchContainer.find('ul'); 58 | var $anchorList = $('nav ul li a'); 59 | var $selected = $(); 60 | 61 | var KEY_CODE_UP = 38; 62 | var KEY_CODE_DOWN = 40; 63 | var KEY_CODE_ENTER = 13; 64 | 65 | $(window).on('click', function(event) { 66 | if (!$searchContainer[0].contains(event.target)) { 67 | clear(); 68 | } 69 | }); 70 | 71 | $searchedList.on('click', 'li', function(event) { 72 | var currentTarget = event.currentTarget; 73 | var url = $(currentTarget).find('a').attr('href'); 74 | 75 | moveToPage(url); 76 | }); 77 | 78 | $searchInput.on({ 79 | keyup: onKeyupSearchInput, 80 | keydown: onKeydownInput 81 | }); 82 | 83 | function onKeyupSearchInput(event) { 84 | var inputText = removeWhiteSpace($searchInput.val()).toLowerCase(); 85 | 86 | if (event.keyCode === KEY_CODE_UP || event.keyCode === KEY_CODE_DOWN) { 87 | return; 88 | } 89 | 90 | if (!inputText) { 91 | $searchedList.html(''); 92 | return; 93 | } 94 | 95 | if (event.keyCode === KEY_CODE_ENTER) { 96 | onKeyupEnter(); 97 | return; 98 | } 99 | 100 | setList(inputText); 101 | } 102 | 103 | function onKeydownInput(event) { 104 | $selected.removeClass('highlight'); 105 | 106 | switch(event.keyCode) { 107 | case KEY_CODE_UP: 108 | $selected = $selected.prev(); 109 | if (!$selected.length) { 110 | $selected = $searchedList.find('li').last(); 111 | } 112 | break; 113 | case KEY_CODE_DOWN: 114 | $selected = $selected.next(); 115 | if (!$selected.length) { 116 | $selected = $searchedList.find('li').first(); 117 | } 118 | break; 119 | default: break; 120 | } 121 | 122 | $selected.addClass('highlight'); 123 | } 124 | 125 | function onKeyupEnter() { 126 | if (!$selected.length) { 127 | $selected = $searchedList.find('li').first(); 128 | } 129 | moveToPage($selected.find('a').attr('href')); 130 | } 131 | 132 | function moveToPage(url) { 133 | if (url) { 134 | window.location = url; 135 | } 136 | clear(); 137 | } 138 | 139 | function clear() { 140 | $searchedList.html(''); 141 | $searchInput.val(''); 142 | $selected = $(); 143 | } 144 | 145 | function setList(inputText) { 146 | var html = ''; 147 | 148 | $anchorList.filter(function(idx, item) { 149 | return isMatched(item.text, inputText); 150 | }).each(function(idx, item) { 151 | html += makeListItemHtml(item, inputText); 152 | }); 153 | $searchedList.html(html); 154 | } 155 | 156 | function isMatched(itemText, inputText) { 157 | return removeWhiteSpace(itemText).toLowerCase().indexOf(inputText) > - 1; 158 | } 159 | 160 | function makeListItemHtml(item, inputText) { 161 | var itemText = item.text; 162 | var itemHref = item.href; 163 | var $parent = $(item).closest('div'); 164 | var memberof = ''; 165 | 166 | if ($parent.length && $parent.attr('id')) { 167 | memberof = $parent.attr('id').replace('_sub', ''); 168 | } else { 169 | memberof = $(item).closest('div').find('h3').text(); 170 | } 171 | 172 | if (memberof) { 173 | memberof = '' + memberof + ''; 174 | } 175 | 176 | itemText = itemText.replace(new RegExp(inputText, 'ig'), function(matched) { 177 | return '' + matched + ''; 178 | }); 179 | 180 | return '
  • ' + itemText + '' + memberof + '
  • '; 181 | } 182 | 183 | function removeWhiteSpace(value) { 184 | return value.replace(/\s/g, ''); 185 | } 186 | 187 | /*************** TOOGLE SUB NAV ***************/ 188 | function toggleSubNav(e) { 189 | $(e.currentTarget).next().toggleClass('hidden'); 190 | $(e.currentTarget).find('.glyphicon').toggleClass('glyphicon-plus glyphicon-minus'); 191 | } 192 | 193 | $lnb.find('.lnb-api').each(function() { 194 | $(this).find('.toggle-subnav') 195 | .filter(function() { 196 | return $(this).next(':empty').length === 0; 197 | }).each(function() { 198 | $(this).removeClass('hidden').on('click', toggleSubNav); 199 | }); 200 | }); 201 | -------------------------------------------------------------------------------- /examples/PermissionLevels.md: -------------------------------------------------------------------------------- 1 | # Permission Levels (aka PermStructure or Permission Structure) 2 | 3 | Permission levels allow you to tailor your bot to only run commands for whomever you want. This structure is always ran when checking for commands, and is also ran when checking for things like: "Should this command that the user can't use be displayed 4 | in the help menu?". Permission levels can do almost anything you want as long as you follow the structure correctly. 5 | 6 | By default Komada comes with the following permission levels: 7 | 8 | ```js 9 | const defaultPermStructure = new PermLevels() 10 | .addLevel(0, false, () => true) 11 | .addLevel(2, false, (client, msg) => { 12 | if (!msg.guild || !msg.guild.settings.modRole) return false; 13 | const modRole = msg.guild.roles.get(msg.guild.settings.modRole); 14 | return modRole && msg.member.roles.has(modRole.id); 15 | }) 16 | .addLevel(3, false, (client, msg) => { 17 | if (!msg.guild || !msg.guild.settings.adminRole) return false; 18 | const adminRole = msg.guild.roles.get(msg.guild.settings.adminRole); 19 | return adminRole && msg.member.roles.has(adminRole.id); 20 | }) 21 | .addLevel(4, false, (client, msg) => msg.guild && msg.author.id === msg.guild.owner.id) 22 | .addLevel(9, true, (client, msg) => msg.author.id === client.config.ownerID) 23 | .addLevel(10, false, (client, msg) => msg.author.id === client.config.ownerID); 24 | ``` 25 | 26 | Basically, this means that Commands with the permission level: 27 | - 0, Everyone can use. 28 | - 2, Only those with the modRole can use 29 | - 3, Only those with the adminRole can use. 30 | - 4, Only those who are the GuildOwner can use. 31 | - 9-10, Only the bot owner can use these. 32 | 33 | > Permission Level 9 is a very special one. The difference between 9 and 10 is that we break on 9. Basically, breaking means that if you don't have the required permission level, Komada should respond back to you telling you that you can't use the command. 34 | > This allows you to have special interactions when certain people use the commands, such as silent admin commands. 35 | 36 | # Completely new Permission structure 37 | If you want to completely get rid of the default permStructure and create your own, you will do this in the file that you declare a new Komada client. Here's a starting point that you can use. 38 | ```js 39 | const { Client, PermLevels } = require("komada"); 40 | 41 | const client = new Client({ 42 | ownerID : "your-user-id", 43 | prefix: "+", 44 | clientOptions: { 45 | fetchAllMembers: false, 46 | }, 47 | }); 48 | 49 | client.login("your-bot-token"); 50 | ``` 51 | 52 | You'll notice we destructured Komada since we only need access to Client and PermLevels. To get started with PermLevels you'll first create a new class and set it to a variable. 53 | ```js 54 | const { Client, PermLevels } = require("komada"); 55 | 56 | const permStructure = new PermLevels(); 57 | 58 | const client = new Client({ 59 | ownerID : "your-user-id", 60 | prefix: "+", 61 | clientOptions: { 62 | fetchAllMembers: false, 63 | }, 64 | }); 65 | 66 | client.login("your-bot-token"); 67 | ``` 68 | 69 | Now that you've created your new PermLevels and assigned it to a variable, there are two ways you can add to this new structure. Both of these are valid and supported ways of adding levels. 70 | 71 | ```js 72 | const { Client, PermLevels } = require("komada"); 73 | 74 | /** First Way */ 75 | const permStructure = new PermLevels(); 76 | permStructure.addLevel(0, false, () => true); 77 | permStructure.addLevel(10, false, (client, msg) => msg.author === client.owner); 78 | 79 | /** Second Way */ 80 | const permStructure = new PermLevels() 81 | .addLevel(0, false () => true) 82 | .addLevel(10, false, (client, msg) => msg.author === client.owner); 83 | 84 | const client = new Client({ 85 | ownerID : "your-user-id", 86 | prefix: "+", 87 | clientOptions: { 88 | fetchAllMembers: false, 89 | }, 90 | }); 91 | 92 | client.login("your-bot-token"); 93 | ``` 94 | 95 | And now that you've created your new permStructure with your new levels, all you need to do is let Komada know you want to use this one instead, and we do this by simply passing it as a configuration option, like so: 96 | ```js 97 | const { Client, PermLevels } = require("komada"); 98 | 99 | const permStructure = new PermLevels() 100 | .addLevel(0, false () => true) 101 | .addLevel(10, false, (client, msg) => msg.author === client.owner); 102 | 103 | const client = new Client({ 104 | ownerID : "your-user-id", 105 | prefix: "+", 106 | permStructure, 107 | clientOptions: { 108 | fetchAllMembers: false, 109 | }, 110 | }); 111 | 112 | client.login("your-bot-token"); 113 | ``` 114 | 115 | And that's all you need to do. Now you have a new permStructure with just two levels. 116 | 117 | # Edit the Existing Permission Structure 118 | If you want to edit the existing permStructure in Komada you can do so after creating your client. 119 | 120 | ```js 121 | const { Client } = require("komada"); 122 | 123 | const client = new Client({ 124 | ownerID : "your-user-id", 125 | prefix: "+", 126 | clientOptions: { 127 | fetchAllMembers: false, 128 | }, 129 | }); 130 | 131 | client.permStructure.addLevel(8, false, (client, msg) => msg.author.username === "Faith"); 132 | 133 | client.login("your-bot-token"); 134 | ``` 135 | 136 | > Please note that you will not be able to edit already existing permLevels in the default structure, so if you're looking to overwrite a default permission level you should check the other portion of this guide instead. 137 | 138 | # Final Words 139 | That about sums up permission levels. If you're having issues with this or don't quite understand whats happening, I encourage you to read the documentation on [PermissionLevels](https://dirigeants.github.io/komada/PermissionLevels.html). If you still don't understand after reading the documentation, then hop on our Discord server and we'll be more than happy to explain to you what you don't understand. 140 | -------------------------------------------------------------------------------- /docs/tutorials/PermissionLevels.md: -------------------------------------------------------------------------------- 1 | # Permission Levels (aka PermStructure or Permission Structure) 2 | 3 | Permission levels allow you to tailor your bot to only run commands for whomever you want. This structure is always ran when checking for commands, and is also ran when checking for things like: "Should this command that the user can't use be displayed 4 | in the help menu?". Permission levels can do almost anything you want as long as you follow the structure correctly. 5 | 6 | By default Komada comes with the following permission levels: 7 | 8 | ```js 9 | const defaultPermStructure = new PermLevels() 10 | .addLevel(0, false, () => true) 11 | .addLevel(2, false, (client, msg) => { 12 | if (!msg.guild || !msg.guild.settings.modRole) return false; 13 | const modRole = msg.guild.roles.get(msg.guild.settings.modRole); 14 | return modRole && msg.member.roles.has(modRole.id); 15 | }) 16 | .addLevel(3, false, (client, msg) => { 17 | if (!msg.guild || !msg.guild.settings.adminRole) return false; 18 | const adminRole = msg.guild.roles.get(msg.guild.settings.adminRole); 19 | return adminRole && msg.member.roles.has(adminRole.id); 20 | }) 21 | .addLevel(4, false, (client, msg) => msg.guild && msg.author.id === msg.guild.owner.id) 22 | .addLevel(9, true, (client, msg) => msg.author.id === client.config.ownerID) 23 | .addLevel(10, false, (client, msg) => msg.author.id === client.config.ownerID); 24 | ``` 25 | 26 | Basically, this means that Commands with the permission level: 27 | - 0, Everyone can use. 28 | - 2, Only those with the modRole can use 29 | - 3, Only those with the adminRole can use. 30 | - 4, Only those who are the GuildOwner can use. 31 | - 9-10, Only the bot owner can use these. 32 | 33 | > Permission Level 9 is a very special one. The difference between 9 and 10 is that we break on 9. Basically, breaking means that if you don't have the required permission level, Komada should respond back to you telling you that you can't use the command. 34 | > This allows you to have special interactions when certain people use the commands, such as silent admin commands. 35 | 36 | # Completely new Permission structure 37 | If you want to completely get rid of the default permStructure and create your own, you will do this in the file that you declare a new Komada client. Here's a starting point that you can use. 38 | ```js 39 | const { Client, PermLevels } = require("komada"); 40 | 41 | const client = new Client({ 42 | ownerID : "your-user-id", 43 | prefix: "+", 44 | clientOptions: { 45 | fetchAllMembers: false, 46 | }, 47 | }); 48 | 49 | client.login("your-bot-token"); 50 | ``` 51 | 52 | You'll notice we destructured Komada since we only need access to Client and PermLevels. To get started with PermLevels you'll first create a new class and set it to a variable. 53 | ```js 54 | const { Client, PermLevels } = require("komada"); 55 | 56 | const permStructure = new PermLevels(); 57 | 58 | const client = new Client({ 59 | ownerID : "your-user-id", 60 | prefix: "+", 61 | clientOptions: { 62 | fetchAllMembers: false, 63 | }, 64 | }); 65 | 66 | client.login("your-bot-token"); 67 | ``` 68 | 69 | Now that you've created your new PermLevels and assigned it to a variable, there are two ways you can add to this new structure. Both of these are valid and supported ways of adding levels. 70 | 71 | ```js 72 | const { Client, PermLevels } = require("komada"); 73 | 74 | /** First Way */ 75 | const permStructure = new PermLevels(); 76 | permStructure.addLevel(0, false, () => true); 77 | permStructure.addLevel(10, false, (client, msg) => msg.author === client.owner); 78 | 79 | /** Second Way */ 80 | const permStructure = new PermLevels() 81 | .addLevel(0, false () => true) 82 | .addLevel(10, false, (client, msg) => msg.author === client.owner); 83 | 84 | const client = new Client({ 85 | ownerID : "your-user-id", 86 | prefix: "+", 87 | clientOptions: { 88 | fetchAllMembers: false, 89 | }, 90 | }); 91 | 92 | client.login("your-bot-token"); 93 | ``` 94 | 95 | And now that you've created your new permStructure with your new levels, all you need to do is let Komada know you want to use this one instead, and we do this by simply passing it as a configuration option, like so: 96 | ```js 97 | const { Client, PermLevels } = require("komada"); 98 | 99 | const permStructure = new PermLevels() 100 | .addLevel(0, false () => true) 101 | .addLevel(10, false, (client, msg) => msg.author === client.owner); 102 | 103 | const client = new Client({ 104 | ownerID : "your-user-id", 105 | prefix: "+", 106 | permStructure, 107 | clientOptions: { 108 | fetchAllMembers: false, 109 | }, 110 | }); 111 | 112 | client.login("your-bot-token"); 113 | ``` 114 | 115 | And that's all you need to do. Now you have a new permStructure with just two levels. 116 | 117 | # Edit the Existing Permission Structure 118 | If you want to edit the existing permStructure in Komada you can do so after creating your client. 119 | 120 | ```js 121 | const { Client } = require("komada"); 122 | 123 | const client = new Client({ 124 | ownerID : "your-user-id", 125 | prefix: "+", 126 | clientOptions: { 127 | fetchAllMembers: false, 128 | }, 129 | }); 130 | 131 | client.permStructure.addLevel(8, false, (client, msg) => msg.author.username === "Faith"); 132 | 133 | client.login("your-bot-token"); 134 | ``` 135 | 136 | > Please note that you will not be able to edit already existing permLevels in the default structure, so if you're looking to overwrite a default permission level you should check the other portion of this guide instead. 137 | 138 | # Final Words 139 | That about sums up permission levels. If you're having issues with this or don't quite understand whats happening, I encourage you to read the documentation on [PermissionLevels](https://dirigeants.github.io/komada/PermissionLevels.html). If you still don't understand after reading the documentation, then hop on our Discord server and we'll be more than happy to explain to you what you don't understand. 140 | -------------------------------------------------------------------------------- /src/classes/Resolver.js: -------------------------------------------------------------------------------- 1 | const url = require("url"); 2 | const { Message, User, GuildMember, Role, Guild, Channel } = require("discord.js"); 3 | 4 | const regex = { 5 | userOrMember: new RegExp("^(?:<@!?)?(\\d{17,21})>?$"), 6 | channel: new RegExp("^(?:<#)?(\\d{17,21})>?$"), 7 | role: new RegExp("^(?:<@&)?(\\d{17,21})>?$"), 8 | snowflake: new RegExp("^(\\d{17,21})$"), 9 | }; 10 | 11 | /* eslint-disable class-methods-use-this */ 12 | 13 | /** 14 | * The base resolver class 15 | */ 16 | class Resolver { 17 | 18 | /** 19 | * @param {KomadaClient} client The Komada Client 20 | */ 21 | constructor(client) { 22 | Object.defineProperty(this, "client", { value: client }); 23 | } 24 | 25 | /** 26 | * Fetch a Message object by its Snowflake or instanceof Message. 27 | * @param {Snowflake} message The message snowflake to validate. 28 | * @param {Channel} channel The Channel object in which the message can be found. 29 | * @returns {Promise} 30 | */ 31 | async msg(message, channel) { 32 | if (message instanceof Message) return message; 33 | return regex.snowflake.test(message) ? channel.messages.fetch(message).catch(() => null) : undefined; 34 | } 35 | 36 | /** 37 | * Fetch messages by a snowflake or instanceof Message 38 | * @param {Snowflake} message The message snowflake to validate. 39 | * @param {Channel} channel The Channel object in which the message can be found. 40 | * @param {number} [limit=100] The number of messages to fetch and send back. 41 | * @return {Promise>} 42 | */ 43 | async msgs(message, channel, limit = 100) { 44 | if (message instanceof Message) message = message.id; 45 | return regex.snowflake.test(message) ? channel.messages.fetch(message, { limit, around: message }).catch(() => null) : undefined; 46 | } 47 | 48 | /** 49 | * Resolve a User object by its instance of User, GuildMember, or by its Snowflake. 50 | * @param {User} user The user to validate. 51 | * @returns {Promise} 52 | */ 53 | async user(user) { 54 | if (user instanceof User) return user; 55 | if (user instanceof GuildMember) return user.user; 56 | if (typeof user === "string" && regex.userOrMember.test(user)) return this.client.user.bot ? this.client.users.fetch(regex.userOrMember.exec(user)[1]).catch(() => null) : this.client.users.get(regex.userOrMember.exec(user)[1]); 57 | return null; 58 | } 59 | 60 | /** 61 | * Resolve a GuildMember object by its instance of GuildMember, User, or by its Snowflake. 62 | * @param {(GuildMember|User|Snowflake)} member The number to validate. 63 | * @param {Guild} guild The Guild object in which the member can be found. 64 | * @returns {Promise} 65 | */ 66 | async member(member, guild) { 67 | if (member instanceof GuildMember) return member; 68 | if (member instanceof User) return guild.members.fetch(member); 69 | if (typeof member === "string" && regex.userOrMember.test(member)) { 70 | const user = this.client.user.bot ? await this.client.users.fetch(regex.userOrMember.exec(member)[1]).catch(() => null) : this.client.users.get(regex.userOrMember.exec(member)[1]); 71 | if (user) return guild.members.fetch(user).catch(() => null); 72 | } 73 | return null; 74 | } 75 | 76 | /** 77 | * Resolve a Channel object by its instance of Channel, or by its Snowflake. 78 | * @param {Channel} channel The channel to validate. 79 | * @returns {Promise} 80 | */ 81 | async channel(channel) { 82 | if (channel instanceof Channel) return channel; 83 | if (typeof channel === "string" && regex.channel.test(channel)) return this.client.channels.get(regex.channel.exec(channel)[1]); 84 | return null; 85 | } 86 | 87 | /** 88 | * Resolve a Guild object by its instance of Guild, or by its Snowflake. 89 | * @param {Guild} guild The guild to validate/find. 90 | * @returns {Promise} 91 | */ 92 | async guild(guild) { 93 | if (guild instanceof Guild) return guild; 94 | if (typeof guild === "string" && regex.snowflake.test(guild)) return this.client.guilds.get(guild); 95 | return null; 96 | } 97 | 98 | /** 99 | * Resolve a Role object by its instance of Role, or by its Snowflake. 100 | * @param {Role} role The role to validate/find. 101 | * @param {Guild} guild The Guild object in which the role can be found. 102 | * @returns {Promise} 103 | */ 104 | async role(role, guild) { 105 | if (role instanceof Role) return role; 106 | if (typeof role === "string" && regex.role.test(role)) return guild.roles.get(regex.role.exec(role)[1]); 107 | return null; 108 | } 109 | 110 | /** 111 | * Resolve a Boolean instance. 112 | * @param {(boolean|string)} bool The boolean to validate. 113 | * @returns {Promise} 114 | */ 115 | async boolean(bool) { 116 | if (bool instanceof Boolean) return bool; 117 | if (["1", "true", "+", "t", "yes", "y"].includes(String(bool).toLowerCase())) return true; 118 | if (["0", "false", "-", "f", "no", "n"].includes(String(bool).toLowerCase())) return false; 119 | return null; 120 | } 121 | 122 | /** 123 | * Resolve a String instance. 124 | * @param {string} string The string to validate. 125 | * @returns {Promise} 126 | */ 127 | async string(string) { 128 | return String(string); 129 | } 130 | 131 | /** 132 | * Resolve an Integer. 133 | * @param {(string|number)} integer The integer to validate. 134 | * @returns {Promise} 135 | */ 136 | async integer(integer) { 137 | integer = parseInt(integer); 138 | if (Number.isInteger(integer)) return integer; 139 | return null; 140 | } 141 | 142 | /** 143 | * Resolve a Float. 144 | * @param {(string|number)} number The float to validate. 145 | * @returns {Promise} 146 | */ 147 | async float(number) { 148 | number = parseFloat(number); 149 | if (!Number.isNaN(number)) return number; 150 | return null; 151 | } 152 | 153 | /** 154 | * Resolve a hyperlink. 155 | * @param {string} hyperlink The hyperlink to validate. 156 | * @returns {Promise} 157 | */ 158 | async url(hyperlink) { 159 | const res = url.parse(hyperlink); 160 | if (res.protocol && res.hostname) return hyperlink; 161 | return null; 162 | } 163 | 164 | } 165 | 166 | module.exports = Resolver; 167 | -------------------------------------------------------------------------------- /src/classes/settings/Settings.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable no-underscore-dangle */ 2 | 3 | const Gateway = require("./Gateway"); 4 | const Schema = require("./Schema"); 5 | const { resolve } = require("path"); 6 | const fs = require("fs-nextra"); 7 | 8 | class Settings { 9 | 10 | /** 11 | * Creates a new settings instance. 12 | * @param {KomadaClient} client The Komada clien 13 | * @param {string} name The name of these new settings 14 | * @param {Function} validate The validate function for gateway 15 | * @param {Schema|Object} schema The schema object 16 | * @param {SettingResolver} resolver The resolver class 17 | */ 18 | constructor(client, name, validate, schema, resolver) { 19 | /** 20 | * The komada client. 21 | * @type {KomadaClient} 22 | */ 23 | Object.defineProperty(this, "client", { value: client }); 24 | 25 | /** 26 | * The name or type of settings 27 | * @type {string} 28 | */ 29 | this.type = name; 30 | 31 | /** 32 | * The gateway for this settings instance. 33 | * @type {Gateway} 34 | */ 35 | this.gateway = new Gateway(this, validate); 36 | 37 | /** 38 | * The cache used to store data for this instance. 39 | * @type {Cache} 40 | */ 41 | this.cache = client.config.provider.cache === "js" ? client.providers.get("collection") : client.providers.get(client.config.provider.cache); 42 | 43 | /** 44 | * The schema that we will use for this instance. 45 | * @name Settings#schema 46 | * @type {Schema} 47 | */ 48 | Object.defineProperty(this, "_schema", { value: schema }); 49 | this.schema = null; 50 | 51 | /** 52 | * The settings resolver used for this instance. 53 | * @type {SettingResolver} 54 | */ 55 | this.resolver = resolver; 56 | 57 | /** 58 | * The base directory where this instance will save to. 59 | * @type {string} 60 | */ 61 | this.baseDir = resolve(this.client.clientBaseDir, "bwd"); 62 | 63 | /** 64 | * The path to the schema for this instance. 65 | * @type {string} 66 | */ 67 | this.schemaPath = resolve(this.baseDir, `${this.type}_Schema.json`); 68 | } 69 | 70 | /** 71 | * Initializes all of our different components. 72 | */ 73 | async init() { 74 | await fs.ensureDir(this.baseDir); 75 | const schema = await fs.readJSON(this.schemaPath) 76 | .catch(() => fs.outputJSONAtomic(this.schemaPath, this._schema).then(() => this._schema)); 77 | await this.validateSchema(schema); 78 | await this.gateway.init(this.schema); 79 | } 80 | 81 | // BEGIN SCHEMA EXPOSURE // 82 | 83 | /** 84 | * Validates our schema. Ensures that the object was created correctly and will not break. 85 | * @param {Object|Schema} schema The schema we are validating. 86 | */ 87 | validateSchema(schema) { 88 | if (!(schema instanceof Schema)) schema = new Schema(schema); 89 | for (const [key, value] of Object.entries(schema)) { // eslint-disable-line 90 | if (value instanceof Object && "type" in value && "default" in value) { 91 | if (value.array && !(value.default instanceof Array)) { 92 | this.client.emit("log", `The default value for ${key} must be an array.`, "error"); 93 | delete schema[key]; 94 | continue; 95 | } 96 | } else { 97 | delete schema[key]; 98 | this.client.emit("log", `The type value for ${key} is not supported. It must be an object with type and default properties.`, "error"); 99 | } 100 | } 101 | this.schema = schema; 102 | } 103 | 104 | /** 105 | * @param {string} name The name of the key you want to add. 106 | * @param {Schema.Options} options Schema options. 107 | * @param {boolean} [force=true] Whether or not we should force update all settings. 108 | * @returns {Promise} The new schema object 109 | */ 110 | async add(name, options, force = true) { 111 | this.schema.add(name, options); 112 | if (force) await this.force("add", name); 113 | fs.outputJSONAtomic(this.schemaPath, this.schema); 114 | return this.schema; 115 | } 116 | 117 | /** 118 | * Remove a key from the schema. 119 | * @param {string} key The key to remove. 120 | * @param {boolean} [force=false] Whether this change should modify all configurations or not. 121 | * @returns {Promise} The new schema object 122 | * @example 123 | * // Remove a key called 'modlog'. 124 | * await client.settings.guilds.remove("modlog"); 125 | */ 126 | async remove(key, force = true) { 127 | if (!(key in this.schema)) throw `The key ${key} does not exist in the schema.`; 128 | delete this.schema[key]; 129 | if (force) await this.force("delete", key); 130 | fs.outputJSONAtomic(this.schemaPath, this.schema); 131 | return this.schema; 132 | } 133 | 134 | /** 135 | * Modify all configurations. Do NOT use this directly. 136 | * @param {string} action Whether reset, add, or delete. 137 | * @param {string} key The key to update. 138 | * @returns {Promise} 139 | * @private 140 | */ 141 | async force(action, key) { 142 | if (this.gateway.sql) await this.gateway.sql.updateColumns(this.schema, this.schema.defaults, key); 143 | const data = this.cache.getAll(this.type); 144 | let value; 145 | if (action === "add") value = this.schema.defaults[key]; 146 | await Promise.all(data.map(async (obj) => { 147 | const object = obj; 148 | if (action === "delete") delete object[key]; else object[key] = value; 149 | if (obj.id) await this.gateway.provider.replace(this.type, obj.id, object); 150 | return true; 151 | })); 152 | return this.gateway.sync(); 153 | } 154 | 155 | // BEGIN GATEWAY EXPOSURE // 156 | 157 | /** 158 | * Creates a new entry in the cache. 159 | * @param {Object|string} input An object containing a id property, like discord.js objects, or a string. 160 | * @returns {Promisie} 161 | */ 162 | create(...args) { 163 | return this.gateway.create(...args); 164 | } 165 | 166 | /** 167 | * Removes an entry from the cache. 168 | * @param {Object|string} input An object containing a id property, like discord.js objects, or a string. 169 | * @returns {Promise} 170 | */ 171 | destroy(...args) { 172 | return this.gateway.destroy(...args); 173 | } 174 | 175 | /** 176 | * Gets an entry from the cache 177 | * @param {string} input The key you are you looking for. 178 | * @returns {Schema} 179 | */ 180 | get(...args) { 181 | return this.gateway.get(...args); 182 | } 183 | 184 | /** 185 | * Reset a key's value to default from a entry. 186 | * @param {Object|string} input An object containing a id property, like Discord.js objects, or a string. 187 | * @param {string} key The key to reset. 188 | * @returns {any} 189 | */ 190 | reset(...args) { 191 | return this.gateway.reset(...args); 192 | } 193 | 194 | /** 195 | * Updates an entry. 196 | * @param {Object|string} input An object or string that can be parsed by this instance's resolver. 197 | * @param {Object} object An object with pairs of key/value to update. 198 | * @param {Object|string} [guild=null] A Guild resolvable, useful for when the instance of SG doesn't aim for Guild settings. 199 | * @returns {Object} 200 | */ 201 | update(...args) { 202 | return this.gateway.update(...args); 203 | } 204 | 205 | /** 206 | * Update an array from the a Guild's configuration. 207 | * @param {Object|string} input An object containing a id property, like discord.js objects, or a string. 208 | * @param {string} type Either 'add' or 'remove'. 209 | * @param {string} key The key from the Schema. 210 | * @param {any} data The value to be added or removed. 211 | * @param {Object|string} [guild=null] The guild for this new setting change, useful for when settings don't aim for guilds. 212 | * @returns {boolean} 213 | */ 214 | updateArray(...args) { 215 | return this.gateway.updateArray(...args); 216 | } 217 | 218 | } 219 | 220 | module.exports = Settings; 221 | -------------------------------------------------------------------------------- /src/classes/parsedUsage.js: -------------------------------------------------------------------------------- 1 | /* eslint-disable class-methods-use-this */ 2 | /** 3 | * Converts usage strings into objects to compare against later 4 | */ 5 | class ParsedUsage { 6 | 7 | /** 8 | * @param {KomadaClient} client The Komada client 9 | * @param {Command} command The command this parsed usage is for 10 | */ 11 | constructor(client, command) { 12 | /** 13 | * The client this CommandMessage was created with. 14 | * @name ParsedUsage#client 15 | * @type {KomadaClient} 16 | * @readonly 17 | */ 18 | Object.defineProperty(this, "client", { value: client }); 19 | 20 | /** 21 | * All names and aliases for the command 22 | * @type {string[]} 23 | */ 24 | this.names = [command.help.name, ...command.conf.aliases]; 25 | 26 | /** 27 | * The compiled string for all names/aliases in a usage string 28 | * @type {string} 29 | */ 30 | this.commands = this.names.length === 1 ? this.names[0] : `(${this.names.join("|")})`; 31 | 32 | /** 33 | * The usage string re-deliminated with the usageDelim 34 | * @type {string} 35 | */ 36 | this.deliminatedUsage = command.help.usage !== "" ? ` ${command.help.usage.split(" ").join(command.help.usageDelim)}` : ""; 37 | 38 | /** 39 | * The usage string 40 | * @type {string} 41 | */ 42 | this.usageString = command.help.usage; 43 | 44 | /** 45 | * The usage object to compare against later 46 | * @type {Object[]} 47 | */ 48 | this.parsedUsage = this.parseUsage(); 49 | 50 | /** 51 | * The concatenated string of this.commands and this.deliminatedUsage 52 | * @type {string} 53 | */ 54 | this.nearlyFullUsage = `${this.commands}${this.deliminatedUsage}`; 55 | } 56 | 57 | /** 58 | * Creates a full usage string including prefix and commands/aliases for documentation/help purposes 59 | * @param {external:Message} msg a message to check to get the current prefix 60 | * @returns {string} 61 | */ 62 | fullUsage(msg) { 63 | const prefix = msg.guildSettings.prefix || this.client.config.prefix; 64 | return `${prefix.length !== 1 ? `${prefix} ` : prefix}${this.nearlyFullUsage}`; 65 | } 66 | 67 | /** 68 | * Method responsible for building the usage object to check against 69 | * @private 70 | * @returns {Object} 71 | */ 72 | parseUsage() { 73 | let usage = { 74 | tags: [], 75 | opened: 0, 76 | current: "", 77 | openReq: false, 78 | last: false, 79 | char: 0, 80 | from: 0, 81 | at: "", 82 | fromto: "", 83 | }; 84 | 85 | this.usageString.split("").forEach((com, i) => { 86 | usage.char = i + 1; 87 | usage.from = usage.char - usage.current.length; 88 | usage.at = `at char #${usage.char} '${com}'`; 89 | usage.fromto = `from char #${usage.from} to #${usage.char} '${usage.current}'`; 90 | 91 | if (usage.last && com !== " ") { 92 | throw `${usage.at}: there can't be anything else after the repeat tag.`; 93 | } 94 | 95 | if (this[com]) { 96 | usage = this[com](usage); 97 | } else { 98 | usage.current += com; 99 | } 100 | }); 101 | 102 | if (usage.opened) throw `from char #${this.usageString.length - usage.current.length} '${this.usageString.substr(-usage.current.length - 1)}' to end: a tag was left open`; 103 | if (usage.current) throw `from char #${(this.usageString.length + 1) - usage.current.length} to end '${usage.current}' a literal was found outside a tag.`; 104 | 105 | return usage.tags; 106 | } 107 | 108 | ["<"](usage) { 109 | if (usage.opened) throw `${usage.at}: you might not open a tag inside another tag.`; 110 | if (usage.current) throw `${usage.fromto}: there can't be a literal outside a tag`; 111 | usage.opened++; 112 | usage.openReq = true; 113 | return usage; 114 | } 115 | 116 | [">"](usage) { 117 | if (!usage.opened) throw `${usage.at}: invalid close tag found`; 118 | if (!usage.openReq) throw `${usage.at}: Invalid closure of '[${usage.current}' with '>'`; 119 | usage.opened--; 120 | if (usage.current) { 121 | usage.tags.push({ 122 | type: "required", 123 | possibles: this.parseTag(usage.current, usage.tags.length + 1), 124 | }); 125 | usage.current = ""; 126 | } else { throw `${usage.at}: empty tag found`; } 127 | return usage; 128 | } 129 | 130 | ["["](usage) { 131 | if (usage.opened) throw `${usage.at}: you might not open a tag inside another tag.`; 132 | if (usage.current) throw `${usage.fromto}: there can't be a literal outside a tag`; 133 | usage.opened++; 134 | usage.openReq = false; 135 | return usage; 136 | } 137 | 138 | ["]"](usage) { 139 | if (!usage.opened) throw `${usage.at}: invalid close tag found`; 140 | if (usage.openReq) throw `${usage.at}: Invalid closure of '<${usage.current}' with ']'`; 141 | usage.opened--; 142 | if (usage.current === "...") { 143 | if (usage.tags.length < 1) { throw `${usage.fromto}: there can't be a loop at the begining`; } 144 | usage.tags.push({ type: "repeat" }); 145 | usage.last = true; 146 | usage.current = ""; 147 | } else if (usage.current) { 148 | usage.tags.push({ 149 | type: "optional", 150 | possibles: this.parseTag(usage.current, usage.tags.length + 1), 151 | }); 152 | usage.current = ""; 153 | } else { throw `${usage.at}: empty tag found`; } 154 | return usage; 155 | } 156 | 157 | [" "](usage) { 158 | if (usage.opened) throw `${usage.at}: spaces aren't allowed inside a tag`; 159 | if (usage.current) throw `${usage.fromto}: there can't be a literal outside a tag.`; 160 | return usage; 161 | } 162 | 163 | ["\n"](usage) { 164 | throw `${usage.at}: there can't be a line break in the command!`; 165 | } 166 | 167 | parseTag(tag, count) { 168 | const literals = []; 169 | const types = []; 170 | const toRet = []; 171 | 172 | const members = tag.split("|"); 173 | 174 | members.forEach((elemet, i) => { 175 | const current = `at tag #${count} at bound #${i + 1}`; 176 | 177 | const result = /^([^:]+)(?::([^{}]+))?(?:{([^,]+)?(?:,(.+))?})?$/i.exec(elemet); 178 | 179 | if (!result) throw `${current}: invalid syntax, non specific`; 180 | 181 | const fill = { 182 | name: result[1], 183 | type: result[2] ? result[2].toLowerCase() : "literal", 184 | }; 185 | 186 | if (result[3]) { 187 | const proto = " in the type length (min): "; 188 | 189 | if (fill.type === "literal") throw `${current + proto}you cannot set a length for a literal type`; 190 | 191 | if (Number.isNaN(result[3])) throw `${current + proto}must be a number`; 192 | 193 | const temp = parseFloat(result[3]); 194 | if ((fill.type === "string" || fill.type === "str") && temp % 1 !== 0) throw `${current + proto}the string type must have an integer length`; 195 | 196 | fill.min = temp; 197 | } 198 | 199 | if (result[4]) { 200 | const proto = " in the type length (max): "; 201 | if (fill.type === "literal") throw `${current + proto}you canno't set a length for a literal type`; 202 | 203 | if (Number.isNaN(result[4])) throw `${current + proto}must be a number`; 204 | 205 | const temp = parseFloat(result[4]); 206 | 207 | if ((fill.type === "string" || fill.type === "str") && temp % 1 !== 0) throw `${current + proto}the string type must have an integer length`; 208 | fill.max = temp; 209 | } 210 | 211 | if (fill.type === "literal") { 212 | if (literals.includes(fill.name)) throw `${current}: there can't be two literals with the same text.`; 213 | 214 | literals.push(fill.name); 215 | } else if (members.length > 1) { 216 | if (fill.type === "string" && members.length - 1 !== i) throw `${current}: the String type is vague, you must specify it at the last bound`; 217 | if (types.includes(fill.type)) throw `${current}: there can't be two bounds with the same type (${fill.type})`; 218 | types.push(fill.type); 219 | } 220 | 221 | toRet.push(fill); 222 | }); 223 | 224 | return toRet; 225 | } 226 | 227 | } 228 | 229 | module.exports = ParsedUsage; 230 | -------------------------------------------------------------------------------- /examples/SettingGateway.md: -------------------------------------------------------------------------------- 1 | # SettingGateway 2 | 3 | SettingGateway is an object-oriented and highly dynamic settings system designed to provide a full interface of settings, which works in a NoSQL environment but that it is also able to work with SQL. 4 | 5 | The concept of SettingGateway is designed to provide users a very useful interface for most things, for example, as in 0.20.6, you can create new instances of it, in which, each instance is able to handle a completely different schema and database. 6 | 7 | This system has been implemented in Komada **0.20.3**, after the PR [#255](https://github.com/dirigeants/komada/pull/255), still far from its concept level, it has been able to provide users a fully functional system that is able to work with any provider from [komada-pieces](https://github.com/dirigeants/komada-pieces), check the [Providers](https://dirigeants.github.io/komada/module-Provider.html) page for further information about them, and [SettingGateway](https://dirigeants.github.io/komada/SettingGateway.html)'s documentation. 8 | 9 | By default, Komada uses the [json](https://github.com/dirigeants/komada/blob/master/src/providers/json.js) provider by default, do not scream about it and insta-replace with SQLite, Komada's JSON provider writes the data [atomically](https://en.wikipedia.org/wiki/Atomicity_(database_systems)), in other words, it is very rare for the data to corrupt. 10 | 11 | However, as Komada works on a [NoSQL](https://en.wikipedia.org/wiki/NoSQL) environment, the data cannot be used directly, for that, you need a special set of methods that defines a [ProviderSQL](https://dirigeants.github.io/komada/module-ProviderSQL.html) which is required for the [SQL](https://dirigeants.github.io/komada/SQL.html) engine to work. 12 | 13 | ## Change the *provider's engine*. 14 | 15 | For example, let's say I have downloaded the *levelup* provider and I want to work with it, then we go to your main script file (`app.js`, `bot.js`..., wherever you declare the new Komada.Client), and write the following code: 16 | ```js 17 | provider: { engine: "levelup" } 18 | ``` 19 | 20 | Your Komada's configuration will look something like this: 21 | ```js 22 | const client = new Komada.Client({ 23 | ownerID: "", 24 | prefix: "k!", 25 | clientOptions: {}, 26 | provider: { engine: "levelup" }, 27 | }); 28 | 29 | client.login("..."); 30 | ``` 31 | And now, you're using levelup's provider to store the data from SettingGateway. 32 | 33 | What happens when I use an engine that does not exist as a provider? Simply, SettingGateway will throw an error, it is enough user-friendly and readable, if that happens, make sure you wrote the provider's name correctly. 34 | 35 | ## Add new 'keys' to the guild settings. 36 | 37 | As [`SettingGateway`](https://dirigeants.github.io/komada/SettingGateway.html) extends [`SchemaManager`](https://dirigeants.github.io/komada/SchemaManager.html), you can easily add new keys to your schema by simply calling `SettingGateway#add` (inherited from [`SchemaManager#add`](https://dirigeants.github.io/komada/SchemaManager.html#add)) by running this: 38 | 39 | ```js 40 | client.settings.guilds.add(key, options, force?); 41 | ``` 42 | 43 | Where: 44 | - `key` is the key's name to add, `String` type. 45 | - `options` is an object containing the options for the key, such as `type`, `default`, `sql`, `array`... 46 | - `force` (defaults to `true`) is whether SchemaManager should update all documents/rows to match the new schema, using the `options.default` value. 47 | 48 | For example, let's say I want to add a new settings key, called `modlogs`, which takes a channel. 49 | 50 | ```js 51 | client.settings.guilds.add("modlogs", { type: "TextChannel" }); 52 | ``` 53 | 54 | This will create a new settings key, called `modlogs`, and will take a `TextChannel` type. 55 | 56 | > The `TextChannel` type has been implemented in `0.20.7` as a critical security measurement to avoid server administrators to set up channels in the wrong type, for example, configuring the modlogs channels in a `VoiceChannel` one. 57 | 58 | > As in `0.20.7`, the force parameter defaults to `true` instead to `false`. It is also recommended to use it as it can avoid certain unwanted actions. 59 | 60 | But now, I want to add another key, with name of `users`, *so I can set a list of blacklisted users who won't be able to use commands*, which will take an array of Users. 61 | 62 | ```js 63 | client.settings.guilds.add("users", { type: "User", array: true }); 64 | ``` 65 | 66 | > `options.array` defaults to `false`, and when `options.default` is not specified, it defaults to `null`, however, when `options.array` is `true`, `options.default` defaults to `[]` (empty array). 67 | 68 | ## Editing keys from the guild settings. 69 | 70 | Now that I have a new key called `modlogs`, I want to configure it outside the `conf` command, how can we do this? 71 | 72 | ```js 73 | client.settings.guilds.update(msg.guild, { modlogs: "267727088465739778" }); 74 | ``` 75 | 76 | Check: [SettingGateway#update](https://dirigeants.github.io/komada/SettingGateway.html#update) 77 | 78 | > You can use a Channel instance, [SettingResolver](https://dirigeants.github.io/komada/SettingResolver.html) will make sure the input is valid and the database gets an **ID** and not an object. 79 | 80 | Now, I want to **add** a new user user to the `users` key, which takes an array. 81 | 82 | ```js 83 | client.settings.guilds.updateArray(msg.guild, "add", "users", "146048938242211840"); 84 | ``` 85 | 86 | That will add the user `"146048938242211840"` to the `users` array. To remove it: 87 | 88 | ```js 89 | client.settings.guilds.updateArray(msg.guild, "remove", "users", "146048938242211840"); 90 | ``` 91 | 92 | Check: [SettingGateway#updateArray](https://dirigeants.github.io/komada/SettingGateway.html#updateArray) 93 | 94 | ## Removing a key from the guild settings. 95 | 96 | I have a key which is useless for me, so I *want* to remove it from the schema. 97 | 98 | ```js 99 | client.settings.guilds.remove("users"); 100 | ``` 101 | 102 | > Do not confuse `SchemaManager#remove` and `SchemaManager#delete`, the first one deletes an entry from the schema, whereas the second deletes an entry for the selected key from the database. 103 | 104 | ## Add a key to the guild settings if it doesn't exist. 105 | 106 | In [Komada-Pieces](https://github.com/dirigeants/komada-pieces/), specially, some pieces require a key from the settings to work, however, the creator of the pieces does not know if the user who downloads the piece has it, so this function becomes is useful in this case. 107 | 108 | ```js 109 | async function() { 110 | if (!client.settings.guilds.schema.modlog) { 111 | await client.settings.guilds.add("modlog", { type: "TextChannel" }); 112 | } 113 | } 114 | ``` 115 | 116 | ## How can I create new SettingGateway instances? 117 | 118 | **1.** By using [SettingsCache](https://dirigeants.github.io/komada/SettingsCache.html), (available from `client.settings`). 119 | 120 | Let's say I want to add a new SettingGateway instance, called `users`, which input takes users, and stores a quote which is a string between 2 and 140 characters. 121 | 122 | ```js 123 | async function validate(resolver, user) { 124 | const result = await resolver.user(user); 125 | if (!result) throw "The parameter expects either a User ID or a User Object."; 126 | return result; 127 | }; 128 | 129 | const schema = { 130 | quote: { 131 | type: "String", 132 | default: null, 133 | array: false, 134 | min: 2, 135 | max: 140, 136 | }, 137 | }; 138 | 139 | client.settings.add("users", validate, schema); 140 | ``` 141 | 142 | > The `validate` function must be a [**function**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/function), not a [**Arrow Function**](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/Arrow_functions), the difference between them is that an arrow function binds `this` to wherever the function has been created (for example, the `exports` from your eval command, if you are doing this with eval), while the normal functions does not do this. 143 | 144 | > If the `validate` function does not resolve **Guild** type, you might want to use the third argument of `SettingGateway#update`, which takes a Guild resolvable. 145 | 146 | And then, you can access to it by: 147 | 148 | ```js 149 | client.settings.users; 150 | ``` 151 | 152 | **2.** By extending SettingGateway (you can use it in `require("komada").SettingGateway`), which is a bit hacky but gives you total freedom and customization, this method may not completely work and needs some knowledge, however, as this practise is not completely supported, nothing stops you from doing this. 153 | -------------------------------------------------------------------------------- /docs/tutorials/SettingGateway.md: -------------------------------------------------------------------------------- 1 | # SettingGateway 2 | 3 | SettingGateway is an object-oriented and highly dynamic settings system designed to provide a full interface of settings, which works in a NoSQL environment but that it is also able to work with SQL. 4 | 5 | The concept of SettingGateway is designed to provide users a very useful interface for most things, for example, as in 0.20.6, you can create new instances of it, in which, each instance is able to handle a completely different schema and database. 6 | 7 | This system has been implemented in Komada **0.20.3**, after the PR [#255](https://github.com/dirigeants/komada/pull/255), still far from its concept level, it has been able to provide users a fully functional system that is able to work with any provider from [komada-pieces](https://github.com/dirigeants/komada-pieces), check the [Providers](https://dirigeants.github.io/komada/module-Provider.html) page for further information about them, and [SettingGateway](https://dirigeants.github.io/komada/SettingGateway.html)'s documentation. 8 | 9 | By default, Komada uses the [json](https://github.com/dirigeants/komada/blob/master/src/providers/json.js) provider by default, do not scream about it and insta-replace with SQLite, Komada's JSON provider writes the data [atomically](https://en.wikipedia.org/wiki/Atomicity_(database_systems)), in other words, it is very rare for the data to corrupt. 10 | 11 | However, as Komada works on a [NoSQL](https://en.wikipedia.org/wiki/NoSQL) environment, the data cannot be used directly, for that, you need a special set of methods that defines a [ProviderSQL](https://dirigeants.github.io/komada/module-ProviderSQL.html) which is required for the [SQL](https://dirigeants.github.io/komada/SQL.html) engine to work. 12 | 13 | ## Change the *provider's engine*. 14 | 15 | For example, let's say I have downloaded the *levelup* provider and I want to work with it, then we go to your main script file (`app.js`, `bot.js`..., wherever you declare the new Komada.Client), and write the following code: 16 | ```js 17 | provider: { engine: "levelup" } 18 | ``` 19 | 20 | Your Komada's configuration will look something like this: 21 | ```js 22 | const client = new Komada.Client({ 23 | ownerID: "", 24 | prefix: "k!", 25 | clientOptions: {}, 26 | provider: { engine: "levelup" }, 27 | }); 28 | 29 | client.login("..."); 30 | ``` 31 | And now, you're using levelup's provider to store the data from SettingGateway. 32 | 33 | What happens when I use an engine that does not exist as a provider? Simply, SettingGateway will throw an error, it is enough user-friendly and readable, if that happens, make sure you wrote the provider's name correctly. 34 | 35 | ## Add new 'keys' to the guild settings. 36 | 37 | As [`SettingGateway`](https://dirigeants.github.io/komada/SettingGateway.html) extends [`SchemaManager`](https://dirigeants.github.io/komada/SchemaManager.html), you can easily add new keys to your schema by simply calling `SettingGateway#add` (inherited from [`SchemaManager#add`](https://dirigeants.github.io/komada/SchemaManager.html#add)) by running this: 38 | 39 | ```js 40 | client.settings.guilds.add(key, options, force?); 41 | ``` 42 | 43 | Where: 44 | - `key` is the key's name to add, `String` type. 45 | - `options` is an object containing the options for the key, such as `type`, `default`, `sql`, `array`... 46 | - `force` (defaults to `true`) is whether SchemaManager should update all documents/rows to match the new schema, using the `options.default` value. 47 | 48 | For example, let's say I want to add a new settings key, called `modlogs`, which takes a channel. 49 | 50 | ```js 51 | client.settings.guilds.add("modlogs", { type: "TextChannel" }); 52 | ``` 53 | 54 | This will create a new settings key, called `modlogs`, and will take a `TextChannel` type. 55 | 56 | > The `TextChannel` type has been implemented in `0.20.7` as a critical security measurement to avoid server administrators to set up channels in the wrong type, for example, configuring the modlogs channels in a `VoiceChannel` one. 57 | 58 | > As in `0.20.7`, the force parameter defaults to `true` instead to `false`. It is also recommended to use it as it can avoid certain unwanted actions. 59 | 60 | But now, I want to add another key, with name of `users`, *so I can set a list of blacklisted users who won't be able to use commands*, which will take an array of Users. 61 | 62 | ```js 63 | client.settings.guilds.add("users", { type: "User", array: true }); 64 | ``` 65 | 66 | > `options.array` defaults to `false`, and when `options.default` is not specified, it defaults to `null`, however, when `options.array` is `true`, `options.default` defaults to `[]` (empty array). 67 | 68 | ## Editing keys from the guild settings. 69 | 70 | Now that I have a new key called `modlogs`, I want to configure it outside the `conf` command, how can we do this? 71 | 72 | ```js 73 | client.settings.guilds.update(msg.guild, { modlogs: "267727088465739778" }); 74 | ``` 75 | 76 | Check: [SettingGateway#update](https://dirigeants.github.io/komada/SettingGateway.html#update) 77 | 78 | > You can use a Channel instance, [SettingResolver](https://dirigeants.github.io/komada/SettingResolver.html) will make sure the input is valid and the database gets an **ID** and not an object. 79 | 80 | Now, I want to **add** a new user user to the `users` key, which takes an array. 81 | 82 | ```js 83 | client.settings.guilds.updateArray(msg.guild, "add", "users", "146048938242211840"); 84 | ``` 85 | 86 | That will add the user `"146048938242211840"` to the `users` array. To remove it: 87 | 88 | ```js 89 | client.settings.guilds.updateArray(msg.guild, "remove", "users", "146048938242211840"); 90 | ``` 91 | 92 | Check: [SettingGateway#updateArray](https://dirigeants.github.io/komada/SettingGateway.html#updateArray) 93 | 94 | ## Removing a key from the guild settings. 95 | 96 | I have a key which is useless for me, so I *want* to remove it from the schema. 97 | 98 | ```js 99 | client.settings.guilds.remove("users"); 100 | ``` 101 | 102 | > Do not confuse `SchemaManager#remove` and `SchemaManager#delete`, the first one deletes an entry from the schema, whereas the second deletes an entry for the selected key from the database. 103 | 104 | ## Add a key to the guild settings if it doesn't exist. 105 | 106 | In [Komada-Pieces](https://github.com/dirigeants/komada-pieces/), specially, some pieces require a key from the settings to work, however, the creator of the pieces does not know if the user who downloads the piece has it, so this function becomes is useful in this case. 107 | 108 | ```js 109 | async function() { 110 | if (!client.settings.guilds.schema.modlog) { 111 | await client.settings.guilds.add("modlog", { type: "TextChannel" }); 112 | } 113 | } 114 | ``` 115 | 116 | ## How can I create new SettingGateway instances? 117 | 118 | **1.** By using [SettingsCache](https://dirigeants.github.io/komada/SettingsCache.html), (available from `client.settings`). 119 | 120 | Let's say I want to add a new SettingGateway instance, called `users`, which input takes users, and stores a quote which is a string between 2 and 140 characters. 121 | 122 | ```js 123 | async function validate(resolver, user) { 124 | const result = await resolver.user(user); 125 | if (!result) throw "The parameter expects either a User ID or a User Object."; 126 | return result; 127 | }; 128 | 129 | const schema = { 130 | quote: { 131 | type: "String", 132 | default: null, 133 | array: false, 134 | min: 2, 135 | max: 140, 136 | }, 137 | }; 138 | 139 | client.settings.add("users", validate, schema); 140 | ``` 141 | 142 | > The `validate` function must be a [**function**](https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Operators/function), not a [**Arrow Function**](https://developer.mozilla.org/en/docs/Web/JavaScript/Reference/Functions/Arrow_functions), the difference between them is that an arrow function binds `this` to wherever the function has been created (for example, the `exports` from your eval command, if you are doing this with eval), while the normal functions does not do this. 143 | 144 | > If the `validate` function does not resolve **Guild** type, you might want to use the third argument of `SettingGateway#update`, which takes a Guild resolvable. 145 | 146 | And then, you can access to it by: 147 | 148 | ```js 149 | client.settings.users; 150 | ``` 151 | 152 | **2.** By extending SettingGateway (you can use it in `require("komada").SettingGateway`), which is a bit hacky but gives you total freedom and customization, this method may not completely work and needs some knowledge, however, as this practise is not completely supported, nothing stops you from doing this. 153 | -------------------------------------------------------------------------------- /src/classes/console/Console.js: -------------------------------------------------------------------------------- 1 | const { Console } = require("console"); 2 | const Colors = require("./Colors"); 3 | const { inspect } = require("util"); 4 | const Timestamp = require("../../util/Timestamp"); 5 | 6 | /** 7 | * Komada's console class, extends NodeJS Console class. 8 | * 9 | */ 10 | class KomadaConsole extends Console { 11 | 12 | /** 13 | * Constructs our KomadaConsole instance 14 | * @param {boolean} [stdout=process.stdout] The location of standard output. Must be a writable stream. 15 | * @param {boolean} [stderr=process.stderr] The location of standrad error outputt. Must be a writable stream. 16 | * @param {boolean} [colors={}] The colors for this console instance. 17 | * @param {boolean} [timestamps=false] Whether or not Timestamps should be enabled. 18 | */ 19 | constructor({ stdout, stderr, useColor, colors = {}, timestamps = true }) { 20 | super(stdout, stderr); 21 | /** 22 | * The standard output stream for this console, defaulted to process.stderr. 23 | * @name KomadaConsole#stdout 24 | * @type {WritableStream} 25 | */ 26 | Object.defineProperty(this, "stdout", { value: stdout }); 27 | 28 | /** 29 | * The standard error output stream for this console, defaulted to process.stderr. 30 | * @name KomadaConsole#stderr 31 | * @type {WritableStream} 32 | */ 33 | Object.defineProperty(this, "stderr", { value: stderr }); 34 | 35 | /** 36 | * Whether or not timestamps should be enabled for this console. 37 | * @type {boolean} 38 | */ 39 | this.timestamps = timestamps; 40 | 41 | /** 42 | * Whether or not this console should use colors. 43 | * @type {boolean} 44 | */ 45 | this.useColors = typeof useColor === "undefined" ? this.stdout.isTTY || false : useColor; 46 | 47 | /** 48 | * The colors for this console. 49 | * @name KomadaConsole#colors 50 | * @type {boolean|Colors} 51 | */ 52 | this.colors = { 53 | debug: colors.debug || { message: { background: null, text: null, style: null }, time: { background: null, text: "lightmagenta", style: null } }, 54 | error: colors.error || { message: { background: null, text: null, style: null }, time: { background: "red", text: null, style: null } }, 55 | log: colors.log || { message: { background: null, text: null, style: null }, time: { background: null, text: "lightblue", style: null } }, 56 | verbose: colors.verbose || { message: { background: null, text: "gray", style: null }, time: { background: null, text: "gray", style: null } }, 57 | warn: colors.warn || { message: { background: null, text: null, style: null }, time: { background: "lightyellow", text: "black", style: null } }, 58 | wtf: colors.wtf || { message: { background: "red", text: null, style: ["bold", "underline"] }, time: { background: "red", text: null, style: ["bold", "underline"] } }, 59 | }; 60 | } 61 | 62 | /** 63 | * @memberof KomadaConsole 64 | * @typedef {object} Colors - Time is for the timestamp of the log, message is for the actual output. 65 | * @property {ColorObjects} debug An object containing a message and time color object. 66 | * @property {ColorObjects} error An object containing a message and time color object. 67 | * @property {ColorObjects} log An object containing a message and time color object. 68 | * @property {ColorObjects} verbose An object containing a message and time color object. 69 | * @property {ColorObjects} warn An object containing a message and time color object. 70 | * @property {ColorObjects} wtf An object containing a message and time Color Object. 71 | */ 72 | 73 | /** 74 | * @memberof KomadaConsole 75 | * @typedef {object} ColorObjects 76 | * @property {MessageObject} message A message object containing colors and styles. 77 | * @property {TimeObject} time A time object containing colors and styles. 78 | */ 79 | 80 | /** 81 | * @memberof KomadaConsole 82 | * @typedef {object} MessageObject 83 | * @property {BackgroundColorTypes} background The background color. Can be a basic string like "red", a hex string, or a RGB array. 84 | * @property {TextColorTypes} text The text color. Can be a basic string like "red", a hex string, or a RGB array. 85 | * @property {StyleTypes} style A style string from StyleTypes. 86 | */ 87 | 88 | /** 89 | * @memberof KomadaConsole 90 | * @typedef {object} TimeObject 91 | * @property {BackgroundColorTypes} background The background color. Can be a basic string like "red", a hex string, or a RGB array. 92 | * @property {TextColorTypes} text The text color. Can be a basic string like "red", a hex string, a RGB array, or HSL array. 93 | * @property {StyleTypes} style A style string from StyleTypes. 94 | */ 95 | 96 | /** 97 | * @memberof KomadaConsole 98 | * @typedef {*} TextColorTypes - All the valid color types. 99 | * @property {string} black 100 | * @property {string} red 101 | * @property {string} green 102 | * @property {string} yellow 103 | * @property {string} blue 104 | * @property {string} magenta 105 | * @property {string} cyan 106 | * @property {string} gray 107 | * @property {string} grey 108 | * @property {string} lightgray 109 | * @property {string} lightgrey 110 | * @property {string} lightred 111 | * @property {string} lightgreen 112 | * @property {string} lightyellow 113 | * @property {string} lightblue 114 | * @property {string} lightmagenta 115 | * @property {string} lightcyan 116 | * @property {string} white 117 | * @property {string} #008000 green 118 | * @property {Array} [255,0,0] red 119 | * @property {Array} [229,50%,50%] blue 120 | */ 121 | 122 | /** 123 | * @memberof KomadaConsole 124 | * @typedef {*} BackgroundColorTypes - One of these strings, HexStrings, RGB, or HSL are valid types. 125 | * @property {string} black 126 | * @property {string} red 127 | * @property {string} green 128 | * @property {string} blue 129 | * @property {string} magenta 130 | * @property {string} cyan 131 | * @property {string} gray 132 | * @property {string} grey 133 | * @property {string} lightgray 134 | * @property {string} lightgrey 135 | * @property {string} lightred 136 | * @property {string} lightgreen 137 | * @property {string} lightyellow 138 | * @property {string} lightblue 139 | * @property {string} lightmagenta 140 | * @property {string} lightcyan 141 | * @property {string} white 142 | * @property {string} #008000 green 143 | * @property {Array} [255,0,0] red 144 | * @property {Array} [229,50%,50%] blue 145 | */ 146 | 147 | /** 148 | * @memberof KomadaConsole 149 | * @typedef {*} StyleTypes 150 | * @property {string} normal 151 | * @property {string} bold 152 | * @property {string} dim 153 | * @property {string} italic 154 | * @property {string} underline 155 | * @property {string} inverse 156 | * @property {string} hidden 157 | * @property {string} strikethrough 158 | */ 159 | 160 | /** 161 | * Logs everything to the console/writable stream. 162 | * @param {*} stuff The stuff we want to print. 163 | * @param {string} [type="log"] The type of log, particularly useful for coloring. 164 | */ 165 | log(stuff, type = "log") { 166 | stuff = KomadaConsole.flatten(stuff, this.useColors); 167 | const message = this.colors ? this.colors[type.toLowerCase()].message : {}; 168 | const time = this.colors ? this.colors[type.toLowerCase()].time : {}; 169 | const timestamp = this.timestamps ? `${this.timestamp(`[${Timestamp.format(new Date())}]`, time)} ` : ""; 170 | if (this[`_${type}`]) { 171 | this[`_${type}`](stuff.split("\n").map(str => `${timestamp}${this.messages(str, message)}`).join("\n")); 172 | } else { 173 | super.log(stuff.split("\n").map(str => `${timestamp}${this.messages(str, message)}`).join("\n")); 174 | } 175 | } 176 | 177 | /** 178 | * Print something to console as a simple log. 179 | * @param {*} stuff The stuff to log 180 | */ 181 | _log(stuff) { 182 | super.log(stuff); 183 | } 184 | 185 | /** 186 | * Print something to console as an error. 187 | * @param {*} stuff The stuff to log 188 | */ 189 | _error(stuff) { 190 | super.error(stuff); 191 | } 192 | 193 | /** 194 | * Print something to console as a "WTF" error. 195 | * @param {*} stuff The stuff to log 196 | */ 197 | _wtf(stuff) { 198 | super.error(stuff); 199 | } 200 | 201 | /** 202 | * Print something to console as a debug log. 203 | * @param {*} stuff The stuff to log 204 | */ 205 | _debug(stuff) { 206 | super.log(stuff); 207 | } 208 | 209 | /** 210 | * Print something to console as a verbose log. 211 | * @param {*} stuff The stuff to log 212 | */ 213 | _verbose(stuff) { 214 | super.log(stuff); 215 | } 216 | 217 | /** 218 | * Print something to console as a warning. 219 | * @param {*} stuff The stuff to log 220 | */ 221 | _warn(stuff) { 222 | super.log(stuff); 223 | } 224 | 225 | timestamp(timestamp, time) { 226 | if (!this.useColors) return timestamp; 227 | return `${Colors.format(timestamp, time)} `; 228 | } 229 | 230 | messages(string, message) { 231 | if (!this.useColors) return string; 232 | return Colors.format(string, message); 233 | } 234 | 235 | /** 236 | * Flattens our data into a readable string. 237 | * @param {*} data Some data to flatten 238 | * @param {boolean} useColors Whether or not the inspection should color the output 239 | * @return {string} 240 | */ 241 | static flatten(data, useColors) { 242 | data = data.stack || data.message || data; 243 | if (typeof data === "object" && typeof data !== "string" && !Array.isArray(data)) data = inspect(data, { depth: 0, colors: useColors }); 244 | if (Array.isArray(data)) data = data.join("\n"); 245 | return data; 246 | } 247 | 248 | } 249 | 250 | module.exports = KomadaConsole; 251 | -------------------------------------------------------------------------------- /src/classes/settings/Gateway.js: -------------------------------------------------------------------------------- 1 | const SQL = require("../sql"); 2 | 3 | /** 4 | * The gateway for this settings instance. The gateway handles all the creation and setting of non-default entries, along with saving. 5 | */ 6 | 7 | class Gateway { 8 | 9 | /** 10 | * Constructs our instance of Gateway 11 | * @param {any} settings The settings that created this gateway. 12 | * @param {any} validateFunction The validation function used to validate user input. 13 | */ 14 | constructor(settings, validateFunction) { 15 | /** 16 | * The Settings class that this gateway is a part of. 17 | * @name Gateway.settings 18 | * @type {Settings} 19 | * @readonly 20 | */ 21 | Object.defineProperty(this, "settings", { value: settings }); 22 | 23 | /** 24 | * The provider engine that will handle saving and getting all data for this instance. 25 | * @type {string} 26 | */ 27 | this.engine = this.client.config.provider.engine; 28 | 29 | if (!this.provider) throw `This provider(${this.engine}) does not exist in your system.`; 30 | 31 | /** 32 | * If the provider is SQL, this property will ensure data is serialized and deserialized. 33 | * @type {string} 34 | */ 35 | this.sql = this.provider.conf.sql ? new SQL(this.client, this) : null; 36 | 37 | /** 38 | * The function validator for this gateway. 39 | * @type {function} 40 | */ 41 | this.validate = validateFunction; 42 | } 43 | 44 | /** 45 | * Initializes the gateway, creating tables, ensuring the schema exists, and caching values. 46 | * @param {Schema} schema The Schema object, validated from settings. 47 | * @returns {void} 48 | */ 49 | async init(schema) { 50 | if (!(await this.provider.hasTable(this.type))) await this.provider.createTable(this.type, this.sql ? this.sql.buildSQLSchema(schema) : undefined); 51 | const data = await this.provider.getAll(this.type); 52 | if (this.sql) { 53 | this.sql.initDeserialize(); 54 | for (let i = 0; i < data.length; i++) this.sql.deserializer(data[i]); 55 | } 56 | for (const key of data) this.cache.set(this.type, key.id, key); // eslint-disable-line 57 | } 58 | 59 | /** 60 | * Creates a new entry in the cache. 61 | * @param {Object|string} input An object containing a id property, like discord.js objects, or a string. 62 | */ 63 | async create(input) { 64 | const target = await this.validate(input).then(output => (output.id || output)); 65 | await this.provider.create(this.type, target, this.schema.defaults); 66 | this.cache.set(this.type, target, this.schema.defaults); 67 | } 68 | 69 | /** 70 | * Removes an entry from the cache. 71 | * @param {Object|string} input An object containing a id property, like discord.js objects, or a string. 72 | */ 73 | async destroy(input) { 74 | const target = await this.validate(input).then(output => (output.id || output)); 75 | await this.provider.delete(this.type, target); 76 | this.cache.delete(this.type, target); 77 | } 78 | 79 | /** 80 | * Gets an entry from the cache 81 | * @param {string} input The key you are you looking for. 82 | * @returns {Schema} 83 | */ 84 | get(input) { 85 | return input !== "default" ? this.cache.get(this.type, input) || this.schema.defaults : this.schema.defaults; 86 | } 87 | 88 | /** 89 | * Sync either all entries from the provider, or a single one. 90 | * @param {Object|string} [input=null] An object containing a id property, like discord.js objects, or a string. 91 | * @returns {void} 92 | */ 93 | async sync(input = null) { 94 | if (!input) { 95 | const data = await this.provider.getAll(this.type); 96 | if (this.sql) for (let i = 0; i < data.length; i++) this.sql.deserializer(data[i]); 97 | for (const key of data) this.cache.set(this.type, key.id, key); // eslint-disable-line 98 | return; 99 | } 100 | const target = await this.validate(input).then(output => (output.id || output)); 101 | const data = await this.provider.get(this.type, target); 102 | if (this.sql) this.sql.deserializer(data); 103 | await this.cache.set(this.type, target, data); 104 | } 105 | 106 | /** 107 | * Reset a key's value to default from a entry. 108 | * @param {Object|string} input An object containing a id property, like Discord.js objects, or a string. 109 | * @param {string} key The key to reset. 110 | * @returns {any} 111 | */ 112 | async reset(input, key) { 113 | const target = await this.validate(input).then(output => (output.id || output)); 114 | if (!(key in this.schema)) throw `The key ${key} does not exist in the current data schema.`; 115 | const defaultKey = this.schema[key].default; 116 | await this.provider.update(this.type, target, { [key]: defaultKey }); 117 | this.sync(target); 118 | return defaultKey; 119 | } 120 | 121 | /** 122 | * Updates an entry. 123 | * @param {Object|string} input An object or string that can be parsed by this instance's resolver. 124 | * @param {Object} object An object with pairs of key/value to update. 125 | * @param {Object|string} [guild=null] A Guild resolvable, useful for when the instance of SG doesn't aim for Guild settings. 126 | * @returns {Object} 127 | */ 128 | async update(input, object, guild = null) { 129 | const target = await this.validate(input).then(output => output.id || output); 130 | guild = await this.resolver.guild(guild || target); 131 | 132 | const resolved = await Promise.all(Object.entries(object).map(async ([key, value]) => { 133 | if (!(key in this.schema)) throw `The key ${key} does not exist in the current data schema.`; 134 | return this.resolver[this.schema[key].type.toLowerCase()](value, guild, this.schema[key]) 135 | .then(res => ({ [key]: res.id || res })); 136 | })); 137 | 138 | const result = Object.assign({}, ...resolved); 139 | 140 | await this.ensureCreate(target); 141 | await this.provider.update(this.type, target, result); 142 | await this.sync(target); 143 | return result; 144 | } 145 | 146 | /** 147 | * Creates the settings if it did not exist previously. 148 | * @param {Object|string} target An object or string that can be parsed by this instance's resolver. 149 | * @returns {true} 150 | */ 151 | async ensureCreate(target) { 152 | if (typeof target !== "string") throw `Expected input type string, got ${typeof target}`; 153 | let exists = this.cache.has(this.type, target); 154 | if (exists instanceof Promise) exists = await exists; 155 | if (exists === false) return this.create(target); 156 | return true; 157 | } 158 | 159 | /** 160 | * Update an array from the a Guild's configuration. 161 | * @param {Object|string} input An object containing a id property, like discord.js objects, or a string. 162 | * @param {string} type Either 'add' or 'remove'. 163 | * @param {string} key The key from the Schema. 164 | * @param {any} data The value to be added or removed. 165 | * @param {Object|string} [guild=null] The guild for this setting, useful for when the settings aren't aimed for guilds 166 | * @returns {boolean} 167 | */ 168 | async updateArray(input, type, key, data, guild = null) { 169 | if (!["add", "remove"].includes(type)) throw "The type parameter must be either add or remove."; 170 | if (!(key in this.schema)) throw `The key ${key} does not exist in the current data schema.`; 171 | if (!this.schema[key].array) throw `The key ${key} is not an Array.`; 172 | if (data === undefined) throw "You must specify the value to add or filter."; 173 | const target = await this.validate(input).then(output => (output.id || output)); 174 | guild = await this.resolver.guild(guild || target); 175 | let result = await this.resolver[this.schema[key].type.toLowerCase()](data, guild, this.schema[key]); 176 | if (result.id) result = result.id; 177 | let cache = this.cache.get(this.type, target); 178 | if (cache instanceof Promise) cache = await cache; 179 | if (type === "add") { 180 | if (cache[key].includes(result)) throw `The value ${data} for the key ${key} already exists.`; 181 | cache[key].push(result); 182 | await this.provider.update(this.type, target, { [key]: cache[key] }); 183 | await this.sync(target); 184 | return result; 185 | } 186 | if (!cache[key].includes(result)) throw `The value ${data} for the key ${key} does not exist.`; 187 | cache[key] = cache[key].filter(v => v !== result); 188 | 189 | await this.ensureCreate(target); 190 | await this.provider.update(this.type, target, { [key]: cache[key] }); 191 | await this.sync(target); 192 | return true; 193 | } 194 | 195 | /** 196 | * The client this SettingGateway was created with. 197 | * @type {KomadaClient} 198 | * @readonly 199 | */ 200 | get client() { 201 | return this.settings.client; 202 | } 203 | 204 | /** 205 | * The resolver instance this SettingGateway uses to parse the data. 206 | * @type {Resolver} 207 | * @readonly 208 | */ 209 | get resolver() { 210 | return this.settings.resolver; 211 | } 212 | 213 | /** 214 | * The provider this SettingGateway instance uses for the persistent data operations. 215 | * @type {Provider} 216 | * @readonly 217 | */ 218 | get provider() { 219 | return this.client.providers.get(this.engine); 220 | } 221 | 222 | /** 223 | * The schema this gateway instance is handling. 224 | * @type {Schema} 225 | * @readonly 226 | */ 227 | get schema() { 228 | return this.settings.schema; 229 | } 230 | 231 | /** 232 | * The cache created with this instance 233 | * @type {Cache} 234 | * @readonly 235 | */ 236 | 237 | get cache() { 238 | return this.settings.cache; 239 | } 240 | 241 | /** 242 | * The type of settings (or name). 243 | * @type {string} 244 | * @readonly 245 | */ 246 | get type() { 247 | return this.settings.type; 248 | } 249 | 250 | } 251 | 252 | module.exports = Gateway; 253 | --------------------------------------------------------------------------------