Cloudflare workersのLINE Botに重めの処理を接続する

以前、Clouflare wokersにhonoでLINE Botを作成する記事を作成しました

https://qiita.com/watanabe-tsubasa/items/24da39d2b38d348270e9

その末尾にあとはよしなになんかと繋げて使ってください！というメッセージを残したのですが、流石に投げっぱなしは忍びないと思いまして、

- ノーコードツール、makeでwebhookを作る
- cloudflare workersからそのwebhookにリクエストを送る
- 結果をBotのメッセージとして返す

というところを実際に実装してみたいと思います。

## 0. 準備

### 0.1. makeのアカウント登録

そもそもmakeとは、ノーコードでアプリケーションを繋ぎ合わせることができるサービスです。
いわゆるiPaaS(integration Platform as a Service) に該当し、さまざまなサービスやアプリケーションを繋ぎ合わせることで、独自のアプリケーションを構築することができます。

https://www.make.com/en

こちらからGoogleなどのソーシャルログインで問題ありませんので登録しておいてください。

### 0.2. makeでGPTと会話できるLINE Botの作成

https://qiita.com/watanabe-tsubasa/items/8342595d641143b780f7

少々古いですが、こちらの3-3までを実施いただけると、本記事の意味合いがわかりやすいかと思います

### 0.3. Cloudflare workers上で動作するecho botの作成

https://qiita.com/watanabe-tsubasa/items/24da39d2b38d348270e9

この記事の続きですので、おうむ返しBotができているものとします。

## 1. ハンズオン

### 1.0. 全体を通して作るもの

今回は`Cloudflare`から`make`にリクエストを送り、makeの中で具体的なロジックを実行（サンプル例はGPTのAPIです）。再度Cloudflareに返して、結果を返答するLINE Botを作成します。
そのために、`make側の準備を整えたのち`、`Cloudflare側のコードを書き換え`、`つなぎこむ`、という手順で進めます

### 1.1. makeのLINE Botをwebhook経由のAPIに切り替える

まずは`0.2`で作成したLINE Botについて、これをAPIに切り替えることでCloudflareと繋げる準備をします。

#### 1.1.1. 両端のLINE モジュールを削除して、webhook モジュールに切り替える

- 今、こんな感じだと思いますので、両側のLINEモジュールを削除します

> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/a87cd69a-59e7-1bdd-f33f-0a2a97faf7d5.png" width="75%" />

- 削除したら、Webhooksのモジュールを選んでください

> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/cdecab92-db41-09d6-878a-5f29a476b9aa.png" width="75%" />

- 選ぶのはCustom webhookです

> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/ee21c1f1-18f8-e3a2-2bc0-5d4c7b502fab.png" width="75%" />

- `Create a webhook`のボタンを押します

> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/518a6227-ee00-c80c-d48c-06e4975a8aaf.png" width="75%" />

- わかりやすい名前をつけて、`save`ボタンを押してください

> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/50fad9bf-350d-3b60-bfa8-c7568cb9e8f1.png" width="75%" />

- こんな感じの待機画面になったらひとまずOKです

> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/582c9438-832a-7ba3-559f-e282261f5d56.png" width="75%" />

- 一旦`stop`を押して止めてください

> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/6704bb08-84eb-8ef6-abb2-012b6b3dce45.png" width="75%" />

- 末尾に同様のwebhooksモジュール（webhook response）を作ったら完成です

> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/25ff7b40-2e46-0021-bccc-e5589d0260fd.png" width="75%" />

#### 1.1.2. 補足｜さっきの待機画面が気になる方向け

<details><summary>詳細は折りたたみで表示、読み飛ばして問題ないです</summary>

先ほどは、webからのリクエストを待機していました。
表示されていたwebhook URLに対して、実際のリクエストを送ることで、**データ構造を定義して、makeの中で使えるように**してくれます。
なので、このあとcloudflare側の準備を整えて、繋ぎ込んでいきます。

</details>

### 1.2. Cloudflareとmakeと繋ぐ

それでは、makeとCloudflareを繋いでいきます。まずは、Cloudflare側で作成したLINE Botを触っていきます。」

#### 1.2.1. Cloudflareのコードを書き換える

次のコードを`index.ts`の中に貼り付けます（前回記事の通り実施している前提です）。

