🔥

honoとCloudflareでLINE Botを作る

2024/06/07に公開

今話題(?)のhonoとCloudflare Workersで試しにLINE Botを作ってみました。
以下に全体のコードを公開しています。

https://github.com/tokku5552/hono-cloudflare-sample

まずはhonoのチュートリアルをやってみる

honoのgetting-startedのページにはベーシックなものからCloudflare、AWS Lambda、Vercelなど各デプロイ先に即したチュートリアルページが用意されています。
今回はこの中からCloudflare Workersを選択して実施しました。

https://hono.dev/getting-started/cloudflare-workers

詳細は公式ページを参照いただければと思いますが以下の4行でデプロイまではすんなり済んでしまいます。

yarn create hono my-app
cd my-app
yarn
yarn deploy

LINE Botに作り変える

以前作ったCloud Run x Expressでのコードを移植し、honoに書き換えていきます。

https://zenn.dev/tokku5552/books/linedc-gcp-handson-202403

import { Hono } from 'hono';

type Bindings = {
  LINE_CHANNEL_ACCESS_TOKEN: string;
  LINE_CHANNEL_SECRET: string;
};

const app = new Hono<{ Bindings: Bindings }>();

Bindingsとして環境変数を定義しておいて、const app = new Hono<{ Bindings: Bindings }>();のように渡すとc.envで値を取得できるようになります。
KV namespaces, D1 database, R2 bucketなどもここで定義してcontext経由で使用するようです。
個人的には少し変な感じがしますが、型もちゃんと付くので慣れれば特に問題ない気がします。

https://hono.dev/api/context

次にメインの/webhookエンドポイントです。

import * as line from '@line/bot-sdk';

app.post('/webhook', async (c) => {
  const config: line.ClientConfig = {
    channelAccessToken: c.env.LINE_CHANNEL_ACCESS_TOKEN,
  };
  const client = new line.messagingApi.MessagingApiClient(config);
  line.middleware({ channelSecret: c.env.LINE_CHANNEL_SECRET });

  const events: line.WebhookEvent[] = await c.req.json().then((data) => data.events);

  await Promise.all(
    events.map(async (event: line.WebhookEvent) => {
      try {
        await textEventHandler(client, event);
      } catch (err: unknown) {
        if (err instanceof Error) {
          console.error(err);
        }
        return c.status(500);
      }
    }),
  );

  return c.status(200);
});

Cloud Runにデプロイしたときのコードをほぼ流用していますが、@line/bot-sdkの使い方が少し変わっていました。

https://line.github.io/line-bot-sdk-nodejs/getting-started/basic-usage.html

公式によるとimport * as line from '@line/bot-sdk';のように名前空間のimportを行い、さらにsecretはclient生成時ではなくline.middlewareを通して設定するようです。

// ES Modules or TypeScript
import * as line from '@line/bot-sdk';

new line.messagingApi.MessagingApiClient({
  channelAccessToken: 'YOUR_CHANNEL_ACCESS_TOKEN',
});
line.middleware({
  channelSecret: 'YOUR_CHANNEL_SECRET'
});

また、この際以下の記事にあるようなエラーが出たので、wrangler.tomlに設定を追加しています。

https://developers.cloudflare.com/workers/runtime-apis/nodejs/#enable-nodejs-with-workers

wrangler.toml
compatibility_flags = [ "nodejs_compat" ]

最後にtextEventHandlerという関数ですが、こちらは前回のリポジトリのコードからBigQueryの処理を省いて、それ以外はほぼそのまま流用しています。

const textEventHandler = async (
  client: line.messagingApi.MessagingApiClient,
  event: line.WebhookEvent,
): Promise<line.MessageAPIResponseBase | undefined> => {
  if (event.type !== 'message' || event.message.type !== 'text') {
    return;
  }

  const { replyToken, message: { text } = {} } = event;

  const response: line.TextMessage = {
    type: 'text',
    text: `${text}と言われましても`,
  };

  const replyMessageRequest: line.messagingApi.ReplyMessageRequest = {
    replyToken: replyToken,
    messages: [response],
  };

  await client.replyMessage(replyMessageRequest);
};

Secretの設定とデプロイ

Bindingsに定義している環境変数ですが、公開して良いものであればwrangler.tomlに直接記載することができます。

https://developers.cloudflare.com/workers/configuration/environment-variables/#add-environment-variables-via-wrangler

wrangler.toml
name = "my-worker-dev"

[vars]
API_HOST = "example.com"
API_ACCOUNT_ID = "example_user"
SERVICE_X_DATA = { URL = "service-x-api.dev.example", MY_ID = 123 }

ただしこれだとリポジトリにcommitすることになりますし、Cloudflareのコンソールでも値が見えてしまいます。
ですので、シークレットの場合は以下の様にコマンドにて設定すると良さそうです。

https://developers.cloudflare.com/workers/wrangler/commands/#put-3

LINE_CHANNEL_ACCESS_TOKEN=your_value
LINE_CHANNEL_SECRET=your_value
echo $LINE_CHANNEL_ACCESS_TOKEN | yarn wrangler secret put LINE_CHANNEL_ACCESS_TOKEN
echo $LINE_CHANNEL_SECRET | yarn wrangler secret put LINE_CHANNEL_SECRET

これでオウム返しのLINE Botができました🎉

まとめ

かなり簡単にLINE Botをデプロイできました。
CloudflareだとこのままD1などと繋げば本格的なBotの開発もスムーズにできそうでした。

この記事は以下の作業配信で経た知見をまとめたものです。試行錯誤をする姿が見たい方はぜひ御覧ください笑

https://www.youtube.com/live/u3kYzBqOHn0?si=ow6uOzUO-7UAzeyV

Discussion