HiddenPhox/src/lib/utils.js

441 lines
11 KiB
JavaScript

const {Collection} = require("@projectdysnomia/dysnomia");
const murmurhash = require("murmurhash").v3;
const {tinycolor} = require("@ctrl/tinycolor");
const logger = require("../lib/logger.js");
function pastelize(id) {
const hue = murmurhash(id) % 360;
const hex = tinycolor(`hsl(${hue},75%,60%)`).toHex();
return parseInt(hex, 16);
}
function formatUsername(user) {
return user.discriminator && user.discriminator != "0"
? `${user.username}#${user.discriminator}`
: `@${user.username}`;
}
function getTopColor(msg, id, fallback = 0x7289da) {
if (!msg.guildID) return fallback;
const guild = msg.channel?.guild ?? hf.bot.guilds.get(msg.guildID);
if (!guild) return fallback;
const roles = guild.members
.get(id)
.roles.map((role) => guild.roles.get(role))
.filter((role) => role.color);
roles.sort((a, b) => b.position - a.position);
return roles[0]?.color || fallback;
}
function safeString(string, newLines = true) {
string = string ? string.toString() : "";
string = string.replace(/`/g, "'");
string = string.replace(/<@/g, "<@\u200b");
string = string.replace(/<#/g, "<#\u200b");
string = string.replace(/<&/g, "<&\u200b");
if (newLines) string = string.replace(/\n/g, " ");
return string;
}
function formatTime(number) {
let seconds = parseInt(number) / 1000;
const days = Math.floor(seconds / 86400);
seconds = seconds % 86400;
const hours = Math.floor(seconds / 3600);
seconds = seconds % 3600;
const minutes = Math.floor(seconds / 60);
seconds = Math.floor(seconds % 60);
return (
(days !== 0 ? `${days.toString().padStart(2, "0")}:` : "") +
(hours !== 0 ? `${hours.toString().padStart(2, "0")}:` : "") +
`${minutes.toString().padStart(2, "0")}:${seconds
.toString()
.padStart(2, "0")}`
);
}
function readableTime(number) {
const seconds = number / 1000;
const days = seconds / 60 / 60 / 24;
const years = days / 365.25;
if (years >= 1) {
return `${years.toFixed(2)} years`;
} else {
return `${days.toFixed(2)} days`;
}
}
async function isGif(url) {
const type = await fetch(url, {method: "HEAD"}).then((res) =>
res.headers.get("Content-Type")
);
return type == "image/gif";
}
async function findLastImage(msg, gif = false) {
const messages = await msg.channel.getMessages(20);
let img;
for (const message of messages) {
if (message.attachments.size > 0) {
img = [...msg.attachments.values()][0].url;
if (gif && (await isGif(img))) {
break;
} else {
break;
}
} else if (message.embeds.length > 0) {
img = message.embeds[0]?.thumbnail?.url || message.embeds[0]?.image?.url;
if (img) {
if (gif && (await isGif(img))) {
break;
} else {
break;
}
}
}
}
return await new Promise((resolve, reject) => {
if (!img) {
reject("Image not found in last 20 messages.");
} else {
resolve(img);
}
});
}
const urlRegex = /((https?):\/)?\/?([^:/\s]+)((\/\w+)*\/)([\w\-.]+)/;
async function getImage(msg, str) {
const refMsg = msg.referencedMessage;
if (refMsg) {
const attachments = [...refMsg.attachments.values()];
if (attachments[0]?.url) {
return attachments[0].url;
} else if (/<a?:[a-zA-Z0-9_]+:([0-9]+)>/.test(refMsg.content)) {
const id = refMsg.content.match(/<a?:[a-zA-Z0-9_]+:([0-9]+)>/)[1];
return `https://cdn.discordapp.com/emojis/${id}.png?v=1`;
} else if (/<@!?([0-9]+)>/.test(refMsg.content)) {
const id = refMsg.content.match(/<@!?([0-9]+)>/)[1];
const user = await hf.bot.requestHandler.request(
"GET",
"/users/" + id,
true
);
if (user)
return `https://cdn.discordapp.com/avatars/${id}/${user.avatar}.png?size=1024`;
}
}
const img = await findLastImage(msg, false);
if (!str) {
if (img) return img;
}
const attachments = [...msg.attachments.values()];
if (attachments[0]?.url) {
return attachments[0]?.url;
} else if (urlRegex.test(str)) {
return str;
} else if (/<a?:[a-zA-Z0-9_]+:([0-9]+)>/.test(str)) {
const id = str.match(/<a?:[a-zA-Z0-9_]+:([0-9]+)>/)[1];
return `https://cdn.discordapp.com/emojis/${id}.png?v=1`;
} else if (/<@!?([0-9]+)>/.test(str)) {
const id = str.match(/<@!?([0-9]+)>/)[1];
const user = await hf.bot.requestHandler.request(
"GET",
"/users/" + id,
true
);
if (user)
return `https://cdn.discordapp.com/avatars/${id}/${user.avatar}.png?size=1024`;
} else if (img) {
return img;
}
return null;
}
async function hastebin(body) {
const res = await fetch(`${hf.config.haste_provider}/documents`, {
method: "POST",
body,
}).then((r) => r.json());
return `<${hf.config.haste_provider}/${res.key}>`;
}
hf.selectionMessages = hf.selectionMessages ?? new Collection();
async function selectionMessage(
msg,
heading,
options,
timeout = 30000,
maxItems = 1
) {
const data = {
content: heading,
allowedMentions: {
repliedUser: false,
},
messageReference: {
messageID: msg.id,
},
components: [
{
type: 1,
components: [
{
type: 3,
custom_id: msg.id,
options: [],
max_values: maxItems,
},
],
},
{
type: 1,
components: [
{
type: 2,
style: 4,
label: "Cancel",
custom_id: "cancel",
},
],
},
],
};
options.slice(0, 25).forEach((value) => {
data.components[0].components[0].options.push({
label: value.display,
value: value.key,
description: value.description,
});
});
if (options.length > 25) {
data.content += `\n\nDisplaying 25/${options.length} results`;
}
const displayMessage = await msg.channel
.createMessage(data)
.catch((err) =>
logger.error(
"selectionMessage",
"Failed to create selection message: " + err
)
);
return await new Promise((resolve, reject) => {
function listener(interaction) {
const user = interaction.member.user || interaction.user;
if (
user.id == msg.author.id &&
interaction.channel.id == msg.channel.id &&
interaction.message.components[0].components[0].custom_id == msg.id
) {
if (interaction.data.custom_id == "cancel") {
hf.events.remove("interactionCreate", `selection.${msg.id}`);
clearTimeout(hf.selectionMessages.get(msg.id));
hf.selectionMessages.delete(msg.id);
interaction.deferUpdate();
displayMessage.delete();
reject("Canceled");
} else {
hf.events.remove("interactionCreate", `selection.${msg.id}`);
clearTimeout(hf.selectionMessages.get(msg.id));
hf.selectionMessages.delete(msg.id);
interaction.deferUpdate();
displayMessage.delete();
let result;
if (maxItems > 1) {
result = options
.filter((opt) => interaction.data.values.includes(opt.key))
.map((opt) => opt.key);
} else {
result = options.filter(
(opt) => opt.key == interaction.data.values[0]
)[0].value;
}
resolve(result);
}
}
}
hf.events.add("interactionCreate", `selection.${msg.id}`, listener);
hf.selectionMessages.set(
msg.id,
setTimeout(() => {
hf.events.remove("interactionCreate", `selection.${msg.id}`);
hf.selectionMessages.delete(msg.id);
reject("Request timed out");
}, timeout)
);
});
}
async function lookupUser(msg, str, filter) {
if (/[0-9]{17,21}/.test(str)) {
return await hf.bot.requestHandler.request(
"GET",
"/users/" + str.match(/([0-9]{17,21})/)[1],
true
);
}
let users;
if (filter) {
users = hf.bot.users.filter(filter).values();
} else if (msg.guildID) {
const guild = msg.channel.guild || hf.bot.guilds.get(msg.guildID);
users = guild.members.values();
} else {
users = hf.bot.users.values();
}
if (/(.+?)#([0-9]{4})/.test(str)) {
const [_, name, discrim] = str.match(/(.+?)#([0-9]{4})/);
for (const user of users) {
if (
user.username.toLowerCase() == name.toLowerCase() &&
user.discriminator == discrim
) {
return user;
}
}
}
const selection = [];
for (const user of users) {
if (
user.username.toLowerCase() == str.toLowerCase() ||
(user.nickname && user.nickname == str.toLowerCase())
) {
return user;
} else if (
user.username.toLowerCase().indexOf(str.toLowerCase()) > -1 ||
(user.nick && user.nick.toLowerCase().indexOf(str.toLowerCase()) > -1) ||
(user.globalName &&
user.globalName.toLowerCase().indexOf(str.toLowerCase()) > -1)
) {
selection.push({
value: user,
key: user.id,
display: `${formatUsername(user)}${
user.nick
? ` (${user.nick})`
: user.globalName
? ` (${user.globalName})`
: ""
}`,
});
}
}
selection.sort((a, b) => a.display - b.display);
if (selection.length == 0) {
return "No results";
} else if (selection.length == 1) {
return selection[0].value;
} else {
selection.splice(20);
try {
return await selectionMessage(
msg,
"Multiple users found, please pick from this list:",
selection
);
} catch (out) {
return out;
}
}
}
// https://stackoverflow.com/a/39243641
const htmlEntities = {
nbsp: " ",
cent: "¢",
pound: "£",
yen: "¥",
euro: "€",
copy: "©",
reg: "®",
lt: "<",
gt: ">",
quot: '"',
amp: "&",
apos: "'",
};
function parseHtmlEntities(str) {
return str.replace(/&([^;]+);/g, function (entity, entityCode) {
let match;
if (entityCode in htmlEntities) {
return htmlEntities[entityCode];
} else if ((match = entityCode.match(/^#x([\da-fA-F]+)$/))) {
return String.fromCharCode(parseInt(match[1], 16));
} else if ((match = entityCode.match(/^#(\d+)$/))) {
return String.fromCharCode(~~match[1]);
} else {
return entity;
}
});
}
const UPLOAD_LIMIT = 26214400;
const UPLOAD_LIMIT_TIER_2 = 52428800;
const UPLOAD_LIMIT_TIER_3 = 104857600;
function getUploadLimit(guild) {
if (!guild) return UPLOAD_LIMIT;
if (guild.premiumTier == 2) return UPLOAD_LIMIT_TIER_2;
if (guild.premiumTier == 3) return UPLOAD_LIMIT_TIER_3;
return UPLOAD_LIMIT;
}
const TWITTER_EPOCH = 1288834974657;
const DISCORD_EPOCH = 1420070400000;
function snowflakeToTimestamp(snowflake, twitter = false) {
return (
Math.floor(Number(snowflake) / Math.pow(2, 22)) +
(twitter == true ? TWITTER_EPOCH : DISCORD_EPOCH)
);
}
module.exports = {
pastelize,
formatUsername,
getTopColor,
safeString,
formatTime,
readableTime,
isGif,
findLastImage,
getImage,
hastebin,
selectionMessage,
lookupUser,
parseHtmlEntities,
getUploadLimit,
snowflakeToTimestamp,
};