```ts: index.ts
import { 
  messagingApi,
  webhook,
} from '@line/bot-sdk'
import { ExecutionContext, Hono } from 'hono'
import HmacSHA256 from "crypto-js/hmac-sha256";
import Base64 from "crypto-js/enc-base64";

type Bindings = {
  CHANNEL_ACCESS_TOKEN: string,
  CHANNEL_SECRET: string,
  WEBHOOK_URL: string,
}

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

app.get('/', (c) => {
  return c.text('Hello hono & line!')
})

app.post('/webhook', async (c) => {
  const body = await c.req.text() // JSONではなくテキストで取得
  const channelAccessToken = c.env.CHANNEL_ACCESS_TOKEN || ''
  const channelSecret = c.env.CHANNEL_SECRET || ''
  const webhookURL = c.env.WEBHOOK_URL || ''

  // シグネチャの取得
  const signature = c.req.header('x-line-signature')
  if (!signature) {
    return c.text('Missing signature', 400)
  }

  // HMACを使ってシグネチャを生成
  const hash = await generateHmac(channelSecret, body);

  // シグネチャの検証
  if (signature !== hash) {
    return c.text('Invalid signature', 403)
  }

  const events = JSON.parse(body).events
  const promises = events.map((event: webhook.Event) => handleEvent(event, channelAccessToken, webhookURL, c.executionCtx))
  await Promise.all(promises)

  return c.text('OK')
})

const handleEvent = async (
  event: webhook.Event,
  accessToken: string,
  webhookURL: string,
  ctx: ExecutionContext
) => {
  if (event.type !== 'message' || event.message.type !== 'text') return;

  try {
    fetch('https://api.line.me/v2/bot/chat/loading/start', {
      method: 'POST',
      headers: {
        "Authorization": `Bearer ${accessToken}`,
        "Content-Type": "application/json"
      },
      body: JSON.stringify({"chatId": event.source?.userId})
    })

    ctx.waitUntil(
      (async () => {
        const res = await fetch(webhookURL, {
          method: 'POST',
          headers: {
            'Content-Type': 'application/json'
          },
          body: JSON.stringify(event)
        })
        const text = await res.text();
        if(!event.replyToken) return
        const responseBody: messagingApi.ReplyMessageRequest = {
          replyToken: event.replyToken,
          messages: [
            {'type': 'text', 'text': text}
          ] 
        }
        
        return fetch('https://api.line.me/v2/bot/message/reply', {
          method: 'POST',
          headers: {
            "Authorization": `Bearer ${accessToken}`,
            "Content-Type": "application/json"
          },
          body: JSON.stringify(responseBody)
        }) 
      })()
    )
  } catch (error) {
    console.error(error);
  }
}

export default app

const generateHmac = async (secret: string, message: string) => {
  const hmac = HmacSHA256(message, secret);
  return Base64.stringify(hmac);
}
```

#### 1.2.2. 環境変数の設定

環境変数として、makeのwebhook URLを設定し、Cloudflare側からリクエストを送れるようにします。

- 次のコマンドをターミナルに入力してください

```bash
bunx wrangler secret put WEBHOOK_URL
```

- 次のようにコピペしてくれと言われます

> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/cdaf8a6b-5c1d-c960-2b28-685769073f89.png" width="75%" />

- make側に戻り、`Redetermine data structure`ボタンを押します

> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/2887ac89-279e-3022-1ef3-6d2d25ff07f3.png" width="75%" />

- `Copy address to clipboard`ボタンを押して、URLをコピーします

> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/2ebe12d7-ee8d-0771-0982-a84c99745a3a.png" width="75%" />

- コピーしたコードをコピペしてenter

> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/89333d54-189a-dd8c-fde1-f3adeb857b3e.png" width="75%" />

- 成功したら、画面のようになります

> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/d4c3df85-df21-6db0-1a00-e9cdecf129bc.png" width="75%" />

- 次のコマンドを入力してデプロイ

```bash
bun run deploy
```

#### 1.2.3. webhookにリクエストを送る

- なんでも良いので、LINEからメッセージを送ってみます
- うまくいくと、LINEに`Accepted`と表示されます

> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/0d15b5e9-cf83-3c58-9c9a-af6c9cc32842.png" width="75%" />

- またモジュール側にも`Successfuly determined`と表示されます

> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/eed578f4-e74f-a88b-d457-d9247f2ef215.png" width="75%" />

#### 1.2.4. OpenAIのモジュールにwebhookの値を入れる

- 先ほどの手順で、OpenAIのモジュールにデータ構造が定義されています

> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/d117c63a-89d6-6770-12ae-fac8425b7e24.png" width="75%" />

- text contentにwebhooks経由できた値を入れます

> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/7ef8f6d1-ca21-58f1-cc56-96b12cb96395.png" width="75%" />

- webhooksのBodyにOpenAIから返ってきた値を入れます

> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/0ad94f32-439e-e8af-bedf-8c3b248f22b8.png" width="75%" />

#### 1.2.5. 補足｜コードの変更点

<details><summary>詳細は折りたたみで表示、読み飛ばして問題ないです</summary>

先ほどは、webからのリクエストを待機していました。
表示されていたwebhook URLに対して、実際のリクエストを送ることで、**データ構造を定義して、makeの中で使えるように**してくれます。
なので、このあとcloudflare側の準備を整えて、繋ぎ込んでいきます。

</details>

### 1.3. 実行してみる

- Run onceを押してみます

> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/a696e2b0-3e09-15ec-4ec4-cb3a96f34ec4.png" width="75%" />

- LINE からメッセージを送ってみましょう

> <img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2100048/b06d397b-c613-7fa1-0516-9f4bc399d186.png" width="75%" />

まあはやいかな

## 2. 動作しているところ

https://x.com/shuyin02/status/1826424450243592446

https://developers.cloudflare.com/workers/runtime-apis/context/

動作の肝はこれですね
honoのExecutionContextにあるwaitUntilがポイントです