diff --git a/config.example.js b/config.example.js index d904719..0d108d8 100644 --- a/config.example.js +++ b/config.example.js @@ -13,7 +13,7 @@ module.exports = { readInactive: 30, // How long a channel should be inactive for in the read category before moving to graveyard channelSweepInterval: 10, // How often channel transitions should be processed in minutes saveInterval: 1, // How often modmail history should be written to file in minutes - evalAccess: [], // Array of IDs that should have access to the bot's eval function + sudo: [], // Array of IDs (user or role) that have elevated access to the bot, i.e. eval, disable and any other elevated permission commands anonColor: 0, // A colour value, 0 will default to the bot's highest coloured role modmailReminderInterval: 10, // How often the bot should send a reminder of x new modmails in queue modmailReminderChannel: '', // channel to send reminders in @@ -32,7 +32,7 @@ module.exports = { } }, loggerOptions: { // This is for logging errors to a discord webhook - webhook: { // If you're not using the webhook, disable it + webhook: { // If you're not using the webhook, disable it disabled: true, id: '', token: '' diff --git a/structure/Util.js b/structure/Util.js new file mode 100644 index 0000000..09d6c82 --- /dev/null +++ b/structure/Util.js @@ -0,0 +1,158 @@ +const moment = require('moment'); +const path = require('path'); +const fs = require('fs'); +const fetch = require('node-fetch'); +const { Util: DiscordUtil } = require('discord.js'); + +class Util { + + constructor () { + throw new Error("Class may not be instantiated."); + } + + static paginate (items, page = 1, pageLength = 10) { + const maxPage = Math.ceil(items.length / pageLength); + if (page < 1) page = 1; + if (page > maxPage) page = maxPage; + const startIndex = (page - 1) * pageLength; + return { + items: items.length > pageLength ? items.slice(startIndex, startIndex + pageLength) : items, + page, + maxPage, + pageLength + }; + } + + static arrayIncludesAny (target, compareTo = []) { + + if (!(compareTo instanceof Array)) compareTo = [ compareTo ]; + for (const elem of compareTo) { + if (target.includes(elem)) return true; + } + return false; + + } + + static downloadAsBuffer (source) { + return new Promise((resolve, reject) => { + fetch(source).then((res) => { + if (res.ok) resolve(res.buffer()); + else reject(res.statusText); + }); + }); + } + + static readdirRecursive (directory) { + + const result = []; + + // eslint-disable-next-line no-shadow + (function read (directory) { + const files = fs.readdirSync(directory); + for (const file of files) { + const filePath = path.join(directory, file); + + if (fs.statSync(filePath).isDirectory()) { + read(filePath); + } else { + result.push(filePath); + } + } + }(directory)); + + return result; + + } + + static wait (ms) { + return this.delayFor(ms); + } + + static delayFor (ms) { + return new Promise((resolve) => { + setTimeout(resolve, ms); + }); + } + + static escapeMarkdown (text, options) { + if (typeof text !== 'string') return text; + return DiscordUtil.escapeMarkdown(text, options); + } + + static get formattingPatterns () { + return [ + [ '\\*{1,3}([^*]*)\\*{1,3}', '$1' ], + [ '_{1,3}([^_]*)_{1,3}', '$1' ], + [ '`{1,3}([^`]*)`{1,3}', '$1' ], + [ '~~([^~])~~', '$1' ] + ]; + } + + static removeMarkdown (content) { + if (!content) throw new Error('Missing content'); + this.formattingPatterns.forEach(([ pattern, replacer ]) => { + content = content.replace(new RegExp(pattern, 'gu'), replacer); + }); + return content.trim(); + } + + /** + * Sanitise user given regex; escapes unauthorised characters + * + * @static + * @param {string} input + * @param {string[]} [allowed=['?', '\\', '(', ')', '|']] + * @return {string} The sanitised expression + * @memberof Util + */ + static sanitiseRegex (input, allowed = [ '?', '\\', '(', ')', '|' ]) { + if (!input) throw new Error('Missing input'); + const reg = new RegExp(`[${this.regChars.filter((char) => !allowed.includes(char)).join('')}]`, 'gu'); + return input.replace(reg, '\\$&'); + } + + static get regChars () { + return [ '.', '+', '*', '?', '\\[', '\\]', '^', '$', '(', ')', '{', '}', '|', '\\\\', '-' ]; + } + + static escapeRegex (string) { + if (typeof string !== 'string') { + throw new Error("Invalid type sent to escapeRegex."); + } + + return string + .replace(/[|\\{}()[\]^$+*?.]/gu, '\\$&') + .replace(/-/gu, '\\x2d'); + } + + static duration (seconds) { + const { plural } = this; + let s = 0, + m = 0, + h = 0, + d = 0, + w = 0; + s = Math.floor(seconds); + m = Math.floor(s / 60); + s %= 60; + h = Math.floor(m / 60); + m %= 60; + d = Math.floor(h / 24); + h %= 24; + w = Math.floor(d / 7); + d %= 7; + return `${w ? `${w} ${plural(w, 'week')} ` : ''}${d ? `${d} ${plural(d, 'day')} ` : ''}${h ? `${h} ${plural(h, 'hour')} ` : ''}${m ? `${m} ${plural(m, 'minute')} ` : ''}${s ? `${s} ${plural(s, 'second')} ` : ''}`.trim(); + } + + static plural (amt, word) { + if (amt === 1) return word; + return `${word}s`; + } + + static get date () { + return moment().format("YYYY-MM-DD HH:mm:ss"); + } + +} + +module.exports = Util; \ No newline at end of file diff --git a/structure/commands/Disable.js b/structure/commands/Disable.js index 612264d..33f7ba1 100644 --- a/structure/commands/Disable.js +++ b/structure/commands/Disable.js @@ -1,4 +1,5 @@ const Command = require('../Command'); +const Util = require('../Util'); class Ping extends Command { @@ -9,7 +10,12 @@ class Ping extends Command { }); } - async execute ({ _caller }, { clean }) { + async execute ({ author, member, _caller }, { clean }) { + + + const { sudo } = this.client._options; + const roleIds = member.roles.cache.map(r => r.id); + if (!Util.arrayIncludesAny(roleIds, sudo) && !sudo.includes(author.id)) return; if (_caller === 'enable') this.client.modmail.enable(); else this.client.modmail.disable(clean); diff --git a/structure/commands/Eval.js b/structure/commands/Eval.js index ce7418f..13ea942 100644 --- a/structure/commands/Eval.js +++ b/structure/commands/Eval.js @@ -2,6 +2,7 @@ const { inspect } = require('util'); const { username } = require('os').userInfo(); const Command = require('../Command'); +const Util = require('../Util'); class Eval extends Command { @@ -14,8 +15,10 @@ class Eval extends Command { async execute (message, { clean }) { - if (!this.client._options.evalAccess.includes(message.author.id)) return; + const { sudo } = this.client._options; const { guild, author, member, client, channel } = message; // eslint-disable-line no-unused-vars + const roleIds = member.roles.cache.map(r => r.id); + if (!Util.arrayIncludesAny(roleIds, sudo) && !sudo.includes(author.id)) return; try { let evaled = eval(clean); // eslint-disable-line no-eval diff --git a/structure/commands/Logs.js b/structure/commands/Logs.js index 6ca571d..e625378 100644 --- a/structure/commands/Logs.js +++ b/structure/commands/Logs.js @@ -1,4 +1,5 @@ const Command = require('../Command'); +const Util = require('../Util'); class Logs extends Command { @@ -27,7 +28,7 @@ class Logs extends Command { const { member, channel } = message; const history = await this.client.cache.loadModmailHistory(user.id); if (!history.length) return 'Not found in modmail DB'; - const page = this.paginate([ ...history ].filter((e) => !('readState' in e)).reverse(), pageNr, 10); + const page = Util.paginate([ ...history ].filter((e) => !('readState' in e)).reverse(), pageNr, 10); const embed = { author: { @@ -61,19 +62,6 @@ class Logs extends Command { } - paginate (items, page = 1, pageLength = 10) { - const maxPage = Math.ceil(items.length / pageLength); - if (page < 1) page = 1; - if (page > maxPage) page = maxPage; - const startIndex = (page - 1) * pageLength; - return { - items: items.length > pageLength ? items.slice(startIndex, startIndex + pageLength) : items, - page, - maxPage, - pageLength - }; - } - } module.exports = Logs; \ No newline at end of file