Flexible Template な🐈にゃんこボット🐈

これは [LINE WORKS Advent Calendar 2022](https://qiita.com/advent-calendar/2022/lineworks) 17日目の記事です。

LINE WORKS のトークボットをもっと華やかにしたい！
という時に便利そうな「フレキシブル テンプレート」を見つけたので紹介します。

そして、単に紹介するだけでは・・・という事で、例によってにゃんこネタを絡めたかったのでググったら、なんとも素敵なサービスを見つけました。これはフュージョンするしかない！

という欲にまみれた記事でございます。

# 猫 on LINE WORKS

「にゃんこ」と発言すると 5 匹の猫ちゃんを表示しつつ、好みの子に投票できるボットはいかが？おかわりもあるよ！

<iframe width="560" height="315" src="https://www.youtube.com/embed/ehhutORgUSo" title="YouTube video player" frameborder="0" allow="accelerometer; autoplay; clipboard-write; encrypted-media; gyroscope; picture-in-picture" allowfullscreen></iframe>

見ての通り、カルーセル表示ですが画像を全体に表示しつつ、半透明のボタンやテキストを置けたりと綺麗な見た目をつくれました。見てるだけでも楽しくなります。

では実装や詳細を早速みていきましょう。

# フレキシブル テンプレート is 何？

トークメッセージの 1 つで、[CSS Flexible Box](https://www.w3.org/TR/css-flexbox-1/) を活用したレイアウトをカスタマイズできるものです。

https://developers.worksmobile.com/jp/reference/bot-send-flex?lang=ja

実際のところは、ボタンや画像などの大きさや位置を **全て JSON の構造式** に起こして組み立てていきます。

<details><summary>サンプル</summary><div>

```jsonc
{
  "content": {
    "type": "flex",
    "altText": "にゃんこ投票",
    "contents": {
      "type": "carousel",
      "contents": [
        // エントリー No.1
        {
          "type": "bubble",
          "size": "micro",
          "body": {
            "type": "box",
            "layout": "vertical",
            "contents": [
              {
                "type": "image",
                "url": "【猫ちゃんの画像URL】",
                "size": "full",
                "aspectMode": "cover",
                "aspectRatio": "2:3",
                "gravity": "top"
              },
              {
                "type": "box",
                "layout": "vertical",
                "position": "absolute",
                "offsetStart": "0px",
                "offsetEnd": "0px",
                "offsetBottom": "0px",
                "paddingAll": "20px",
                "paddingTop": "18px",
                "backgroundColor": "#1e5a2ecc",
                "contents": [
                  {
                    "type": "box",
                    "layout": "vertical",
                    "spacing": "sm",
                    "height": "40px",
                    "borderWidth": "1px",
                    "cornerRadius": "4px",
                    "margin": "xxl",
                    "borderColor": "#ffffff",
                    "action": {
                      "type": "postback",
                      "label": "",
                      "data": "【ポストバックID】"
                    },
                    "contents": [
                      {
                        "type": "filler"
                      },
                      {
                        "type": "box",
                        "layout": "baseline",
                        "spacing": "sm",
                        "contents": [
                          {
                            "type": "filler"
                          },
                          {
                            "type": "text",
                            "text": "投票！",
                            "color": "#ffffff",
                            "flex": 0,
                            "offsetTop": "-2px",
                            "weight": "regular",
                            "margin": "sm"
                          },
                          {
                            "type": "filler"
                          }
                        ]
                      },
                      {
                        "type": "filler"
                      }
                    ]
                  }
                ]
              },
              {
                "type": "box",
                "layout": "vertical",
                "position": "absolute",
                "offsetTop": "18px",
                "offsetStart": "18px",
                "width": "75px",
                "height": "25px",
                "cornerRadius": "20px",
                "backgroundColor": "#ff334b90",
                "contents": [
                  {
                    "type": "text",
                    "text": "28j",
                    "color": "#ffffff",
                    "align": "center",
                    "size": "sm",
                    "offsetTop": "3px"
                  }
                ]
              }
            ]
          }
        },
        {
          // エントリー No.2
        },
        {
          // エントリー No.3
        },
        // 省略
      ]
    }
  }
}
```
</div></details>

これをゼロから書き起こすのは流石にしんどいので、それを補助してくれるオンラインツール「[Flexible Template Simulator](https://flex-simulator.worksmobile.com/)」があります。

今回作ったボットも元々あるサンプルをベースに好みの大きさや配置に手直しして JSON の雛形をつくりました。後は繰り返し処理や画像 URL などの変数値埋め込みをプログラムしていくことに集中できます。

## Flexible Template Simulator

[Flexible Template Simulator](https://flex-simulator.worksmobile.com/) を開くとこんな感じで既にサンプルが表示されてる状態から始まります。

![image-01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/e946c826-d9d7-e2cc-b485-fd0264271546.png)

左上の「サンプル」ボタンをクリックすると、さらにバリエーション豊かなサンプルが選べます。今回のボットでは「Brochure」をベースにしました。

![image-02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/275499aa-7b38-8c45-27a5-7997f8d12371.png)

「作成」をクリックすると編集モードに移行します。色々矢印を引いた通り、1つ1つ部品ツリーになっているので見ながら調整します。この辺りは CSS の基礎知識があれば十分にカスタマイズできる感じでした。

![image-03.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/0ccc1c5d-4251-84ec-ae6f-332f35147cf8.png)

仕上げにアクションを受け付ける部分の Box を選んで、右下にある「Action」で色々定義します。値部分は埋め込む想定に近いダミー値を配置しておきます。後で巨大な JSON から該当部分を探すのに役立ちます。

今回は「投票する！」ボタンとして作り込みます。押したらボットへ、にゃんこIDを伴ってコールバック受信してほしいので postback タイプで設定しました。

![image-04.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/b622c4ab-c634-d38d-fbf8-e08566f247b5.png)

あとは右上の「JSONで表示」をクリックして目的の雛形を手に入れます。

![image-05.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/0caf3c5c-87e0-a363-bb05-cd86b57e65e0.png)

## どうやって使うか

https://developers.worksmobile.com/jp/reference/bot-send-flex?lang=ja

に書いてある通りではありますが、エンドポイントに POST するペイロード部分を以下のようにします。

```jsonc
{
  "type": "flex",  // お決まり
  "altText": "にゃんこ投票",
  "contents": {
    // ★ここに Flexible Template Simulator で生成した JSON を張り付ける
  }
}
```
後ほどプログラムするときのために JSON ファイルなどにサンプルとして保存しておくと良いです。Flexible Template Simulator に保存機能は無いのでブラウザーを閉じると全て消えます。

余談ですが、ここでかなりハマりました。contents の位置が分からなかったです。
最初はコピーした内容をまるごとメッセージ送信のペイロードになるのかと勘違いして送信したら HTTP 400 応答で失敗しました。

そろそろ猫成分を補充しましょう！

# 🐈 Cats as a Service !

「猫画像 API」でググったら見つけました。サービスとしてのにゃんこたち！

https://thecatapi.com/

![the-cats-api.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/5db0c24b-9942-dd15-770c-4301f95bffae.png)

ロゴマークのキュートさにやられましたわ。

未登録だと 1 画像のみ検索・指定なし検索（10件）しかできず、絞り込みや投票もできないので、アカウント登録して API キーを発行しました。

## 無料登録と API 仕様

下部にある「SIGNUP FOR FREE」からできます。登録したメールアドレス宛に API キーが送られてくるので保管しておきます。

細かな仕様については右上の「DOCUMENTATION」から見れます。OpenAPI 形式の YAML の手に入るので使い勝手はかなり良いです。

https://developers.thecatapi.com/view-account/ylX4blBYT9FaoVd6OhvR?report=bOoHBz-8t

# 🐈🐈 ネコる

今回はバックエンドの API サーバーとして [nestjs](https://nestjs.com/) をフレームワークにして起動しつつ、ngrok で Webhook 受付用の URL をこさえる事にしました。この構成だとクラウド等にデプロイする必要もなく、クラウドサービスと連携できるのですごく便利です。

![nestjs.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/8275dc85-e00b-6c40-c7fa-a4ec8832a873.png)

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/cff50c18-85fe-2e9e-8c1f-fb17a8600fba.png)

nestjs をデフォルト設定で起動すると「 `http://locahost:3000` 」でアクセスできるので、それにあわせて「`ngrok http 3000`」としてトンネルさせる算段です。

```plantuml
"LINE WORKS" -> "ngrok サービス" : チャットメッセージをコールバック POST
"ngrok サービス" -> "ローカルの ngrok プロセス" : 転送
"ローカルの ngrok プロセス" -> "ローカルの nestjs" : 転送
```

チャットを送る時はローカルの nestjs から直接 LINE WORKS の API にアクセスします。

```plantuml
"ローカルの nestjs" -> "LINE WORKS" : トークメッセージを送信
```

## 実装

かなりボリュームがあるので主要どころのみ。ソースコードは追って github に公開します。

### app.controller.ts

HTTP エンドポイント部分の作り。
`http://localhost/webhooks/適当なID` で受け付けるようにしました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/f8fb261b-6377-f418-93dc-cdd638d5749d.png)

本来は `X-WORKS-Signature` を使った HMAC-SHA256 によるメッセージ検証をすべきですが、書き方が悪いらしくシグネチャが一致せず。
ここは今後の課題になりました。

### app.service.ts

まずは Webhook コールバックを受け付けて、該当キーワードがあったら 5 匹のにゃんこ画像を送ったり、投票の処理＆おかわり送信をしています。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/db4a82b4-48ff-a07e-d740-22b028ded915.png)

Flexible Template Simulator で生成した JSON をもとに繰り返しや値埋め込みを行っていきます。これだけで 120 行に達するので別ファイルにするかテンプレートシステム的なものを入れると良いかも。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/a8ac1ee9-b701-7d49-aba5-33bd400a10bd.png)

### lineworks.service.ts

続いて LINE WORKS 関連を nestjs サービス化します。
API 2.0 に関する認証情報・秘密鍵などは環境変数（.env ファイル）から収集するようにしました。この実装のベースは自分の記事 [LINE WORKS API 2.0 でスタンプメッセージを送ろう - 実装編](https://qiita.com/bugtrap/items/7157d6eb7afc58cd97cd) を読み返しつつ組み込みました。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/c4367d90-53a3-d1e7-0e5f-f661ebbd934b.png)

### the-cats.service.ts

癒しの供給元 The Cats API アクセスについても nestjs サービス化していきます。
LINE WORKS 同様に API キーを環境変数から受け取ります。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/7a0f3f64-e0d0-aa68-6c3b-90fbcccc57b1.png)

# サーバー起動と公開

## ngrok

先に ngrok を立てます。

この記事が参考になりました。感謝！

https://qiita.com/mininobu/items/b45dbc70faedf30f484e

```powershell
ngrok http 3000
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/759daafa-2593-0d51-0df7-4032db9d7687.png)

Web アクセスできる URL が発行されるのでメモします。

## LINE WORKS ボットのコールバック設定

API 2.0 アプリの作成とボットの登録は済ませ、「Callback URL」にメモした URL を入れつつアプリ実装のパスも付け足しておきます。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/4d1a2246-60f9-636c-5593-5905bea9b26b.png)

### .env

環境変数の設定を済ませます。
実装の `this.config.getOrThrow<string>("LINEWORKS_CLIENT_ID")` などのようにして読み取ることができます。

```.env
THE_CAT_API_KEY = "live_xxxxxxxxxxxxxxxxxxxxxxxxxxxx"

LINEWORKS_BOT_ID = "1234567"

LINEWORKS_CLIENT_ID = "xxxxxxxxxxxxxxxxxxxx"
LINEWORKS_CLIENT_SECRET = "xxxxxxxxxx"
LINEWORKS_SERVER_ACCOUNT = "xxxxx.serviceaccount@xxxxxxxx"
LINEWORKS_PRIVATE_KEY = "
-----BEGIN PRIVATE KEY-----
MIIEvQIBADANBgkqhkiG9w0BAQEFAASCBKcwggSjAgEAAoIBAQCFTUyFCZOe+i3m
省略
Gr4AbGF41v7O8Cnwfnnq23Y=
-----END PRIVATE KEY-----
"
```

### nestjs 起動

```
npm run start:dev
```

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3934/d9442274-c42d-a9af-1204-434cf580948f.png)

これで連結完了です。

## この構成の注意点

ngrok は起動するごとに払い出されるアドレスが変わるので、起動の都度 LINE WORKS のボット設定にあるコールバック URL のメンテが必要です。そこは割り切りとしました。

# おわりに

なんとか滑り込みで今日のうちにだせたｗ（早くやっとけ
思いのほか、にゃんこ選択が面白くてデバッグと称してクリックしまくっていたのが時間泥棒になってしまった・・。

アドベントカレンダー参加をきっかけに色々なサービスや機能に触れられたのは新鮮で良かったです。

それではまた！