import { execFile } from "node:child_process"; import https from "node:https"; import { promisify } from "node:util"; import { Client, Events, GatewayIntentBits, REST, Routes, SlashCommandBuilder, } from "discord.js"; const execFileAsync = promisify(execFile); const TOKEN = process.env.DISCORD_TOKEN; const GUILD_ID = process.env.GUILD_ID || ""; const CHECK_INTERVAL_SEC = Number(process.env.WARP_CHECK_INTERVAL || 60); // Cloudflare's trace endpoint reports the IP it sees us as and whether the // request arrived over WARP. The request is made once per address family so // both the IPv4 and the IPv6 Cloudflare egress IPs can be shown. const TRACE_HOST = "www.cloudflare.com"; const TRACE_PATH = "/cdn-cgi/trace"; if (!TOKEN) { console.error("[bot] DISCORD_TOKEN is not set. Exiting."); process.exit(1); } function ts() { return new Date().toISOString(); } function log(scope, msg) { console.log(`[${ts()}] [${scope}] ${msg}`); } // --------------------------------------------------------------------------- // WARP status helpers // --------------------------------------------------------------------------- function parseTrace(text) { const data = {}; for (const line of text.trim().split("\n")) { const idx = line.indexOf("="); if (idx > 0) data[line.slice(0, idx)] = line.slice(idx + 1); } return data; } /** GET the trace endpoint over a specific IP family (4 or 6). */ function fetchTrace(family) { return new Promise((resolve, reject) => { const req = https.get( { host: TRACE_HOST, path: TRACE_PATH, family, timeout: 10_000 }, (res) => { let body = ""; res.setEncoding("utf8"); res.on("data", (c) => (body += c)); res.on("end", () => { if (res.statusCode !== 200) return reject(new Error(`trace HTTP ${res.statusCode}`)); resolve(parseTrace(body)); }); } ); req.on("timeout", () => req.destroy(new Error("timeout"))); req.on("error", reject); }); } /** Returns { ok, ip, warp, colo, loc, error } for one address family. */ async function traceFamily(family) { try { const t = await fetchTrace(family); return { ok: t.warp === "on" || t.warp === "plus", ...t }; } catch (err) { return { ok: false, error: err.message }; } } /** Local view from the WARP daemon inside this container. */ async function warpCliStatus() { try { const { stdout } = await execFileAsync( "warp-cli", ["--accept-tos", "status"], { timeout: 10_000 } ); return stdout.trim().replace(/\s+/g, " "); } catch (err) { return `unavailable (${err.message})`; } } let lastWarp = null; async function checkWarp(reason = "periodic") { const [v4, v6, cliStatus] = await Promise.all([ traceFamily(4), traceFamily(6), warpCliStatus(), ]); // Connected = Cloudflare sees WARP on at least one family (IPv4 is the one // Discord uses, so it is required; IPv6 is reported when available). const connected = v4.ok; const fmt = (r) => (r.ok ? `${r.ip} (warp=${r.warp})` : `n/a (${r.error || `warp=${r.warp}`})`); const colo = v4.colo || v6.colo || "?"; const loc = v4.loc || v6.loc || "?"; const summary = `ipv4=${fmt(v4)} ipv6=${fmt(v6)} colo=${colo} loc=${loc} | warp-cli: ${cliStatus}`; if (connected) { log("WARP", `CONNECTED via Cloudflare (${reason}) ${summary}`); } else { log("WARP", `NOT CONNECTED (${reason}) ${summary}`); await tryReconnect(); } lastWarp = { connected, v4, v6, colo, loc, cliStatus, checkedAt: ts() }; return lastWarp; } async function tryReconnect() { try { log("WARP", "attempting warp-cli connect"); await execFileAsync("warp-cli", ["--accept-tos", "connect"], { timeout: 15_000 }); } catch (err) { log("WARP", `reconnect attempt failed: ${err.message}`); } } // --------------------------------------------------------------------------- // Discord client // --------------------------------------------------------------------------- const commands = [ new SlashCommandBuilder().setName("ping").setDescription("Replies with pong and latency"), new SlashCommandBuilder() .setName("warp") .setDescription("Show the Cloudflare WARP tunnel status and egress IP of this bot"), ].map((c) => c.toJSON()); async function registerCommands(appId) { const rest = new REST({ version: "10" }).setToken(TOKEN); if (GUILD_ID) { await rest.put(Routes.applicationGuildCommands(appId, GUILD_ID), { body: commands }); log("bot", `registered ${commands.length} guild slash commands in ${GUILD_ID}`); } else { await rest.put(Routes.applicationCommands(appId), { body: commands }); log("bot", `registered ${commands.length} global slash commands`); } } const client = new Client({ intents: [GatewayIntentBits.Guilds] }); client.once(Events.ClientReady, async (c) => { log("bot", `logged in as ${c.user.tag} (id ${c.user.id}) in ${c.guilds.cache.size} guild(s)`); try { await registerCommands(c.user.id); } catch (err) { log("bot", `failed to register slash commands: ${err.message}`); } await checkWarp("startup"); setInterval(() => checkWarp("periodic"), CHECK_INTERVAL_SEC * 1000); log("bot", `WARP check scheduled every ${CHECK_INTERVAL_SEC}s`); }); client.on(Events.InteractionCreate, async (interaction) => { if (!interaction.isChatInputCommand()) return; if (interaction.commandName === "ping") { const sent = await interaction.reply({ content: "Pong!", fetchReply: true }); const rtt = sent.createdTimestamp - interaction.createdTimestamp; await interaction.editReply(`Pong! Round-trip ${rtt}ms, gateway ${Math.round(client.ws.ping)}ms`); return; } if (interaction.commandName === "warp") { await interaction.deferReply(); const s = await checkWarp("slash-command"); const line = (label, r) => r.ok ? `${label}: \`${r.ip}\` (warp=${r.warp})` : `${label}: n/a (${r.error || `warp=${r.warp}`})`; await interaction.editReply( `**Cloudflare WARP: ${s.connected ? "connected" : "NOT connected"}**\n` + `${line("IPv4", s.v4)}\n${line("IPv6", s.v6)}\n` + `colo: \`${s.colo}\` location: \`${s.loc}\`\n` + `warp-cli: \`${s.cliStatus}\`\nchecked: ${s.checkedAt}` ); } }); client.on(Events.Error, (err) => log("bot", `client error: ${err.message}`)); client.on(Events.Warn, (msg) => log("bot", `warn: ${msg}`)); for (const sig of ["SIGINT", "SIGTERM"]) { process.on(sig, () => { log("bot", `${sig} received, shutting down`); client.destroy(); process.exit(0); }); } log("bot", "starting, connecting to Discord gateway through WARP tunnel"); client.login(TOKEN).catch((err) => { log("bot", `login failed: ${err.message}`); process.exit(1); });