const Command = require("../lib/command.js"); const InteractionCommand = require("../lib/interactionCommand.js"); const logger = require("../lib/logger.js"); const CATEGORY = "misc"; const {ApplicationCommandOptionTypes} = require("@projectdysnomia/dysnomia").Constants; const {librex} = require("../../config.json"); const {getOption} = require("../lib/interactionDispatcher.js"); const { formatTime, 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) )}\` | \n`; } return out; }; hf.registerCommand(yt); const ytInteraction = new InteractionCommand("youtube"); ytInteraction.helpText = "Search Youtube"; ytInteraction.options.search = { name: "search", type: ApplicationCommandOptionTypes.STRING, description: "Search query", required: true, default: "", }; ytInteraction.callback = async function (interaction) { const search = getOption(interaction, ytInteraction, "search"); return yt.callback(interaction, search); }; hf.registerCommand(ytInteraction); 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 fytInteraction = new InteractionCommand("fyt"); fytInteraction.helpText = "Search Youtube and take the first result."; fytInteraction.options.search = { name: "search", type: ApplicationCommandOptionTypes.STRING, description: "Search query", required: true, default: "", }; fytInteraction.callback = async function (interaction) { const search = getOption(interaction, fytInteraction, "search"); return fyt.callback(interaction, search); }; hf.registerCommand(fytInteraction); 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 : "" }](${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); let text; if (string.length > 2000 - (6 + safeString(query).length)) { text = string; string = "Output too long:"; } return { content: `\`${safeString(query)}\` -> ${string.length > 0 ? string : ""}`, attachments: [ text && { file: text, filename: "message.txt", }, image && { file: image, filename: "wolfram_output.gif", }, ].filter((x) => !!x), }; } }; hf.registerCommand(wolfram); const wolframInteraction = new InteractionCommand("wolfram"); wolframInteraction.helpText = "Wolfram Alpha"; wolframInteraction.options.query = { name: "query", type: ApplicationCommandOptionTypes.STRING, description: "What to query Wolfram Alpha for", required: true, default: "", }; wolframInteraction.options.verbose = { name: "verbose", type: ApplicationCommandOptionTypes.STRING, description: "Verbose output", required: false, default: false, }; wolframInteraction.callback = async function (interaction) { const query = getOption(interaction, wolframInteraction, "query"); const verbose = getOption(interaction, wolframInteraction, "verbose"); return wolfram.callback(interaction, query, [query], {verbose}); }; hf.registerCommand(wolframInteraction); 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: ``, })), }, ], }; }; 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 (query.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 searchInteraction = new InteractionCommand("search"); searchInteraction.helpText = "Search, powered by LibreX"; searchInteraction.options.query = { name: "query", type: ApplicationCommandOptionTypes.STRING, description: "What to search for", required: true, default: "", }; searchInteraction.options.results = { name: "results", type: ApplicationCommandOptionTypes.INTEGER, description: "How many results to show", required: false, min_value: 1, max_value: 10, default: 2, }; searchInteraction.callback = async function (interaction) { const query = getOption(interaction, searchInteraction, "query"); const results = getOption(interaction, searchInteraction, "results"); return search.callback(interaction, query, [query], {results}); }; hf.registerCommand(searchInteraction); 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); const colorInteraction = new InteractionCommand("color"); colorInteraction.helpText = "Show information on a color or get a random color"; colorInteraction.options.input = { name: "input", type: ApplicationCommandOptionTypes.STRING, description: "Color to get info on", required: false, default: "", }; colorInteraction.options.truerandom = { name: "truerandom", type: ApplicationCommandOptionTypes.BOOLEAN, description: "Should the random color give a 'true random' color instead of an adjust color", required: false, default: false, }; colorInteraction.callback = async function (interaction) { const input = getOption(interaction, colorInteraction, "input"); const truerandom = getOption(interaction, colorInteraction, "truerandom"); return color.callback(interaction, input, [input], {truerandom}); }; hf.registerCommand(colorInteraction); 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);