diff --git a/src/commands/owner/sync.ts b/src/commands/owner/sync.ts index 50a7c4b..f94427e 100644 --- a/src/commands/owner/sync.ts +++ b/src/commands/owner/sync.ts @@ -19,8 +19,8 @@ export default defineCommand({ const { guildCount, globalCount } = counts; - await interaction.followUp({ - content: `Successfully synced ${guildCount} guild and ${globalCount} global application commands`, - }); + await interaction.followUp( + `Successfully synced ${guildCount} guild and ${globalCount} global application commands` + ); }, }); diff --git a/src/commands/utility/isdown.ts b/src/commands/utility/isdown.ts new file mode 100644 index 0000000..5afa36c --- /dev/null +++ b/src/commands/utility/isdown.ts @@ -0,0 +1,91 @@ +import { inlineCode, SlashCommandBuilder } from "discord.js"; +import { defineCommand } from ".."; +import { abort } from "../../utils/error"; +import { z } from "zod"; +import ky, { TimeoutError, type KyResponse } from "ky"; +import { fileTypeFromBuffer } from "file-type"; +import { FAKE_USER_AGENT } from "../../utils/constants"; + +export default defineCommand({ + data: new SlashCommandBuilder() + .setName("isdown") + .setDescription("URL healthcheck") + .addStringOption((option) => + option.setName("url").setDescription("The URL to check").setRequired(true) + ), + + async execute(interaction) { + let url = interaction.options.getString("url", true); + + if (!url.startsWith("http")) { + url = "https://" + url; + } + + if (!z.string().url().safeParse(url).success) { + abort("Invalid URL"); + } + + const parsed = new URL(url); + if (parsed.hostname.match(/^(localhost|127\.0\.0\.1)$|^192/)) { + abort("Invalid URL"); + } + + await interaction.deferReply(); + + let res: KyResponse; + try { + res = await ky.get(url, { + headers: { + "User-Agent": FAKE_USER_AGENT, + }, + throwHttpErrors: false, + timeout: 5000, + }); + } catch (err) { + let msg = "Couldn't establish HTTP connection."; + if (err instanceof TimeoutError) { + msg = "Request timed out, no HTTP response."; + } + + await interaction.editReply( + `It's not just you! The site is down.\n${inlineCode(msg)}` + ); + return; + } + + const chunks: Uint8Array[] = []; + let mime = res.headers.get("content-type"); + + if (!mime && res.body) { + for await (const chunk of res.body) { + chunks.push(chunk as Uint8Array); + break; + } + + const type = await fileTypeFromBuffer(Buffer.concat(chunks)); + if (type) { + mime = type.mime; + } + } + + if (res.ok) { + await interaction.editReply( + `It's just you! The site is up.\nHTTP Response: ${inlineCode( + `${res.status} ${res.statusText} • ${mime}` + )}` + ); + } else if (res.status === 404) { + await interaction.editReply( + `It's not just you! Either the resource is down or you entered the wrong URI path.\nHTTP Response: ${inlineCode( + `${res.status} ${res.statusText} • ${mime}` + )}` + ); + } else { + await interaction.editReply( + `It's not just you! The site is down.\nHTTP Response: ${inlineCode( + `${res.status} ${res.statusText}` + )}` + ); + } + }, +}); diff --git a/src/commands/utility/whois.ts b/src/commands/utility/whois.ts new file mode 100644 index 0000000..e5d9e4b --- /dev/null +++ b/src/commands/utility/whois.ts @@ -0,0 +1,23 @@ +import { codeBlock, SlashCommandBuilder } from "discord.js"; +import { defineCommand } from ".."; +import ky from "ky"; + +export default defineCommand({ + data: new SlashCommandBuilder() + .setName("whois") + .setDescription("Look up IP or domain info") + .addStringOption((option) => + option.setName("query").setDescription("IP or domain").setRequired(true) + ), + + async execute(interaction) { + const query = interaction.options.getString("query", true); + await interaction.deferReply(); + + const res = await ky + .get(`http://ip-api.com/json/${encodeURIComponent(query)}`) + .json(); + + await interaction.editReply(codeBlock("js", JSON.stringify(res, null, 2))); + }, +}); diff --git a/src/utils/constants.ts b/src/utils/constants.ts index 56ca338..fdd21be 100644 --- a/src/utils/constants.ts +++ b/src/utils/constants.ts @@ -1,4 +1,9 @@ +import { version } from "discord.js"; import { env } from "../env"; export const DEV = env.NODE_ENV === "development"; export const PROD = env.NODE_ENV === "production"; + +export const USER_AGENT = `artemis (discord.js ${version})`; +export const FAKE_USER_AGENT = + "Mozilla/5.0 (X11; Linux x86_64; rv:135.0) Gecko/20100101 Firefox/135.0";