生成AIにLINE Botを作ってもらう GPT4o編 その3

# はじめに

[前回](https://qiita.com/bathtimefish/items/6d0553fe0a782afe146e)の続きです。前回の様子から、最初のプロンプトで生成したコードのexpress味がなかなか抜けないので、新しいチャットを作成してイチからやり直してみたいと思います。

# シンプルなエコーサーバーを作ってもらう

まずはDenoで動作するシンプルなサーバーを書いてもらいました

```prompt.txt
Denoで動作するシンプルなエコーサーバーをTypeScriptで書いてください。
```

## 回答(転載)

Denoで動作するシンプルなエコーサーバーをTypeScriptで実装する例を以下に示します。このサーバーは、クライアントから送信されたリクエストの内容をそのまま返すエコーサーバーです。

```echo_server.ts
import { serve } from "https://deno.land/std@0.203.0/http/server.ts";

// エコーサーバー
async function echoServer(req: Request): Promise<Response> {
  const { method, url, headers } = req;
  const body = await req.text();

  // クライアントからのリクエスト内容をそのまま返す
  const responseBody = {
    method,
    url,
    headers: [...headers],
    body,
  };

  return new Response(JSON.stringify(responseBody, null, 2), {
    headers: { "Content-Type": "application/json" },
  });
}

// ポート8000でサーバーを起動
console.log("Server running on http://localhost:8000");
serve(echoServer, { port: 8000 });
```

### 実行方法

1. 上記のコードを echo_server.ts という名前で保存します。
2. 以下のコマンドを実行して、Denoでサーバーを起動します。

```bash
deno run --allow-net echo_server.ts
```

## 実行してみる

```shell
deno run --allow-net echo_server.ts

Server running on http://localhost:8000
Listening on http://localhost:8000/
```

```shell
$ curl http://localhost:8000 -d "hello"
{
  "method": "POST",
  "url": "http://localhost:8000/",
  "headers": [
    [
      "accept",
      "*/*"
    ],
    [
      "content-length",
      "5"
    ],
    [
      "content-type",
      "application/x-www-form-urlencoded"
    ],
    [
      "host",
      "localhost:8000"
    ],
    [
      "user-agent",
      "curl/8.4.0"
    ]
  ],
  "body": "hello"
}
```

正常に動作しました。

# Bot Serverを作ってもらう

この流れでLine Bot Serverを書いてもらいます

```prompt.txt
LINE Botを開発したいです。以下の条件で簡単なLINE Botサーバーのコードを書いてください。

# 条件
1. 上記のコードを参考にDeno上で動作するTypeScriptコードとする
2. LINE Messaging API SDK for nodejs を利用する
3. httpsサーバーとするため、コード中に証明書を設定できるようにする
4. TypeScriptで記述する
5. その他LINE Botとして確実に作動するよう配慮したコードとする
6. コード以外に作動するために必要な設定等があれば解説文を書く
```

## 回答(転載)

Deno上で動作するLINE BotサーバーのコードをTypeScriptで実装する際、基本的には公式のLINE Messaging API SDKを利用しつつ、Denoの環境に適した形で実装する必要があります。ただし、LINE Messaging API SDKはNode.js用に設計されているため、Denoで直接動作させるにはいくつかの設定や変更が必要です。

以下にDeno上で動作する簡単なLINE Botサーバーのコードと、それに必要な設定について説明します。

### LINE Botサーバーのコード

#### 1. 必要な依存関係

DenoではNode.jsのパッケージを使うために esm.sh のようなCDNを利用します。これにより、DenoがNode.js用のパッケージをサポートします。

#### 2. 簡単なLINE Botサーバーの実装

```line_bot_server.ts
import { serveTls } from "https://deno.land/std@0.203.0/http/server.ts";
import { Client, middleware, WebhookEvent } from "https://esm.sh/@line/bot-sdk@8.2.2";
import { config } from "https://deno.land/x/dotenv/mod.ts";

// 環境変数の読み込み
const CHANNEL_ACCESS_TOKEN = config().CHANNEL_ACCESS_TOKEN;
const CHANNEL_SECRET = config().CHANNEL_SECRET;
const PORT = Number(config().PORT) || 443;  // HTTPSはデフォルトでポート443

// 証明書と秘密鍵のパス
const CERT_FILE = "./cert.pem";
const KEY_FILE = "./key.pem";

// LINE Messaging APIクライアントを初期化
const client = new Client({
  channelAccessToken: CHANNEL_ACCESS_TOKEN,
  channelSecret: CHANNEL_SECRET,
});

// Webhookイベントのハンドリング
async function handleEvent(event: WebhookEvent): Promise<void> {
  if (event.type === "message" && event.message.type === "text") {
    const message = {
      type: "text",
      text: `You said: ${event.message.text}`,
    };
    // リプライメッセージを送信
    await client.replyMessage(event.replyToken, message);
  }
}

// LINE Botサーバー
async function lineBotServer(req: Request): Promise<Response> {
  const body = await req.text();
  const signature = req.headers.get("x-line-signature") || "";

  // Webhook検証
  if (!middleware({ channelSecret: CHANNEL_SECRET })(signature, body)) {
    return new Response("Invalid signature", { status: 403 });
  }

  const events: WebhookEvent[] = JSON.parse(body).events;

  // イベントごとにハンドラーを呼び出し
  await Promise.all(events.map(handleEvent));

  return new Response("OK", { status: 200 });
}

// サーバーを起動
console.log(`LINE Bot server is running on https://localhost:${PORT}`);
serveTls(lineBotServer, {
  port: PORT,
  certFile: CERT_FILE,
  keyFile: KEY_FILE,
});
```

#### 3. 必要な設定

##### a. .env ファイルを作成
Denoの環境変数を読み込むために、プロジェクトディレクトリに .env ファイルを作成してください。このファイルにLINEのアクセストークンとチャンネルシークレットを設定します。

Denoの環境変数を読み込むために、プロジェクトディレクトリに .env ファイルを作成してください。このファイルにLINEのアクセストークンとチャンネルシークレットを設定します。

```.env
CHANNEL_ACCESS_TOKEN=YOUR_LINE_CHANNEL_ACCESS_TOKEN
CHANNEL_SECRET=YOUR_LINE_CHANNEL_SECRET
```

##### b. Denoの起動コマンド
DenoでこのLINE Botサーバーを実行するには、以下のコマンドを使用します。

```shell
deno run --allow-net --allow-read --allow-env line_bot_server.ts
```

- `--allow-net` はネットワークアクセスを許可します。
- `--allow-read` はファイルシステムの読み込みを許可します（.env ファイル）。
- `--allow-env` は環境変数の読み込みを許可します。

#### 4. LINE Developersでの設定
LINE Botを正しく動作させるためには、LINE DevelopersでWebhook URLを設定する必要があります。

LINE Developersコンソールにログインし、Botの設定画面に移動します。
Webhook URLを `https://YOUR_DOMAIN/webhook` に設定し、Webhookを有効にします（この例ではローカルでテストする場合、ngrok や他のトンネルサービスを使って外部公開します）。

#### 5. 外部接続のためのngrok設定

ローカルでテストする場合、Denoのサーバーを外部に公開する必要があるので、ngrokを使うことが一般的です。ngrokをインストールして以下のコマンドを実行し、LINE BotのWebhook URLを設定できます。

```shell
ngrok http 8000
```

これにより、外部からアクセス可能なURLが生成されます。それをLINE DevelopersのWebhook URLに設定します。

#### まとめ
このコードと設定を利用することで、Deno上でLINE Botサーバーを作成し、メッセージを受信してエコーを返す基本的なBotを動作させることができます。

## bot-sdkのバージョンを最新にする

```prompt.txt
import { Client, middleware, WebhookEvent } from "https://esm.sh/@line/bot-sdk@8.2.2"; の部分について、bot-sdkを最新バージョンの9.4.0にしてください
```

### 回答(抜粋)
@line/bot-sdk のバージョンを最新の 9.4.0 に変更するには、以下のようにインポート部分を修正します。

```line_bot_server.ts
import { Client, middleware, WebhookEvent } from "https://esm.sh/@line/bot-sdk@9.4.0";
```

この変更後のLINE Botサーバーのコード全体は以下の通りです。

#### 修正後のLINE Botサーバーコード（Deno + TypeScript）

```line_bot_server.ts
import { serveTls } from "https://deno.land/std@0.203.0/http/server.ts";
import { Client, middleware, WebhookEvent } from "https://esm.sh/@line/bot-sdk@9.4.0";
import { config } from "https://deno.land/x/dotenv/mod.ts";

// 環境変数の読み込み
const CHANNEL_ACCESS_TOKEN = config().CHANNEL_ACCESS_TOKEN;
const CHANNEL_SECRET = config().CHANNEL_SECRET;
const PORT = Number(config().PORT) || 443;  // HTTPSはデフォルトでポート443

// 証明書と秘密鍵のパス
const CERT_FILE = "./cert.pem";
const KEY_FILE = "./key.pem";

// LINE Messaging APIクライアントを初期化
const client = new Client({
  channelAccessToken: CHANNEL_ACCESS_TOKEN,
  channelSecret: CHANNEL_SECRET,
});

// Webhookイベントのハンドリング
async function handleEvent(event: WebhookEvent): Promise<void> {
  if (event.type === "message" && event.message.type === "text") {
    const message = {
      type: "text",
      text: `You said: ${event.message.text}`,
    };
    // リプライメッセージを送信
    await client.replyMessage(event.replyToken, message);
  }
}

// LINE Botサーバー
async function lineBotServer(req: Request): Promise<Response> {
  const body = await req.text();
  const signature = req.headers.get("x-line-signature") || "";

  // Webhook検証
  if (!middleware({ channelSecret: CHANNEL_SECRET })(signature, body)) {
    return new Response("Invalid signature", { status: 403 });
  }

  const events: WebhookEvent[] = JSON.parse(body).events;

  // イベントごとにハンドラーを呼び出し
  await Promise.all(events.map(handleEvent));

  return new Response("OK", { status: 200 });
}

// サーバーを起動
console.log(`LINE Bot server is running on https://localhost:${PORT}`);
serveTls(lineBotServer, {
  port: PORT,
  certFile: CERT_FILE,
  keyFile: KEY_FILE,
});

```

(中略)

これにより、9.4.0 バージョンのLINE Messaging API SDKを使ったLINE Botサーバーが動作するようになります。


### 実行してみる

Channel Access Tokenや証明書等必要な設定をして起動します

```shell
deno run --allow-net --allow-read --allow-env line_bot_server.ts
LINE Bot server is running on http://localhost:8000
Listening on http://localhost:8000/
```

正常に起動しました。

### メッセージを送信してみる

LINEチャットで`はろー`と送信してみたところ、以下のエラーが発生しました

```shell
{"destination":"U8720493f98687b859eefa005d0717f75","events":[{"type":"message","message":{"type":"text","id":"529102495096766529","quoteToken":"9IcX7fJOl6rwpvWv8PCif14Y6r_Br17LN6SojXg7q1hT-S1PtRg9ULS2jlO18gegcSrRzuB0km88KLwBrEJU_Q_HlPQuvNjlQg7Rtzb_pj_0X_ZHuh4daXyHlz2OelVgoF7-QOQ1pDQp3a7-UYG9rw","text":"はろー"},"webhookEventId":"01J9GCW0N41VH6N5HK9WXWTBGH","deliveryContext":{"isRedelivery":false},"timestamp":1728200966709,"source":{"type":"user","userId":"U5fee7eec2aa8ef912c3173957ab38509"},"replyToken":"4d43d88bdfd34a21b2d70b9d21193a4e","mode":"active"}]}
error: Uncaught (in promise) TypeError: Cannot read properties of undefined (reading 'x-line-signature')
    at e (https://esm.sh/v135/@line/bot-sdk@9.4.0/denonext/bot-sdk.mjs:3:14669)
    at https://esm.sh/v135/@line/bot-sdk@9.4.0/denonext/bot-sdk.mjs:3:15074
    at Server.lineBotServer (file:///home/ubuntu/llm-linebots/GPT4o-3/line_bot_server.ts:39:51)
    at eventLoopTick (ext:core/01_core.js:168:7)
    at async Server.#respond (https://deno.land/std@0.203.0/http/server.ts:305:18)
```

コードに着目すると、よくわからない`middleware()`の使い方があるので、その部分をコメントアウトしてみます

```line_bot_server.ts
// LINE Botサーバー
async function lineBotServer(req: Request): Promise<Response> {
  const body = await req.text();
  /*
  const signature = req.headers.get("x-line-signature") || "";

  // Webhook検証
  if (!middleware({ channelSecret: CHANNEL_SECRET })(signature, body)) {
    return new Response("Invalid signature", { status: 403 });
  }
  */

  const events: WebhookEvent[] = JSON.parse(body).events;
```

サーバーを再起動してメッセージを送り直すと、正常に返信が返ってきました。

### コードを修正する

上記をもとに修正を指示します

```prompt.txt
コードの以下の部分が不必要なので削除してください

  const signature = req.headers.get("x-line-signature") || "";

  // Webhook検証
  if (!middleware({ channelSecret: CHANNEL_SECRET })(signature, body)) {
    return new Response("Invalid signature", { status: 403 });
  }
```

#### 応答（転載）

不要な部分を削除したコードは以下の通りです。この修正では、x-line-signature の検証を削除していますが、セキュリティ上の理由で本番環境ではx-line-signatureを検証することが推奨されます。

##### 修正後のLINE Botサーバーコード

```line_bot_server.ts
import { serveTls } from "https://deno.land/std@0.203.0/http/server.ts";
import { Client, WebhookEvent } from "https://esm.sh/@line/bot-sdk@9.4.0";
import { config } from "https://deno.land/x/dotenv/mod.ts";

// 環境変数の読み込み
const CHANNEL_ACCESS_TOKEN = config().CHANNEL_ACCESS_TOKEN;
const CHANNEL_SECRET = config().CHANNEL_SECRET;
const PORT = Number(config().PORT) || 443;  // HTTPSはデフォルトでポート443

// 証明書と秘密鍵のパス
const CERT_FILE = "./cert.pem";
const KEY_FILE = "./key.pem";

// LINE Messaging APIクライアントを初期化
const client = new Client({
  channelAccessToken: CHANNEL_ACCESS_TOKEN,
  channelSecret: CHANNEL_SECRET,
});

// Webhookイベントのハンドリング
async function handleEvent(event: WebhookEvent): Promise<void> {
  if (event.type === "message" && event.message.type === "text") {
    const message = {
      type: "text",
      text: `You said: ${event.message.text}`,
    };
    // リプライメッセージを送信
    await client.replyMessage(event.replyToken, message);
  }
}

// LINE Botサーバー
async function lineBotServer(req: Request): Promise<Response> {
  const body = await req.text();
  const events: WebhookEvent[] = JSON.parse(body).events;

  // イベントごとにハンドラーを呼び出し
  await Promise.all(events.map(handleEvent));

  return new Response("OK", { status: 200 });
}

// サーバーを起動
console.log(`LINE Bot server is running on https://localhost:${PORT}`);
serveTls(lineBotServer, {
  port: PORT,
  certFile: CERT_FILE,
  keyFile: KEY_FILE,
});
```

##### 修正内容
- x-line-signatureの検証部分を削除しました。

(中略)

このサーバーは、HTTPSでLINE Messaging APIのWebhookリクエストを受け取り、メッセージを受信してその内容をエコーします。

# 最終版を実行してみる

修正されたコードを実行します

```shell
deno run --allow-net --allow-read --allow-env line_bot_server.ts
```

LINEチャットでメッセージを送信すると正常に応答が返ってきました。

![Screenshot_20241006-170011.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/61995/4c2f37cc-7da5-17a8-28b9-5b30623753c2.png)

# おわりに

紆余曲折ありましたが、なんとかChatGPTのコード生成だけでLINE Botサーバーを動かすことができました。ざっとやってみた感想では4oはnode.jsに比べてDenoは詳しくないかな？という感じでした。まあネットに落ちてるコードサンプルの量は比較にならないので当たり前っちゃ当たり前ですが。

bot-sdkの書き方についてはけっこう分かってる感がありますね。上記で削除したWebhook検証の部分は？ですが。そこはSDKに任せてよくね？と思いました。

今回の結果だけ見ると、TypeScriptやLINE Bot SDKをよく理解できてる人ならば自分でコードを書いたほうが早い印象ですが、特にSDKを知らない人ならばSDKのサンプルコードを書かせて自分なりのサーバーに組み込む。というアプローチが効率良いかもしれません。4oはSDKの典型的な使い方についてはかなり分かってる印象を受けるのでその辺を意識して活用するのが良いかもしれませんね。