概要
Discord Botを維持費0でどこまで作れるか試してみた。
十数人くらいなら余裕で維持できると思われる。
諸事情でコードなどは公開できない。
実は、チャネルに送信されたメッセージに反応するには、DiscordとずっとWebsocketを接続する必要があり、サーバをずっと立てなければならないので値段がかかりやすい。
なので、メッセージ送信検知だけRenderに移管している感じ。deno deployでもいいかも
できること:コマンド / 設定したチャネルにメッセージが来たときに何かする
言語:Typescript
総コード行数:299行
開発期間:昨日の夕方から今日の夜
維持費:0円
構造:package.json一つのモノリポ

{
"name": "discord-bot",
"type": "module",
"version": "0.1.0",
"scripts": {
"dev": "wrangler dev",
"deploy": "wrangler deploy --minify",
"run-render": "tsc && node --env-file=.env dist/render-src/render.js",
"run-render-production": "tsc && node dist/render-src/render.js",
"cf-typegen": "wrangler types --env-interface CloudflareBindings",
"register": "tsc && node --env-file=.env dist/src/register.js"
},
"dependencies": {
"@hono/node-server": "^1.19.6",
"@types/node": "^24.10.1",
"discord-hono": "^0.20.1",
"discord.js": "^14.25.1",
"hono": "^4.10.7"
},
"devDependencies": {
"discord-api-types": "^0.38.35",
"wrangler": "^4.51.0",
"typescript": "^5.9.3"
}
}
{
"compilerOptions": {
/* Visit https://aka.ms/tsconfig.json to read more about this file */
/* Language and Environment */
"target": "ESNext",
"lib": ["ESNext"],
"jsx": "react-jsx",
"jsxImportSource": "hono/jsx",
/* Modules */
"module": "ESNext",
"moduleResolution": "Bundler",
"types": ["./worker-configuration.d.ts", "node"],
"resolveJsonModule": true,
/* JavaScript Support */
"allowJs": true,
"checkJs": false,
/* Emit */
"outDir": "./dist",
/* Interop Constraints */
"isolatedModules": true,
"allowSyntheticDefaultImports": true,
"forceConsistentCasingInFileNames": true,
/* Type Checking */
"strict": true,
/* Completeness */
"skipLibCheck": true,
/* tsc Compile */
//"allowImportingTsExtensions": true,
//"rewriteRelativeImportExtensions": true,
"verbatimModuleSyntax": true
},
"exclude": ["test"],
"include": [
"worker-configuration.d.ts",
"src/**/*.ts",
"render-src/render.ts"
]
}
仕組み
メイン:Cloudflare Workers
サブ :Render
監視 :uptimerobot
Workers
メインのやつ。可能な限り処理はここで行う。
コマンドによるチャネル登録とかがあると、renderに通知する。
Wrangler.jsoncを用いて、KVとかも使えるようにしてる。
サービス
- Worker: コマンド(discord用語)処理 + サブ処理
- KV: データ保存
フレームワーク
- wrangler
- discord-hono
- discord-api-types
- hono
- typescript
コマンド登録はローカルで行う。
import { Command, Option, register, SubCommand } from "discord-hono";
const commands = [
new Command("help", "Docs URL"),
new Command("channel", "Manage target channels").options(
new SubCommand("add", "Add target channel").options(
new Option("id", "チャネルID").required(),
),
new SubCommand("remove", "Remove target channel").options(
new Option("id", "チャネルID").required(),
),
new SubCommand("list", "List target channels"),
),
];
register(
commands,
process.env.DISCORD_APPLICATION_ID,
process.env.DISCORD_TOKEN,
// process.env.DISCORD_TEST_GUILD_ID,
);
discord-honoとhononの両方を使うときにちょい苦労した。
hono.mount("/", async (request: Request, ...args: any) => {
// console.log('DiscordHono received a request:', request);
let resp = await discord.fetch(request, ...args);
return resp;
});
無駄なコードに見えるが、GCで下記のdiscordが持ってかれるようなので、exportで保持されるhonoの中にdiscordを直で含める必要があった。
const discord = new DiscordHono<Env>()
.command("help", (c) =>
Env構造体はこんな感じ
export interface Env {
Bindings: {
CHANNELS_KV: KVNamespace;
MESSAGE_IDS_KV: KVNamespace;
DISCORD_TOKEN: string;
};
Variables: {
id: string;
};
}
render
暫くすると落ちるやつ。実は一か月つけっぱ程度なら無料枠ととんとんで行けるので、落ちないようにしても問題ない。
uptimerobotで監視+落ちないようになっている。
denoも検討したが、月のCPU時間20時間はなんか不安だったのでやめた
起動すると、Workersの方からチャネル一覧をfetchする。
サービス
- nodeサーバ一つ
フレームワーク
- discord.js
- hono
- @hono/node-server
- typescript
uptimerobotはHEADメソッドでfetchするのだが、何故か500が返っていた。honoはGETで作ると勝手に対応するはずなのだが...。これでサーバが落ちていることになって困っていた。
対処はこんな感じ
// @ts-ignore
serve({ ...app, port: 8787, createServer: (
// @ts-ignore
options,
// @ts-ignore
request,
) => {
console.log("request", request)
return createHttpServer(options, (req, res) => {
// HEAD method handling
if (req.method === "HEAD") {
res.writeHead(200, { 'Content-Type': 'text/plain' });
res.end();
return;
}
request(req, res);
});
} }, (info) => {
console.log(`Hono server is running on http://${info.address}:${info.port}`);
});
uptimerobot
5分ごとにPINGを送って生存を確かめるやつ
無課金だとHEADメソッドを使うことになる。