342 lines
9.9 KiB
JavaScript
342 lines
9.9 KiB
JavaScript
const moment = require('moment');
|
||
const humaniseDuration = require('humanize-duration');
|
||
const path = require('path');
|
||
const fs = require('fs');
|
||
const fetch = require('node-fetch');
|
||
const { Util: DiscordUtil } = require('discord.js');
|
||
|
||
const Constants = {
|
||
QuotePairs: {
|
||
'"': '"', //regular double
|
||
"'": "'", //regular single
|
||
"‘": "’", //smart single
|
||
"“": "”" //smart double
|
||
}
|
||
};
|
||
|
||
const StartQuotes = Object.keys(Constants.QuotePairs);
|
||
const QuoteMarks = StartQuotes + Object.values(Constants.QuotePairs);
|
||
|
||
const humaniser = humaniseDuration.humanizer({
|
||
language: 'short',
|
||
languages: {
|
||
short: {
|
||
y: () => "y",
|
||
mo: () => "mo",
|
||
w: () => "w",
|
||
d: () => "d",
|
||
h: () => "h",
|
||
m: () => "m",
|
||
s: () => "s",
|
||
ms: () => "ms"
|
||
}
|
||
}
|
||
});
|
||
|
||
const has = (o, k) => Object.prototype.hasOwnProperty.call(o, k);
|
||
|
||
class Util {
|
||
|
||
constructor() {
|
||
throw new Error("Class may not be instantiated.");
|
||
}
|
||
|
||
/**
|
||
* Capitalise the first letter of the string
|
||
* @param {string} str The string to capitalise
|
||
* @returns {string} The capitalised string
|
||
*/
|
||
static capitalise(str) {
|
||
const first = str[0].toUpperCase();
|
||
return `${first}${str.substring(1)}`;
|
||
}
|
||
|
||
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 downloadAsBuffer(source) {
|
||
return new Promise((resolve, reject) => {
|
||
fetch(source).then((res) => {
|
||
if (res.ok) resolve(res.buffer());
|
||
else reject(res.statusText);
|
||
});
|
||
});
|
||
}
|
||
|
||
/**
|
||
* Read directory recursively and return all file paths
|
||
* @static
|
||
* @param {string} directory Full path to target directory
|
||
* @param {boolean} [ignoreDotfiles=true]
|
||
* @return {string[]} Array with the paths to the files within the directory
|
||
* @memberof Util
|
||
*/
|
||
static readdirRecursive(directory, ignoreDotfiles = true) {
|
||
|
||
const result = [];
|
||
|
||
(function read(directory) {
|
||
const files = fs.readdirSync(directory);
|
||
for (const file of files) {
|
||
if (file.startsWith('.') && ignoreDotfiles) continue;
|
||
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);
|
||
}
|
||
|
||
/**
|
||
* Markdown formatting characters
|
||
*/
|
||
static get formattingPatterns() {
|
||
return [
|
||
['\\*{1,3}([^*]*)\\*{1,3}', '$1'],
|
||
['_{1,3}([^_]*)_{1,3}', '$1'],
|
||
['`{1,3}([^`]*)`{1,3}', '$1'],
|
||
['~~([^~])~~', '$1']
|
||
];
|
||
}
|
||
|
||
/**
|
||
* Strips markdown from given text
|
||
* @static
|
||
* @param {string} content
|
||
* @returns {string}
|
||
*/
|
||
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, '\\$&');
|
||
}
|
||
|
||
/**
|
||
* RegEx characters
|
||
*/
|
||
static get regChars() {
|
||
return ['.', '+', '*', '?', '\\[', '\\]', '^', '$', '(', ')', '{', '}', '|', '\\\\', '-'];
|
||
}
|
||
|
||
/**
|
||
* Escape RegEx characters, prefix them with a backslash
|
||
* @param {string} string String representation of the regular expression
|
||
* @returns {string} Sanitised RegEx string
|
||
*/
|
||
static escapeRegex(string) {
|
||
if(typeof string !== 'string') {
|
||
throw new Error("Invalid type sent to escapeRegex.");
|
||
}
|
||
|
||
return string
|
||
.replace(/[|\\{}()[\]^$+*?.]/gu, '\\$&')
|
||
.replace(/-/gu, '\\x2d');
|
||
}
|
||
|
||
/* By Qwerasd#5202 */
|
||
// I'm not entirely sure how this works, except for the fact that it loops through each CHARACTER and tries to match quotes together.
|
||
// Supposedly quicker than regex, and I'd agree with that statement. Big, messy, but quick. Also lets you know if the grouped word(s) was a quote or not, which is useful for non-spaced quotes e.g. "Test" vs. "Test Test"
|
||
static parseQuotes(string) {
|
||
if (!string) return [];
|
||
|
||
let quoted = false,
|
||
wordStart = true,
|
||
startQuote = '',
|
||
endQuote = false,
|
||
isQuote = false,
|
||
word = '';
|
||
|
||
const words = [],
|
||
chars = string.split('');
|
||
|
||
chars.forEach((char) => {
|
||
if ((/\s/u).test(char)) {
|
||
if (endQuote) {
|
||
quoted = false;
|
||
endQuote = false;
|
||
isQuote = true;
|
||
}
|
||
if (quoted) {
|
||
word += char;
|
||
} else if (word !== '') {
|
||
words.push([word, isQuote]);
|
||
isQuote = false;
|
||
startQuote = '';
|
||
word = '';
|
||
wordStart = true;
|
||
}
|
||
} else if (QuoteMarks.includes(char)) {
|
||
if (endQuote) {
|
||
word += endQuote;
|
||
endQuote = false;
|
||
}
|
||
if (quoted) {
|
||
if (char === Constants.QuotePairs[startQuote]) {
|
||
endQuote = char;
|
||
} else {
|
||
word += char;
|
||
}
|
||
} else if (wordStart && StartQuotes.includes(char)) {
|
||
quoted = true;
|
||
startQuote = char;
|
||
} else {
|
||
word += char;
|
||
}
|
||
} else {
|
||
if (endQuote) {
|
||
word += endQuote;
|
||
endQuote = false;
|
||
}
|
||
word += char;
|
||
wordStart = false;
|
||
}
|
||
});
|
||
|
||
if (endQuote) {
|
||
words.push([word, true]);
|
||
} else {
|
||
word.split(/\s/u).forEach((subWord, i) => {
|
||
if (i === 0) {
|
||
words.push([startQuote + subWord, false]);
|
||
} else {
|
||
words.push([subWord, false]);
|
||
}
|
||
});
|
||
}
|
||
|
||
return words;
|
||
|
||
}
|
||
|
||
/**
|
||
* Make seconds human readable
|
||
* @static
|
||
* @param {Number} seconds
|
||
* @param {boolean} [relative=false] Whether to append "ago"
|
||
* @return {String} Humanised format
|
||
* @memberof Util
|
||
*/
|
||
static humanise(seconds, relative = false) {
|
||
return humaniser(seconds * 1000, { largest: 2 }) + (relative ? ' ago' : '');
|
||
}
|
||
|
||
/**
|
||
* Convert seconds to a human readable string representation
|
||
* @param {int} seconds
|
||
* @returns {string}
|
||
*/
|
||
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");
|
||
}
|
||
|
||
static makePlainError(err) {
|
||
return {
|
||
name: err.name,
|
||
message: err.message,
|
||
stack: err.stack
|
||
};
|
||
}
|
||
|
||
static mergeDefault(def, given) {
|
||
if (!given) return def;
|
||
for (const key in def) {
|
||
if (!has(given, key) || given[key] === undefined) {
|
||
given[key] = def[key];
|
||
} else if (given[key] === Object(given[key])) {
|
||
given[key] = Util.mergeDefault(def[key], given[key]);
|
||
}
|
||
}
|
||
return given;
|
||
}
|
||
|
||
//Shard Managing
|
||
static fetchRecommendedShards(token, guildsPerShard = 1000) {
|
||
if (!token) throw new Error("[util] Token missing.");
|
||
return fetch("https://discord.com/api/v7/gateway/bot", {
|
||
method: 'GET',
|
||
headers: { Authorization: `Bot ${token.replace(/^Bot\s*/iu, '')}` }
|
||
}).then((res) => {
|
||
if (res.ok) return res.json();
|
||
throw res;
|
||
})
|
||
.then((data) => data.shards * (1000 / guildsPerShard));
|
||
}
|
||
|
||
}
|
||
|
||
module.exports = Util; |