Initial Discord bot in Docker behind a per-container Cloudflare WARP tunnel
- discord.js 14 bot with /ping and /warp, periodic WARP egress-IP logging - Dockerfile: node:22-bookworm-slim + cloudflare-warp 2026.7.1377.0 (.deb) - entrypoint boots dbus/warp-svc, registers, connects, verifies warp=on - docker-compose with NET_ADMIN, /dev/net/tun, sysctls, healthcheck - deploy script, README, CHANGELOG, lockfile generated inside Docker Co-Authored-By: Claude Fable 5.1 <noreply@anthropic.com>
This commit is contained in:
Generated
+315
@@ -0,0 +1,315 @@
|
||||
{
|
||||
"name": "discordbot-cloudflarewarp",
|
||||
"version": "1.0.0",
|
||||
"lockfileVersion": 3,
|
||||
"requires": true,
|
||||
"packages": {
|
||||
"": {
|
||||
"name": "discordbot-cloudflarewarp",
|
||||
"version": "1.0.0",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"discord.js": "^14.16.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/builders": {
|
||||
"version": "1.14.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/builders/-/builders-1.14.1.tgz",
|
||||
"integrity": "sha512-gSKkhXLqs96TCzk66VZuHHl8z2bQMJFGwrXC0f33ngK+FLNau4hU1PYny3DNJfNdSH+gVMzE85/d5FQ2BpcNwQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@discordjs/formatters": "^0.6.2",
|
||||
"@discordjs/util": "^1.2.0",
|
||||
"@sapphire/shapeshift": "^4.0.0",
|
||||
"discord-api-types": "^0.38.40",
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"ts-mixer": "^6.0.4",
|
||||
"tslib": "^2.6.3"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/collection": {
|
||||
"version": "1.5.3",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-1.5.3.tgz",
|
||||
"integrity": "sha512-SVb428OMd3WO1paV3rm6tSjM4wC+Kecaa1EUGX7vc6/fddvw/6lg90z4QtCqm21zvVe92vMMDt9+DkIvjXImQQ==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=16.11.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/formatters": {
|
||||
"version": "0.6.2",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/formatters/-/formatters-0.6.2.tgz",
|
||||
"integrity": "sha512-y4UPwWhH6vChKRkGdMB4odasUbHOUwy7KL+OVwF86PvT6QVOwElx+TiI1/6kcmcEe+g5YRXJFiXSXUdabqZOvQ==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"discord-api-types": "^0.38.33"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/rest": {
|
||||
"version": "2.6.3",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/rest/-/rest-2.6.3.tgz",
|
||||
"integrity": "sha512-wvOylxNYJkwKjctS/Mn5GP1w9r3/rzyH+ThD1JlAca6zEdlHs8QWBBUQJpU5Q+W6DoIj/Ljh1IPlZs7hTU+UAg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@discordjs/collection": "^2.1.1",
|
||||
"@discordjs/util": "^1.2.0",
|
||||
"@sapphire/async-queue": "^1.5.3",
|
||||
"@sapphire/snowflake": "^3.5.5",
|
||||
"@vladfrangu/async_event_emitter": "^2.4.6",
|
||||
"discord-api-types": "^0.38.50",
|
||||
"magic-bytes.js": "^1.13.0",
|
||||
"tslib": "^2.6.3",
|
||||
"undici": "^6.27.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/rest/node_modules/@discordjs/collection": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
|
||||
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/util": {
|
||||
"version": "1.2.0",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/util/-/util-1.2.0.tgz",
|
||||
"integrity": "sha512-3LKP7F2+atl9vJFhaBjn4nOaSWahZ/yWjOvA4e5pnXkt2qyXRCHLxoBQy81GFtLGCq7K9lPm9R517M1U+/90Qg==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"discord-api-types": "^0.38.33"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/ws": {
|
||||
"version": "1.2.3",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/ws/-/ws-1.2.3.tgz",
|
||||
"integrity": "sha512-wPlQDxEmlDg5IxhJPuxXr3Vy9AjYq5xCvFWGJyD7w7Np8ZGu+Mc+97LCoEc/+AYCo2IDpKioiH0/c/mj5ZR9Uw==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@discordjs/collection": "^2.1.0",
|
||||
"@discordjs/rest": "^2.5.1",
|
||||
"@discordjs/util": "^1.1.0",
|
||||
"@sapphire/async-queue": "^1.5.2",
|
||||
"@types/ws": "^8.5.10",
|
||||
"@vladfrangu/async_event_emitter": "^2.2.4",
|
||||
"discord-api-types": "^0.38.1",
|
||||
"tslib": "^2.6.2",
|
||||
"ws": "^8.17.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=16.11.0"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@discordjs/ws/node_modules/@discordjs/collection": {
|
||||
"version": "2.1.1",
|
||||
"resolved": "https://registry.npmjs.org/@discordjs/collection/-/collection-2.1.1.tgz",
|
||||
"integrity": "sha512-LiSusze9Tc7qF03sLCujF5iZp7K+vRNEDBZ86FT9aQAv3vxMLihUvKvpsCWiQ2DJq1tVckopKm1rxomgNUc9hg==",
|
||||
"license": "Apache-2.0",
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/@sapphire/async-queue": {
|
||||
"version": "1.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/async-queue/-/async-queue-1.5.5.tgz",
|
||||
"integrity": "sha512-cvGzxbba6sav2zZkH8GPf2oGk9yYoD5qrNWdu9fRehifgnFZJMV+nuy2nON2roRO4yQQ+v7MK/Pktl/HgfsUXg==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=v14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@sapphire/shapeshift": {
|
||||
"version": "4.0.0",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/shapeshift/-/shapeshift-4.0.0.tgz",
|
||||
"integrity": "sha512-d9dUmWVA7MMiKobL3VpLF8P2aeanRTu6ypG2OIaEv/ZHH/SUQ2iHOVyi5wAPjQ+HmnMuL0whK9ez8I/raWbtIg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"fast-deep-equal": "^3.1.3",
|
||||
"lodash": "^4.17.21"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=v16"
|
||||
}
|
||||
},
|
||||
"node_modules/@sapphire/snowflake": {
|
||||
"version": "3.5.5",
|
||||
"resolved": "https://registry.npmjs.org/@sapphire/snowflake/-/snowflake-3.5.5.tgz",
|
||||
"integrity": "sha512-xzvBr1Q1c4lCe7i6sRnrofxeO1QTP/LKQ6A6qy0iB4x5yfiSfARMEQEghojzTNALDTcv8En04qYNIco9/K9eZQ==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=v14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/node": {
|
||||
"version": "26.5.0",
|
||||
"resolved": "https://registry.npmjs.org/@types/node/-/node-26.5.0.tgz",
|
||||
"integrity": "sha512-dVSGpriSoCgz8WnDNTuSSuSv1PC/ALXihO4ulRZt7Md8k9mlbdin3lGOcDE8SnWOgf513ByWlXd7BK4azmyg/A==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"undici-types": "~8.9.0"
|
||||
}
|
||||
},
|
||||
"node_modules/@types/ws": {
|
||||
"version": "8.18.1",
|
||||
"resolved": "https://registry.npmjs.org/@types/ws/-/ws-8.18.1.tgz",
|
||||
"integrity": "sha512-ThVF6DCVhA8kUGy+aazFQ4kXQ7E1Ty7A3ypFOe0IcJV8O/M511G99AW24irKrW56Wt44yG9+ij8FaqoBGkuBXg==",
|
||||
"license": "MIT",
|
||||
"dependencies": {
|
||||
"@types/node": "*"
|
||||
}
|
||||
},
|
||||
"node_modules/@vladfrangu/async_event_emitter": {
|
||||
"version": "2.4.7",
|
||||
"resolved": "https://registry.npmjs.org/@vladfrangu/async_event_emitter/-/async_event_emitter-2.4.7.tgz",
|
||||
"integrity": "sha512-Xfe6rpCTxSxfbswi/W/Pz7zp1WWSNn4A0eW4mLkQUewCrXXtMj31lCg+iQyTkh/CkusZSq9eDflu7tjEDXUY6g==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=v14.0.0",
|
||||
"npm": ">=7.0.0"
|
||||
}
|
||||
},
|
||||
"node_modules/discord-api-types": {
|
||||
"version": "0.38.55",
|
||||
"resolved": "https://registry.npmjs.org/discord-api-types/-/discord-api-types-0.38.55.tgz",
|
||||
"integrity": "sha512-ytuaRTzdnHUCXJ6KjL9MrItQX0xKncBKEeYI1Bst4+ud47eejH3cG6gaesYakjpPcUhh68XYS2YwGq3mmujFsA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/discord.js": {
|
||||
"version": "14.27.0",
|
||||
"resolved": "https://registry.npmjs.org/discord.js/-/discord.js-14.27.0.tgz",
|
||||
"integrity": "sha512-qHbFlFG2N7y3LjPySYsL6A1+BnX6bkTVgo842EX0CqVPk/KTMwZkojPHEXKsQUpWZNyz5BISNHK1cPpQw0+m4A==",
|
||||
"license": "Apache-2.0",
|
||||
"dependencies": {
|
||||
"@discordjs/builders": "^1.14.1",
|
||||
"@discordjs/collection": "1.5.3",
|
||||
"@discordjs/formatters": "^0.6.2",
|
||||
"@discordjs/rest": "^2.6.2",
|
||||
"@discordjs/util": "^1.2.0",
|
||||
"@discordjs/ws": "^1.2.3",
|
||||
"@sapphire/snowflake": "3.5.5",
|
||||
"discord-api-types": "^0.38.49",
|
||||
"fast-deep-equal": "3.1.3",
|
||||
"lodash.snakecase": "4.1.1",
|
||||
"magic-bytes.js": "^1.13.0",
|
||||
"tslib": "^2.6.3",
|
||||
"undici": "^6.27.0"
|
||||
},
|
||||
"engines": {
|
||||
"node": ">=18"
|
||||
},
|
||||
"funding": {
|
||||
"url": "https://github.com/discordjs/discord.js?sponsor"
|
||||
}
|
||||
},
|
||||
"node_modules/fast-deep-equal": {
|
||||
"version": "3.1.3",
|
||||
"resolved": "https://registry.npmjs.org/fast-deep-equal/-/fast-deep-equal-3.1.3.tgz",
|
||||
"integrity": "sha512-f3qQ9oQy9j2AhBe/H9VC91wLmKBCCU/gDOnKNAYG5hswO7BLKj09Hc5HYNz9cGI++xlpDCIgDaitVs03ATR84Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash": {
|
||||
"version": "4.18.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash/-/lodash-4.18.1.tgz",
|
||||
"integrity": "sha512-dMInicTPVE8d1e5otfwmmjlxkZoUpiVLwyeTdUsi/Caj/gfzzblBcCE5sRHV/AsjuCmxWrte2TNGSYuCeCq+0Q==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/lodash.snakecase": {
|
||||
"version": "4.1.1",
|
||||
"resolved": "https://registry.npmjs.org/lodash.snakecase/-/lodash.snakecase-4.1.1.tgz",
|
||||
"integrity": "sha512-QZ1d4xoBHYUeuouhEq3lk3Uq7ldgyFXGBhg04+oRLnIz8o9T65Eh+8YdroUwn846zchkA9yDsDl5CVVaV2nqYw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/magic-bytes.js": {
|
||||
"version": "1.13.1",
|
||||
"resolved": "https://registry.npmjs.org/magic-bytes.js/-/magic-bytes.js-1.13.1.tgz",
|
||||
"integrity": "sha512-x5sn4UX2k5gCWlcfmoFwG4TPie8+dctESyqOBdhB5p6MsgWXdBKGmt9nXPObj/JI50TTL928lc5Yt1WntMn1bw==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ts-mixer": {
|
||||
"version": "6.0.4",
|
||||
"resolved": "https://registry.npmjs.org/ts-mixer/-/ts-mixer-6.0.4.tgz",
|
||||
"integrity": "sha512-ufKpbmrugz5Aou4wcr5Wc1UUFWOLhq+Fm6qa6P0w0K5Qw2yhaUoiWszhCVuNQyNwrlGiscHOmqYoAox1PtvgjA==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/tslib": {
|
||||
"version": "2.8.1",
|
||||
"resolved": "https://registry.npmjs.org/tslib/-/tslib-2.8.1.tgz",
|
||||
"integrity": "sha512-oJFu94HQb+KVduSUQL7wnpmqnfmLsOA/nAh6b6EH0wCEoK0/mPeXU6c3wKDV83MkOuHPRHtSXKKU99IBazS/2w==",
|
||||
"license": "0BSD"
|
||||
},
|
||||
"node_modules/undici": {
|
||||
"version": "6.28.1",
|
||||
"resolved": "https://registry.npmjs.org/undici/-/undici-6.28.1.tgz",
|
||||
"integrity": "sha512-zWpdTVD54H48CIybL0rWQ3ukpb9d23wM7eH5RtfdmeP70cWHNjtfo7P4vZX+5CoDcO53J4Pu5uXp7lNfjc6DRA==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=18.17"
|
||||
}
|
||||
},
|
||||
"node_modules/undici-types": {
|
||||
"version": "8.9.0",
|
||||
"resolved": "https://registry.npmjs.org/undici-types/-/undici-types-8.9.0.tgz",
|
||||
"integrity": "sha512-KTDyRTYX8sWmKXAikPHHSyc63CRPETMctyjKFupcC6OBLXT3xsN0e9aF7m+mIXutFWpUXuedtowG7iLOzp0kQg==",
|
||||
"license": "MIT"
|
||||
},
|
||||
"node_modules/ws": {
|
||||
"version": "8.21.3",
|
||||
"resolved": "https://registry.npmjs.org/ws/-/ws-8.21.3.tgz",
|
||||
"integrity": "sha512-201TZ/kPWxoPr/OKWjquZR1SWKXcvxdH+e1xrx89b3YbmzLMFCLfnaG1HFIgWzJOEWZ7MvpK++odZufgYR50Rw==",
|
||||
"license": "MIT",
|
||||
"engines": {
|
||||
"node": ">=10.0.0"
|
||||
},
|
||||
"peerDependencies": {
|
||||
"bufferutil": "^4.0.1",
|
||||
"utf-8-validate": ">=5.0.2"
|
||||
},
|
||||
"peerDependenciesMeta": {
|
||||
"bufferutil": {
|
||||
"optional": true
|
||||
},
|
||||
"utf-8-validate": {
|
||||
"optional": true
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,18 @@
|
||||
{
|
||||
"name": "discordbot-cloudflarewarp",
|
||||
"version": "1.0.0",
|
||||
"description": "Simple discord.js bot that runs behind a Cloudflare WARP tunnel and logs the Cloudflare IP it uses",
|
||||
"main": "src/index.js",
|
||||
"type": "module",
|
||||
"private": true,
|
||||
"engines": {
|
||||
"node": ">=22"
|
||||
},
|
||||
"scripts": {
|
||||
"start": "node src/index.js"
|
||||
},
|
||||
"dependencies": {
|
||||
"discord.js": "^14.16.3"
|
||||
},
|
||||
"license": "MIT"
|
||||
}
|
||||
@@ -0,0 +1,180 @@
|
||||
import { execFile } from "node:child_process";
|
||||
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);
|
||||
const TRACE_URL = "https://www.cloudflare.com/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
|
||||
// ---------------------------------------------------------------------------
|
||||
|
||||
/**
|
||||
* Ask Cloudflare's trace endpoint what it sees. This is the authoritative
|
||||
* check: `warp=on` (or `warp=plus`) only appears when the request reached
|
||||
* Cloudflare through the WARP tunnel. `ip=` is the public egress IP.
|
||||
*/
|
||||
async function fetchTrace() {
|
||||
const res = await fetch(TRACE_URL, { signal: AbortSignal.timeout(10_000) });
|
||||
if (!res.ok) throw new Error(`trace HTTP ${res.status}`);
|
||||
const text = await res.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;
|
||||
}
|
||||
|
||||
/** 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 cliStatus = await warpCliStatus();
|
||||
try {
|
||||
const t = await fetchTrace();
|
||||
const connected = t.warp === "on" || t.warp === "plus";
|
||||
const summary =
|
||||
`ip=${t.ip} warp=${t.warp} colo=${t.colo} loc=${t.loc} ` +
|
||||
`http=${t.http} tls=${t.tls} | warp-cli: ${cliStatus}`;
|
||||
if (connected) {
|
||||
log("WARP", `CONNECTED via Cloudflare (${reason}) ${summary}`);
|
||||
} else {
|
||||
log("WARP", `NOT CONNECTED (${reason}) ${summary}`);
|
||||
await tryReconnect();
|
||||
}
|
||||
lastWarp = { ...t, connected, checkedAt: ts(), cliStatus };
|
||||
} catch (err) {
|
||||
log("WARP", `check failed (${reason}): ${err.message} | warp-cli: ${cliStatus}`);
|
||||
lastWarp = { connected: false, error: err.message, checkedAt: ts(), cliStatus };
|
||||
await tryReconnect();
|
||||
}
|
||||
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");
|
||||
if (s.connected) {
|
||||
await interaction.editReply(
|
||||
`**Cloudflare WARP: connected**\n` +
|
||||
`IP: \`${s.ip}\`\nwarp: \`${s.warp}\`\ncolo: \`${s.colo}\`\nlocation: \`${s.loc}\`\n` +
|
||||
`warp-cli: \`${s.cliStatus}\`\nchecked: ${s.checkedAt}`
|
||||
);
|
||||
} else {
|
||||
await interaction.editReply(
|
||||
`**Cloudflare WARP: NOT connected**\n${s.error ? `error: \`${s.error}\`\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);
|
||||
});
|
||||
Reference in New Issue
Block a user