galactic-bot/src/Util.js
2022-01-16 22:26:52 +02:00

342 lines
9.9 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters

This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.

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;