1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?

Discord Botを作った話

1
Posted at

概要

Discord Botを維持費0でどこまで作れるか試してみた。
十数人くらいなら余裕で維持できると思われる。
諸事情でコードなどは公開できない。

実は、チャネルに送信されたメッセージに反応するには、DiscordとずっとWebsocketを接続する必要があり、サーバをずっと立てなければならないので値段がかかりやすい。
なので、メッセージ送信検知だけRenderに移管している感じ。deno deployでもいいかも

できること:コマンド / 設定したチャネルにメッセージが来たときに何かする
言語:Typescript
総コード行数:299行
開発期間:昨日の夕方から今日の夜
維持費:0円
構造:package.json一つのモノリポ
image.png

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"
	}
}
tsconfig.json
{
	"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

コマンド登録はローカルで行う。

register.ts
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の両方を使うときにちょい苦労した。

index.ts
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を直で含める必要があった。

index.ts
const discord = new DiscordHono<Env>()
	.command("help", (c) =>

Env構造体はこんな感じ

index.ts
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で作ると勝手に対応するはずなのだが...。これでサーバが落ちていることになって困っていた。
対処はこんな感じ

render.ts
// @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メソッドを使うことになる。

1
0
0

Register as a new user and use Qiita more conveniently

  1. You get articles that match your needs
  2. You can efficiently read back useful information
  3. You can use dark theme
What you can do with signing up
1
0

Delete article

Deleted articles cannot be recovered.

Draft of this article would be also deleted.

Are you sure you want to delete this article?