HiddenPhox/src/modules/misc.js

878 lines
22 KiB
JavaScript

const Command = require("../lib/command.js");
const logger = require("../lib/logger.js");
const CATEGORY = "misc";
const {librex} = require("../../config.json");
const {
formatTime,
hastebin,
parseHtmlEntities,
formatUsername,
safeString,
} = require("../lib/utils.js");
const GoogleImages = require("google-images");
const {tinycolor, random: randomColor} = require("@ctrl/tinycolor");
const sharp = require("sharp");
const net = require("node:net");
const imagesClient = new GoogleImages(hf.apikeys.gimg, hf.apikeys.google);
const yt = new Command("youtube");
yt.addAlias("yt");
yt.category = CATEGORY;
yt.helpText = "Search YouTube";
yt.usage = "[search term]";
yt.callback = async function (msg, line) {
if (!line) return "Arguments are required.";
const req = await fetch(
`${hf.config.piped_api}/search?q=${encodeURIComponent(line)}&filter=videos`
).then((x) => x.json());
const topVid = req.items[0];
let out = `**${safeString(
parseHtmlEntities(topVid.title)
)}** | \`${safeString(
parseHtmlEntities(topVid.uploaderName)
)}\`\nhttps://youtube.com${topVid.url}\n\n**__See Also:__**\n`;
for (let i = 1; i < 5; i++) {
const vid = req.items[i];
if (!vid) continue;
out += `- **${safeString(
parseHtmlEntities(vid.title)
)}** | By: \`${safeString(
parseHtmlEntities(vid.uploaderName)
)}\` | <https://youtube.com${vid.url}>\n`;
}
return out;
};
hf.registerCommand(yt);
const fyt = new Command("fyt");
fyt.category = CATEGORY;
fyt.helpText = "Search YouTube and take the first result.";
fyt.usage = "[search term]";
fyt.callback = async function (msg, line) {
if (!line) return "Arguments are required.";
const req = await fetch(
`${hf.config.piped_api}/search?q=${encodeURIComponent(line)}&filter=videos`
).then((x) => x.json());
const vid = req.items[0];
return `**${safeString(parseHtmlEntities(vid.title))}** | \`${safeString(
parseHtmlEntities(vid.uploaderName)
)}\`\nhttps://youtube.com${vid.url}`;
};
hf.registerCommand(fyt);
const WA_NO_ANSWER = "<:ms_cross:503341994974773250> No answer.";
const wolfram = new Command("wolfram");
wolfram.category = CATEGORY;
wolfram.helpText = "Wolfram Alpha";
wolfram.usage = "<-v> [query]";
wolfram.addAlias("wa");
wolfram.addAlias("calc");
wolfram.callback = async function (msg, line, args, {verbose, v}) {
const _verbose = verbose ?? v;
const query = args.join(" ");
const req = await fetch(
`http://api.wolframalpha.com/v2/query?input=${encodeURIComponent(
query
)}&appid=LH2K8H-T3QKETAGT3&output=json`
).then((x) => x.json());
const data = req.queryresult.pods;
if (!data) return WA_NO_ANSWER;
// fake no answer
//if (data[0].subpods[0].plaintext.includes("geoIP")) return WA_NO_ANSWER;
if (_verbose) {
const embed = {
title: `Result for: \`${safeString(query)}\``,
fields: [],
footer: {
icon_url: "http://www.wolframalpha.com/share.png",
text: "Powered by Wolfram Alpha",
},
image: {
url: data[1].subpods[0].img.src,
},
};
const extra = data.slice(1, 6);
for (const x in extra) {
embed.fields.push({
name: extra[x].title,
value: `[${
extra[x].subpods[0].plaintext.length > 0
? extra[x].subpods[0].plaintext
: "<click for image>"
}](${extra[x].subpods[0].img.src})`,
inline: true,
});
}
return {embed};
} else {
let image;
if (data[1].subpods[0].img.src)
try {
const res = await fetch(data[1].subpods[0].img.src);
if (res) {
const imgData = await res.arrayBuffer();
image = Buffer.from(imgData);
}
} catch {
//
}
let string = "";
if (data[1].subpods[0].plaintext.length > 0)
string = safeString(data[1].subpods[0].plaintext);
if (string.length > 2000 - (6 + safeString(query).length))
string = "Output too long: " + (await hastebin(string));
return {
content: `\`${safeString(query)}\` -> ${string.length > 0 ? string : ""}`,
file: image && {
file: image,
name: "wolfram_output.gif",
},
};
}
};
hf.registerCommand(wolfram);
const gimg = new Command("gimg");
gimg.category = CATEGORY;
gimg.helpText = "Search Google Images";
gimg.usage = "[query]";
gimg.addAlias("img");
gimg.callback = async function (msg, line) {
if (!line) return "No arguments given.";
const images = await imagesClient.search(line, {
safe:
msg.channel.nsfw && !msg.channel?.topic.includes("[no_nsfw]")
? "off"
: "high",
});
const index = Math.floor(Math.random() * images.length);
const image = images[index];
return {
embeds: [
{
title: image.description,
url: image.parentPage,
image: {
url: image.url,
},
footer: {
text: `Image ${Number(index) + 1}/${
images.length
}. Rerun to get a different image.`,
},
},
],
};
};
hf.registerCommand(gimg);
const fimg = new Command("fimg");
fimg.category = CATEGORY;
fimg.helpText = "Send first result from Google Images";
fimg.usage = "[query]";
fimg.callback = async function (msg, line) {
if (!line) return "No arguments given.";
const images = await imagesClient.search(line, {
safe:
msg.channel.nsfw && !msg.channel?.topic.includes("[no_nsfw]")
? "off"
: "high",
});
const image = images[0];
return {
embeds: [
{
title: image.description,
url: image.parentPage,
image: {
url: image.url,
},
},
],
};
};
hf.registerCommand(fimg);
const poll = new Command("poll");
poll.category = CATEGORY;
poll.helpText = "Start a poll";
poll.usage = "[topic] [option 1] [option 2] [...option 3-10]";
poll.callback = async function (msg, line, [topic, ...options]) {
if (!line || !topic)
return 'Usage: hf!poll "topic" "option 1" "option 2" "...options 3-10"';
const arrOptions = [...options].slice(0, 10);
if (arrOptions.length < 2) return "A minimum of two options are required.";
const reactions = [];
let displayString = `**${formatUsername(
msg.author
)}** has started a poll:\n## __${topic}__\n`;
for (let i = 0; i < arrOptions.length; i++) {
displayString +=
(i === 9 ? "\ud83d\udd1f" : `${i + 1}\u20e3`) +
": " +
arrOptions[i] +
"\n";
reactions[i] = i === 9 ? "\ud83d\udd1f" : `${i + 1}\u20e3`;
}
return {
content: displayString,
addReactions: reactions,
};
};
hf.registerCommand(poll);
const vote = new Command("vote");
vote.category = CATEGORY;
vote.helpText = "Start a yes/no vote";
vote.usage = "[topic]";
vote.callback = async function (msg, line, topic, {maybe}) {
if (!topic) return "No topic given.";
topic = topic.join(" ");
return {
content: `**${formatUsername(
msg.author
)}** has started a vote:\n## __${topic}__\n<:ms_tick:503341995348066313>: Yes\n<:ms_cross:503341994974773250>: No${
maybe ? "\n<:ms_tilda:581268710925271095>: Maybe/Uncertain" : ""
}`,
addReactions: [
":ms_tick:503341995348066313",
":ms_cross:503341994974773250",
maybe && ":ms_tilda:581268710925271095",
].filter((x) => x != null),
};
};
hf.registerCommand(vote);
const DAYS = ["Sun", "Mon", "Tue", "Wed", "Thu", "Fri", "Sat"];
const anonradio = new Command("anonradio");
anonradio.category = CATEGORY;
anonradio.helpText = "aNONradio.net schedule";
anonradio.callback = async function () {
const now = new Date();
let playing;
try {
playing = await fetch("https://anonradio.net/playing").then((res) =>
res.text()
);
} catch (err) {
try {
playing = await fetch("http://anonradio.net/playing").then((res) =>
res.text()
);
} catch (err) {
//
}
}
let schedule;
try {
schedule = await fetch("https://anonradio.net/schedule/").then((res) =>
res.text()
);
} catch (err) {
try {
schedule = await fetch("http://anonradio.net/schedule/").then((res) =>
res.text()
);
} catch (err) {
//
}
}
if (!playing || !schedule) return "Failed to fetch schedule.";
const icecast = await fetch("http://anonradio.net:8010/status-json.xsl")
.then((res) => res.text())
.then((data) =>
JSON.parse(data.replace(/"title": - ,/g, '"title":" - ",'))
);
let lines = schedule.split("\n");
lines = lines.slice(4, lines.length - 2);
const parsedLines = [];
for (const line of lines) {
const [_, time, id, name] = line.match(/^(.{3,4} .{4})\s+(.+?) {2}(.+?)$/);
const tmp = time.split(" ");
const day = tmp[0];
let hour = tmp[1];
const currentDay = now.getUTCDay();
const targetDay = DAYS.indexOf(day);
const delta = (targetDay + 7 - currentDay) % 7;
let currentYear = now.getUTCFullYear();
const currentMonth = now.getUTCMonth() + 1;
const currentDateDay = now.getUTCDate();
let targetMonth = currentMonth;
const lastDay = new Date(currentYear, currentMonth, 0).getDate();
let targetDateDay = currentDateDay + delta;
if (targetDateDay > lastDay) {
targetMonth = currentMonth === 12 ? 1 : currentMonth + 1;
targetDateDay = 1;
if (currentMonth === 12) currentYear++;
}
hour = hour.slice(0, 2) + ":" + hour.slice(-2);
const timestamp =
Date.parse(
`${DAYS[targetDay]}, ${currentYear}-${targetMonth}-${targetDateDay} ${hour} UTC`
) / 1000;
let nameOut = name;
if (time == "Sat 0300")
nameOut = name.replace(
"Open Mic - Anyone can stream",
"Synth Battle Royale"
);
parsedLines.push({
timestamp,
id,
name: nameOut.replace(" <- Up NEXT", ""),
});
}
let liveNow = {name: "ident", id: "aNONradio"};
if (parsedLines[0].name.includes("<- Live NOW")) {
liveNow = parsedLines.splice(0, 1)[0];
liveNow.name = liveNow.name.replace(" <- Live NOW", "");
}
let title = "";
let subtitle = "";
if (playing.includes("listeners with a daily peak of")) {
title = `${liveNow.name} (\`${liveNow.id}\`)`;
subtitle = playing;
} else {
const [_, current, peakDay, peakMonth, dj, metadata] = playing.match(
/\[(\d+)\/(\d+)\/(\d+)\] \((.+?)\): (.+)/
);
if (
metadata == "https://archives.anonradio.net" ||
liveNow.name == "Synth Battle Royale"
) {
title = `${liveNow.name} (\`${dj}\`)`;
} else {
title = `${metadata} (\`${dj}\`)`;
}
subtitle = `${current} listening with a daily peak of ${peakDay} and ${peakMonth} peak for the month.`;
}
let openmicTime = "";
if (liveNow.id == "openmic") {
const streamData = icecast.icestats.source.find(
(src) => src.listenurl == "http://anonradio.net:8010/openmic"
);
if (streamData && streamData.stream_start_iso8601) {
const startTime = new Date(streamData.stream_start_iso8601).getTime();
openmicTime = `-\\*- OpenMIC DJ has been streaming for ${formatTime(
Date.now() - startTime
)} -\\*-\n`;
}
}
return {
embeds: [
{
title: subtitle,
author: {
name: title,
url: "http://anonradio.net:8000/anonradio",
},
description: openmicTime + "__Schedule:__",
fields: parsedLines.map((line) => ({
inline: true,
name: `${line.name} (\`${line.id}\`)`,
value: `<t:${line.timestamp}:R>`,
})),
},
],
};
};
hf.registerCommand(anonradio);
const REGEX_IPV4 = /^((25[0-5]|(2[0-4]|1\d|[1-9]|)\d)(\.(?!$)|$)){4}$/;
const shodan = new Command("shodan");
shodan.category = CATEGORY;
shodan.helpText = "Look up an IP on Shodan InternetDB";
shodan.callback = async function (msg, line) {
if (!line || line == "") return "Arguments required.";
if (!REGEX_IPV4.test(line)) return "Invalid IP address.";
const data = await fetch("https://internetdb.shodan.io/" + line).then((res) =>
res.json()
);
if (data.detail) return data.detail;
return {
embed: {
title: `Results for \`${data.ip}\``,
fields: [
{
name: "Hostnames",
value:
data.hostnames.length > 0
? data.hostnames.map((str) => `\`${str}\``).join(" ")
: "None",
inline: true,
},
{
name: "Open ports",
value: data.ports.length > 0 ? data.ports.join(", ") : "None",
inline: true,
},
{
name: "Tags",
value:
data.tags.length > 0
? data.tags.map((str) => `\`${str}\``).join(", ")
: "None",
inline: true,
},
{
name: "CPEs",
value:
data.cpes.length > 0
? data.cpes.map((str) => `\`${str}\``).join(" ")
: "None",
inline: true,
},
{
name: "Vulnerabilities",
value:
data.vulns.length > 0
? data.vulns.map((str) => `\`${str}\``).join(" ")
: "None",
inline: true,
},
],
},
};
};
hf.registerCommand(shodan);
const GENERATE_HEADERS = {
Accept: "application/json",
"Content-Type": "application/json",
};
const generate = new Command("generate");
generate.category = CATEGORY;
generate.helpText = "Generate images from prompt via craiyon";
generate.callback = async function (msg, line) {
if (!line || line.length === 0) return "Arguments required.";
msg.channel.sendTyping();
const start = Date.now();
let retries = 0;
let request = await fetch("https://backend.craiyon.com/generate", {
method: "POST",
headers: GENERATE_HEADERS,
body: JSON.stringify({prompt: line}),
});
while (request.status !== 200) {
request = await fetch("https://backend.craiyon.com/generate", {
method: "POST",
headers: GENERATE_HEADERS,
body: JSON.stringify({prompt: line}),
});
retries++;
}
const data = await request.json();
const images = data.images
.map((img) => Buffer.from(img, "base64"))
.map((img, index) => ({contents: img, name: `${index}.jpg`}));
const title = `Responses for "${safeString(line)}"`;
const out = {
content: `Generated in ${formatTime(Date.now() - start)}${
retries > 0 ? " with " + retries + " retries" : ""
}`,
embeds: [],
files: images,
};
let splitIndex = 0;
for (const index in images) {
if (index % 3 == 0) splitIndex++;
out.embeds.push({
title,
url: "https://www.craiyon.com/?" + splitIndex,
image: {
url: `attachment://${index}.jpg`,
},
});
}
return out;
};
hf.registerCommand(generate);
const search = new Command("search");
search.category = CATEGORY;
search.helpText = "Search, powered by LibreX";
search.addAlias("g");
search.addAlias("google");
search.addAlias("ddg");
search.callback = async function (msg, line, args, {results = 2}) {
const query = args.join(" ");
if (!librex) return "LibreX instance not defined.";
if (!query || query == "") return "Search query required.";
const encodedQuery = encodeURIComponent(query);
if (line.startsWith("!")) {
const url = `https://api.duckduckgo.com/?q=${encodedQuery}&format=json`;
const res = await fetch(url);
if (res.url != url) return res.url;
}
const res = await fetch(`${librex}/api.php?q=${encodedQuery}&p=0&t=0`).then(
(res) => res.json()
);
delete res.results_source;
if (res.error?.message) {
if (res.error.message.indexOf("No results found.") > -1) {
return "Search returned no results.";
} else {
return `Search returned error:\n\`\`\`\n${res.error.message}\`\`\``;
}
} else {
const searchResults = Object.values(res)
.filter((result) => !("did_you_mean" in result))
.splice(0, Number(results));
if (searchResults.length > 0) {
let out = `__**Results for \`${safeString(query)}\`**__\n`;
for (const result of searchResults) {
if (result.special_response) {
out +=
"> " +
safeString(
parseHtmlEntities(
result.special_response.response.split("\n").join("\n> ")
)
);
out += `\n<${encodeURI(result.special_response.source)}>`;
} else {
out += `**${safeString(
parseHtmlEntities(result.title)
).trim()}** - <${encodeURI(result.url)}>`;
out += `\n> ${safeString(parseHtmlEntities(result.description))}`;
}
out += "\n\n";
}
return out.trim();
} else {
return "Search returned no results.";
}
}
};
hf.registerCommand(search);
const color = new Command("color");
color.category = CATEGORY;
color.helpText = "Show information on a color or get a random color";
color.callback = async function (msg, line, args, {truerandom}) {
let color = tinycolor(line),
random = false;
if (!line || line == "" || args.length == 0) {
color = truerandom
? tinycolor(Math.floor(Math.random() * 0xffffff))
: randomColor();
random = true;
}
if (!color.isValid) {
return "Color not valid.";
}
const image = await sharp({
create: {
width: 128,
height: 128,
channels: 3,
background: {r: color.r, g: color.g, b: color.b},
},
})
.png()
.toBuffer();
const fileName = `${color.toHex()}.png`;
const fields = [
{
name: "Hex",
value: color.toHexString(),
inline: true,
},
{
name: "RGB",
value: color.toRgbString(),
inline: true,
},
{
name: "HSL",
value: color.toHslString(),
inline: true,
},
{
name: "HSV",
value: color.toHsvString(),
inline: true,
},
{
name: "Integer",
value: color.toNumber(),
inline: true,
},
];
if (color.toName() != false) {
fields.splice(0, 0, {
name: "Name",
value: color.toName(),
inline: true,
});
}
return {
embeds: [
{
title: random ? "Random Color" : "",
color: color.toNumber(),
thumbnail: {
url: `attachment://${fileName}`,
},
fields,
},
],
file: {
file: image,
name: fileName,
},
};
};
hf.registerCommand(color);
function writeVarInt(value) {
let buf = Buffer.alloc(0);
do {
let temp = value & 0b01111111;
value >>>= 7;
if (value != 0) {
temp |= 0b10000000;
}
buf = Buffer.concat([buf, Buffer.from([temp])]);
} while (value != 0);
return buf;
}
const ident = Buffer.from("HiddenPhox (c7.pm) ", "utf8");
const identPort = Buffer.alloc(2);
identPort.writeUInt16BE(3);
const handshake = Buffer.concat([
writeVarInt(0x0),
writeVarInt(1073741953),
writeVarInt(ident.length),
ident,
identPort,
writeVarInt(1),
]);
const handshakeWithLength = Buffer.concat([
writeVarInt(handshake.length),
handshake,
]);
const status = Buffer.concat([writeVarInt(1), writeVarInt(0x0)]);
const HANDSHAKE_PACKET = Buffer.concat([handshakeWithLength, status]);
const formattingToAnsi = {
r: "0",
l: "1",
m: "9",
n: "4",
o: "3",
0: "30",
1: "34",
2: "32",
3: "36",
4: "31",
5: "35",
6: "33",
7: "37",
8: "90",
9: "94",
a: "92",
b: "96",
c: "91",
d: "95",
e: "93",
f: "97",
};
const mcserver = new Command("mcserver");
mcserver.category = CATEGORY;
mcserver.helpText = "Query a Minecraft server";
mcserver.callback = async function (msg, line) {
if (!line || line == "") return "Arguments required.";
const split = line.split(":");
const ip = split[0];
const port = split[1] ?? 25565;
await msg.addReaction("\uD83C\uDFD3");
const data = await new Promise((resolve, reject) => {
logger.verbose("mcserver", "querying", ip, port);
const client = net.createConnection(
{
host: ip,
port: port,
timeout: 180000,
},
function () {
logger.verbose("mcserver", "connect");
client.write(HANDSHAKE_PACKET);
}
);
const timeout = setTimeout(() => {
logger.verbose("mcserver", "timeout");
client.destroy();
resolve("timeout");
}, 180000);
let totalData = Buffer.alloc(0);
client.on("data", function (data) {
totalData = Buffer.concat([totalData, data]);
logger.verbose("mcserver", "data", data.length, totalData.length);
});
client.on("close", function (err) {
if (err) {
logger.verbose("mcserver", "close with error", err);
return reject(err);
}
const dataAsString = totalData.toString().trim();
console.log(dataAsString);
const json = JSON.parse(
dataAsString.slice(
dataAsString.indexOf("{"),
dataAsString.lastIndexOf("}") + 1
)
);
logger.verbose("mcserver", "close", json);
clearTimeout(timeout);
return resolve(json);
});
client.on("timeout", function () {});
});
if (data == "timeout") {
await msg.removeReaction("\uD83C\uDFD3");
return "Timed out trying to query.";
} else {
await msg.removeReaction("\uD83C\uDFD3");
const motd = data.description.text.replace(
/\u00a7([a-f0-9k-or])/gi,
(formatting) => {
const ansi = formattingToAnsi[formatting];
return ansi ? `\x1b[${ansi}m` : "";
}
);
const players = data.players?.sample?.map((player) => player.name) ?? [];
const totalPlayers = `(${data.players.online}/${data.players.max})`;
let image;
if (data.favicon) {
image = Buffer.from(
data.favicon.slice(data.favicon.indexOf(",")),
"base64"
);
}
return {
embed: {
title: `Server info for: \`${line}\``,
fields: [
{
name: "MOTD",
value: `\`\`\`ansi\n${motd}\n\`\`\``,
},
{
name: "Version",
value: `${data.version.name} (\`${data.version.protocol}\`)`,
inline: true,
},
{
name: `Players ${players.length > 0 ? totalPlayers : ""}`,
value: players.length > 0 ? players.join(", ") : totalPlayers,
inline: players.length == 0,
},
],
thumbnail: image && {
url: "attachment://icon.png",
},
},
file: image && {
file: image,
name: "icon.png",
},
};
}
};
hf.registerCommand(mcserver);