LINE BotをBunで外部依存モジュールを使用せずに作ってみる - 2024年1月版


先日書いた記事をベースにBunのAPIに書き換えた版を作ってみます。

https://qiita.com/n0bisuke/items/0ab7b3b38557384370ed

## BunとNode.jsの互換性もあるけど

前提としてBunのランタイムはNode.js互換性があるので、何もしなくてもそのまま（少なくとも今回のコードは）Node.jsで書いたコードも動きます。
そんなに調べてませんが昔は互換性が微妙って話もちらほら聞いてましたけどv1.0になって良くなったのか、シンプルな内容だからなのか

BunのネイティブAPIを使って書き換えられる部分を書き換えてみたいと思います。

## Bun.serve()とBun.CyptoHasher()

主にサーバー部分とHA256アルゴリズムでハッシュ化する部分の2箇所を書き換えてます。

### サーバー部分 - Bun.serve()

ドキュメントみるとこんな感じで書くみたいですね

```js
Bun.serve({
  fetch(req) {
    const url = new URL(req.url);
    if (url.pathname === "/") return new Response("Home page!");
    if (url.pathname === "/blog") return new Response("Blog!");
    return new Response("404!");
  },
});
```

https://bun.sh/docs/api/http#bun-serve

#### 受け取ったPOSTリクエストを処理する部分

ググってて出てきた`response.text()`を使うみたいです。

> https://github.com/oven-sh/bun/issues/4728

#### ハッシュ化部分 - Bun.CyptoHasher()

- Node.js版

```js
crypto.createHmac('SHA256', channelSecret).update(body).digest("base64");
```

- Bun版

```js
new Bun.CryptoHasher('sha256', channelSecret).update(body).digest('base64')
```

https://bun.sh/docs/api/hashing#bun-cryptohasher

#### Buffer.byteLength() - 変更なし

Buffer.byteLength()はNode.jsのAPIをそのままって感じですがBunはNode.jsのAPIがそのまま使えるのも良い点だと思うのでここはこのままにしておきます。

## コピペ用コード

`bun install`などいらないです。

```js

const HOST = 'api.line.me'; 
const REPLY_PATH = '/v2/bot/message/reply';//リプライ用
const CH_SECRET = process.env.CH_SECRET; //Channel Secretを指定
const CH_ACCESS_TOKEN = process.env.CH_ACCESS_TOKEN; //Channel Access Tokenを指定
const SIGNATURE = new Bun.CryptoHasher('sha256', CH_SECRET);
const PORT = 3000;

/**
 * httpリクエスト部分 Fetch APIを使用
 */
const httpClient = async (replyToken, SendMessageObject) => {
    try {
        const REPLY_API_ENDPOINT = `https://${HOST}${REPLY_PATH}`;
        const postDataStr = JSON.stringify({ replyToken: replyToken, messages: SendMessageObject });
        const options = {
            method: 'POST',
            headers: {
                'Content-Type': 'application/json; charset=UTF-8',
                'x-line-signature': SIGNATURE,
                'Authorization': `Bearer ${CH_ACCESS_TOKEN}`,
                'Content-Length': Buffer.byteLength(postDataStr) //長さチェック用
            },
            body: postDataStr
        };

        return fetch(REPLY_API_ENDPOINT, options);
    } catch (error) {
        throw new Error(error);
    };
};

// 署名チェック - Bun.CryptoHasher()を使用
const signatureValidation = async (xLineSignature, channelSecret, body) => {
    const originSignature = new Bun.CryptoHasher('sha256', channelSecret).update(body).digest('base64');
    if(xLineSignature === originSignature){
        return true; //正常
    }else{
        return false; //異常
    }
}

//WebhookEventObjectの処理
const handleEvent = async (event) => {
    //メッセージが送られて来た場合
    if(event.type !== 'message' || event.message.type !== 'text'){
        console.log('TEXTメッセージではないので無視');
        return;
    }

    const SendMessageObject = [{
        type: 'text',
        quoteToken: event.message.quoteToken, //引用リプライ
        text: event.message.text
    }];

    const response = await httpClient(event.replyToken, SendMessageObject);
    const data = await response.json();
    return data;
}

//POSTリクエストの処理
async function handlePostRequest(request) {
    try {
        const bodyStr = await request.text(); // リクエストのボディを取得

        if(!signatureValidation(request.headers['x-line-signature'], CH_SECRET, bodyStr)){
            console.log('署名認証エラー');
            return;
        }

        if(JSON.parse(bodyStr) && JSON.parse(bodyStr).events.length < 1){
            console.log('おそらくLINE Developersからの検証イベント');
            return;
        }

        const events = JSON.parse(bodyStr).events;
        const responses = await Promise.all(events.map(handleEvent));
        console.log(responses[0]);
    
        // レスポンスを返す
        return new Response("POST request received: " + body);
    } catch (error) {
      return new Response("Error processing request", { status: 500 });
    }
}

//サーバーの起動 - Bun.serve()を使用
Bun.serve({
    async fetch(req) {
        const url = new URL(req.url);
        if(url.pathname !== '/' || req.method !== 'POST'){
            console.log('URLが正しくないか、POSTメソッドではありません。');
            return new Response("URLが正しくないか、POSTメソッドではありません。")
        }
        
        if(req.body){
            return handlePostRequest(req);
        }

        // if (url.pathname === "/blog") return new Response("Blog!");
        return new Response("404!");
    },
    port: PORT
});
```

`.env`ファイルは特に外部モジュール依存なしで読み込んでくれます。

```.env
CH_SECRET=XXXXXXXXXXXXXX
CH_ACCESS_TOKEN=YYYYYYYYYYYYYYY
```

実行

```
$ bun server.js
```