2019-01-15 03:04:47 +02:00
|
|
|
import at from "lodash.at";
|
2019-01-12 13:42:11 +02:00
|
|
|
import { Emoji, Guild, GuildAuditLogEntry, TextableChannel } from "eris";
|
2018-07-31 02:42:45 +03:00
|
|
|
import url from "url";
|
|
|
|
import tlds from "tlds";
|
|
|
|
import emojiRegex from "emoji-regex";
|
2018-07-08 13:57:27 +03:00
|
|
|
|
2019-01-15 03:04:47 +02:00
|
|
|
import fs from "fs";
|
|
|
|
const fsp = fs.promises;
|
|
|
|
|
|
|
|
import https from "https";
|
|
|
|
import tmp from "tmp";
|
|
|
|
|
2018-07-08 13:57:27 +03:00
|
|
|
/**
|
|
|
|
* Turns a "delay string" such as "1h30m" to milliseconds
|
|
|
|
* @param {String} str
|
|
|
|
* @returns {Number}
|
|
|
|
*/
|
|
|
|
export function convertDelayStringToMS(str) {
|
|
|
|
const regex = /^([0-9]+)\s*([dhms])?[a-z]*\s*/;
|
|
|
|
let match;
|
|
|
|
let ms = 0;
|
|
|
|
|
|
|
|
str = str.trim();
|
|
|
|
|
|
|
|
// tslint:disable-next-line
|
|
|
|
while (str !== "" && (match = str.match(regex)) !== null) {
|
|
|
|
if (match[2] === "d") ms += match[1] * 1000 * 60 * 60 * 24;
|
|
|
|
else if (match[2] === "h") ms += match[1] * 1000 * 60 * 60;
|
|
|
|
else if (match[2] === "s") ms += match[1] * 1000;
|
|
|
|
else if (match[2] === "m" || !match[2]) ms += match[1] * 1000 * 60;
|
|
|
|
|
|
|
|
str = str.slice(match[0].length);
|
|
|
|
}
|
|
|
|
|
|
|
|
// Invalid delay string
|
|
|
|
if (str !== "") {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
|
|
|
|
return ms;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function successMessage(str) {
|
|
|
|
return `👌 ${str}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function errorMessage(str) {
|
2018-07-14 20:56:30 +03:00
|
|
|
return `⚠ ${str}`;
|
2018-07-08 13:57:27 +03:00
|
|
|
}
|
|
|
|
|
|
|
|
export function uclower(str) {
|
|
|
|
return str[0].toLowerCase() + str.slice(1);
|
|
|
|
}
|
2018-07-09 02:48:36 +03:00
|
|
|
|
2018-07-29 18:46:49 +03:00
|
|
|
export function stripObjectToScalars(obj, includedNested: string[] = []) {
|
2018-07-09 02:48:36 +03:00
|
|
|
const result = {};
|
|
|
|
|
|
|
|
for (const key in obj) {
|
|
|
|
if (
|
|
|
|
obj[key] == null ||
|
|
|
|
typeof obj[key] === "string" ||
|
|
|
|
typeof obj[key] === "number" ||
|
|
|
|
typeof obj[key] === "boolean"
|
|
|
|
) {
|
|
|
|
result[key] = obj[key];
|
|
|
|
} else if (typeof obj[key] === "object") {
|
|
|
|
const prefix = `${key}.`;
|
|
|
|
const nestedNested = includedNested
|
|
|
|
.filter(p => p === key || p.startsWith(prefix))
|
|
|
|
.map(p => (p === key ? p : p.slice(prefix.length)));
|
|
|
|
|
|
|
|
if (nestedNested.length) {
|
|
|
|
result[key] = stripObjectToScalars(obj[key], nestedNested);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return result;
|
|
|
|
}
|
|
|
|
|
|
|
|
const stringFormatRegex = /{([^{}]+?)}/g;
|
|
|
|
export function formatTemplateString(str: string, values) {
|
2018-08-05 01:32:59 +03:00
|
|
|
return str.replace(stringFormatRegex, (match, prop) => {
|
|
|
|
const value = at(values, prop)[0];
|
2018-07-31 02:42:45 +03:00
|
|
|
return typeof value === "string" || typeof value === "number" ? String(value) : "";
|
2018-07-09 02:48:36 +03:00
|
|
|
});
|
|
|
|
}
|
2018-07-29 15:18:26 +03:00
|
|
|
|
2018-12-15 23:01:45 +02:00
|
|
|
export const snowflakeRegex = /[1-9][0-9]{5,19}/;
|
|
|
|
|
2019-02-09 14:36:31 +02:00
|
|
|
const isSnowflakeRegex = new RegExp(`^${snowflakeRegex.source}$`);
|
2018-07-29 15:18:26 +03:00
|
|
|
export function isSnowflake(v: string): boolean {
|
2018-12-15 23:01:45 +02:00
|
|
|
return isSnowflakeRegex.test(v);
|
2018-07-29 15:18:26 +03:00
|
|
|
}
|
2018-07-29 18:46:49 +03:00
|
|
|
|
2018-07-29 23:30:24 +03:00
|
|
|
export function sleep(ms: number): Promise<void> {
|
|
|
|
return new Promise(resolve => {
|
|
|
|
setTimeout(resolve, ms);
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-07-29 18:46:49 +03:00
|
|
|
/**
|
|
|
|
* Attempts to find a relevant audit log entry for the given user and action
|
|
|
|
*/
|
|
|
|
export async function findRelevantAuditLogEntry(
|
2018-07-29 23:30:24 +03:00
|
|
|
guild: Guild,
|
2018-07-29 18:46:49 +03:00
|
|
|
actionType: number,
|
|
|
|
userId: string,
|
|
|
|
attempts: number = 3,
|
2019-02-09 14:36:31 +02:00
|
|
|
attemptDelay: number = 3000,
|
2018-07-29 18:46:49 +03:00
|
|
|
): Promise<GuildAuditLogEntry> {
|
2018-07-29 23:30:24 +03:00
|
|
|
const auditLogEntries = await guild.getAuditLogs(5, null, actionType);
|
2018-07-29 18:46:49 +03:00
|
|
|
|
|
|
|
auditLogEntries.entries.sort((a, b) => {
|
|
|
|
if (a.createdAt > b.createdAt) return -1;
|
|
|
|
if (a.createdAt > b.createdAt) return 1;
|
|
|
|
return 0;
|
|
|
|
});
|
|
|
|
|
|
|
|
const cutoffTS = Date.now() - 1000 * 60 * 2;
|
|
|
|
|
|
|
|
const relevantEntry = auditLogEntries.entries.find(entry => {
|
2018-07-29 23:30:24 +03:00
|
|
|
return entry.targetID === userId && entry.createdAt >= cutoffTS;
|
2018-07-29 18:46:49 +03:00
|
|
|
});
|
|
|
|
|
|
|
|
if (relevantEntry) {
|
|
|
|
return relevantEntry;
|
|
|
|
} else if (attempts > 0) {
|
2018-07-29 23:30:24 +03:00
|
|
|
await sleep(attemptDelay);
|
|
|
|
return findRelevantAuditLogEntry(guild, actionType, userId, attempts - 1, attemptDelay);
|
2018-07-29 18:46:49 +03:00
|
|
|
} else {
|
|
|
|
return null;
|
|
|
|
}
|
|
|
|
}
|
2018-07-31 02:42:45 +03:00
|
|
|
|
|
|
|
const urlRegex = /(\S+\.\S+)/g;
|
|
|
|
const protocolRegex = /^[a-z]+:\/\//;
|
|
|
|
|
|
|
|
export function getUrlsInString(str: string): url.URL[] {
|
|
|
|
const matches = str.match(urlRegex) || [];
|
|
|
|
return matches.reduce((urls, match) => {
|
|
|
|
if (!protocolRegex.test(match)) {
|
|
|
|
match = `https://${match}`;
|
|
|
|
}
|
|
|
|
|
|
|
|
let matchUrl: url.URL;
|
|
|
|
try {
|
|
|
|
matchUrl = new url.URL(match);
|
|
|
|
} catch (e) {
|
|
|
|
return urls;
|
|
|
|
}
|
|
|
|
|
|
|
|
const hostnameParts = matchUrl.hostname.split(".");
|
|
|
|
const tld = hostnameParts[hostnameParts.length - 1];
|
|
|
|
if (tlds.includes(tld)) {
|
|
|
|
urls.push(matchUrl);
|
|
|
|
}
|
|
|
|
|
|
|
|
return urls;
|
|
|
|
}, []);
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getInviteCodesInString(str: string): string[] {
|
|
|
|
const inviteCodeRegex = /(?:discord.gg|discordapp.com\/invite)\/([a-z0-9]+)/gi;
|
|
|
|
const inviteCodes = [];
|
|
|
|
let match;
|
|
|
|
|
|
|
|
// tslint:disable-next-line
|
|
|
|
while ((match = inviteCodeRegex.exec(str)) !== null) {
|
|
|
|
inviteCodes.push(match[1]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return inviteCodes;
|
|
|
|
}
|
|
|
|
|
|
|
|
export const unicodeEmojiRegex = emojiRegex();
|
2019-01-06 12:30:52 +02:00
|
|
|
export const customEmojiRegex = /<a?:(.*?):(\d+)>/;
|
|
|
|
|
|
|
|
const matchAllEmojiRegex = new RegExp(`(${unicodeEmojiRegex.source})|(${customEmojiRegex.source})`, "g");
|
2018-07-31 02:42:45 +03:00
|
|
|
|
|
|
|
export function getEmojiInString(str: string): string[] {
|
2018-12-15 23:01:45 +02:00
|
|
|
return str.match(matchAllEmojiRegex) || [];
|
2018-07-31 02:42:45 +03:00
|
|
|
}
|
|
|
|
|
2019-01-12 13:42:11 +02:00
|
|
|
export function isEmoji(str: string): boolean {
|
|
|
|
return str.match(`^(${unicodeEmojiRegex.source})|(${customEmojiRegex.source})$`) !== null;
|
|
|
|
}
|
|
|
|
|
2019-02-09 14:36:31 +02:00
|
|
|
export function isUnicodeEmoji(str: string): boolean {
|
|
|
|
return str.match(`^${unicodeEmojiRegex.source}$`) !== null;
|
|
|
|
}
|
|
|
|
|
2018-07-31 04:02:45 +03:00
|
|
|
export function trimLines(str: string) {
|
|
|
|
return str
|
|
|
|
.trim()
|
|
|
|
.split("\n")
|
|
|
|
.map(l => l.trim())
|
|
|
|
.join("\n")
|
|
|
|
.trim();
|
|
|
|
}
|
|
|
|
|
|
|
|
export const emptyEmbedValue = "\u200b";
|
|
|
|
export const embedPadding = "\n" + emptyEmbedValue;
|
2018-08-01 19:13:32 +03:00
|
|
|
|
|
|
|
export const userMentionRegex = /<@!?([0-9]+)>/g;
|
2019-02-17 16:01:04 +02:00
|
|
|
export const roleMentionRegex = /<@&([0-9]+)>/g;
|
2018-08-01 19:13:32 +03:00
|
|
|
|
|
|
|
export function getUserMentions(str: string) {
|
|
|
|
const regex = new RegExp(userMentionRegex.source, "g");
|
|
|
|
const userIds = [];
|
|
|
|
let match;
|
|
|
|
|
|
|
|
// tslint:disable-next-line
|
|
|
|
while ((match = regex.exec(str)) !== null) {
|
|
|
|
userIds.push(match[1]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return userIds;
|
|
|
|
}
|
|
|
|
|
|
|
|
export function getRoleMentions(str: string) {
|
|
|
|
const regex = new RegExp(roleMentionRegex.source, "g");
|
|
|
|
const roleIds = [];
|
|
|
|
let match;
|
|
|
|
|
|
|
|
// tslint:disable-next-line
|
|
|
|
while ((match = regex.exec(str)) !== null) {
|
|
|
|
roleIds.push(match[1]);
|
|
|
|
}
|
|
|
|
|
|
|
|
return roleIds;
|
|
|
|
}
|
2018-08-02 00:51:25 +03:00
|
|
|
|
|
|
|
/**
|
|
|
|
* Disables link previews in the given string by wrapping links in < >
|
|
|
|
*/
|
|
|
|
export function disableLinkPreviews(str: string): string {
|
|
|
|
return str.replace(/(?<!\<)(https?:\/\/\S+)/gi, "<$1>");
|
|
|
|
}
|
2018-08-05 00:18:50 +03:00
|
|
|
|
2018-11-24 17:59:05 +02:00
|
|
|
export function deactivateMentions(content: string): string {
|
|
|
|
return content.replace(/@/g, "@\u200b");
|
|
|
|
}
|
|
|
|
|
|
|
|
export function disableCodeBlocks(content: string): string {
|
|
|
|
return content.replace(/`/g, "`\u200b");
|
|
|
|
}
|
|
|
|
|
2018-12-15 17:15:32 +02:00
|
|
|
export function useMediaUrls(content: string): string {
|
|
|
|
return content.replace(/cdn\.discordapp\.com/g, "media.discordapp.net");
|
|
|
|
}
|
|
|
|
|
2019-02-15 03:55:18 +02:00
|
|
|
export function chunkArray<T>(arr: T[], chunkSize): T[][] {
|
|
|
|
const chunks: T[][] = [];
|
|
|
|
let currentChunk = [];
|
|
|
|
|
|
|
|
for (let i = 0; i < arr.length; i++) {
|
|
|
|
currentChunk.push(arr[i]);
|
|
|
|
if ((i !== 0 && i % chunkSize === 0) || i === arr.length - 1) {
|
|
|
|
chunks.push(currentChunk);
|
|
|
|
currentChunk = [];
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return chunks;
|
|
|
|
}
|
|
|
|
|
2018-11-24 19:14:12 +02:00
|
|
|
export function chunkLines(str: string, maxChunkLength = 2000): string[] {
|
|
|
|
if (str.length < maxChunkLength) {
|
|
|
|
return [str];
|
|
|
|
}
|
|
|
|
|
|
|
|
const chunks = [];
|
|
|
|
|
|
|
|
while (str.length) {
|
|
|
|
if (str.length <= maxChunkLength) {
|
|
|
|
chunks.push(str);
|
|
|
|
break;
|
|
|
|
}
|
|
|
|
|
|
|
|
const slice = str.slice(0, maxChunkLength);
|
|
|
|
|
|
|
|
const lastLineBreakIndex = slice.lastIndexOf("\n");
|
|
|
|
if (lastLineBreakIndex === -1) {
|
|
|
|
chunks.push(str.slice(0, maxChunkLength));
|
|
|
|
str = str.slice(maxChunkLength);
|
|
|
|
} else {
|
|
|
|
chunks.push(str.slice(0, lastLineBreakIndex));
|
|
|
|
str = str.slice(lastLineBreakIndex + 1);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
return chunks;
|
|
|
|
}
|
|
|
|
|
|
|
|
/**
|
|
|
|
* Chunks a long message to multiple smaller messages, retaining leading and trailing line breaks
|
|
|
|
*/
|
|
|
|
export function chunkMessageLines(str: string): string[] {
|
|
|
|
const chunks = chunkLines(str, 1999);
|
|
|
|
return chunks.map(chunk => {
|
|
|
|
if (chunk[0] === "\n") chunk = "\u200b" + chunk;
|
|
|
|
if (chunk[chunk.length - 1] === "\n") chunk = chunk + "\u200b";
|
|
|
|
return chunk;
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2019-01-06 15:27:51 +02:00
|
|
|
export async function createChunkedMessage(channel: TextableChannel, messageText: string) {
|
|
|
|
const chunks = chunkMessageLines(messageText);
|
|
|
|
for (const chunk of chunks) {
|
|
|
|
await channel.createMessage(chunk);
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2019-01-15 03:04:47 +02:00
|
|
|
/**
|
|
|
|
* Downloads the file from the given URL to a temporary file, with retry support
|
|
|
|
*/
|
|
|
|
export function downloadFile(attachmentUrl: string, retries = 3): Promise<{ path: string; deleteFn: () => void }> {
|
|
|
|
return new Promise(resolve => {
|
|
|
|
tmp.file((err, path, fd, deleteFn) => {
|
|
|
|
if (err) throw err;
|
|
|
|
|
|
|
|
const writeStream = fs.createWriteStream(path);
|
|
|
|
|
|
|
|
https
|
|
|
|
.get(attachmentUrl, res => {
|
|
|
|
res.pipe(writeStream);
|
|
|
|
writeStream.on("finish", () => {
|
|
|
|
writeStream.end();
|
|
|
|
resolve({
|
|
|
|
path,
|
2019-02-09 14:36:31 +02:00
|
|
|
deleteFn,
|
2019-01-15 03:04:47 +02:00
|
|
|
});
|
|
|
|
});
|
|
|
|
})
|
|
|
|
.on("error", httpsErr => {
|
|
|
|
fsp.unlink(path);
|
|
|
|
|
|
|
|
if (retries === 0) {
|
|
|
|
throw httpsErr;
|
|
|
|
} else {
|
|
|
|
console.warn("File download failed, retrying. Error given:", httpsErr.message);
|
|
|
|
resolve(downloadFile(attachmentUrl, retries - 1));
|
|
|
|
}
|
|
|
|
});
|
|
|
|
});
|
|
|
|
});
|
|
|
|
}
|
|
|
|
|
2018-12-15 17:04:04 +02:00
|
|
|
export function noop() {
|
|
|
|
// IT'S LITERALLY NOTHING
|
|
|
|
}
|
|
|
|
|
2018-08-05 00:18:50 +03:00
|
|
|
export const DBDateFormat = "YYYY-MM-DD HH:mm:ss";
|
2019-01-12 13:42:11 +02:00
|
|
|
|
|
|
|
export type CustomEmoji = {
|
|
|
|
id: string;
|
|
|
|
} & Emoji;
|