【kintone×LINE Bot】餃子検定[非公式]を作ってみた

## はじめに
本格的に寒くていよいよ暖房を入れました、私です！
`アドベントカレンダー = ネタ披露` という脳内変換がされたので、書きに来ました！

## 何を作ったのか
私は、餃子を食べることを「自主練」と呼んでいるのですが、今年は自主練が月1の時もあり、
「そんな自主練の頻度で、本当に餃子好きと言えるのか？」「ビジネス餃子ではないのか？」と周囲の方々からのありがたい~~手厳しい~~フィードバックを頂きまして、「私の餃子好きが分かるものを作るぞ！」ということでLINE BOTを作成してみました！

自主練はサボり気味ですが、
日々、餃子の知識をインプットしている私の作った最強の(?)餃子検定を受験して下さい。
みなさま、対戦よろしくお願いします！

## デモ
まずは、何はともあれ触ってみると早いかと思います！
早速ですが、良かったらLINE BOTと友達になって餃子検定問題に回答して頂けると嬉しいです。
10問解答すると、自分の餃子レベルが分かります。
https://lin.ee/znPnwXn
![M_gainfriends_qr.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168550/f11120c7-46a0-6d37-cf29-857d4fbcf90a.png)


<img width="400" alt="餃子検定" src="https://user-images.githubusercontent.com/24198251/145142283-4928da00-f47a-4689-8b81-f5c3076a88e3.gif">

## 全体像

### 構成図

![構成図.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168550/aa87be67-b5ba-7c8d-e05a-cc247c31134c.png)

## 開発に必要なもの
- LINEアカウント
    - Messaging APIの設定
- Herokuアカウント
    - デプロイ先のServerに利用する
- kintoneアプリ
    - 餃子検定問題[非公式]
        - 問題、選択肢、解答を管理するアプリ
    - 餃子検定ランク
        - 正解数に合わせた餃子ランク、メッセージを管理するアプリ
    - 餃子検定挑戦者
        - 問題と回答を管理するアプリ
- 餃子に関する問題
    - 地味に問題作るのは時間がかかる...

## ざっくり解説

### 1. LINE

#### BOTを作成する

- [LINE Developer Console](https://developers.line.biz/)にログイン
- プロバイダーやMessaging APIを使用したチャネルを登録
    - ※後で使う： `チャネルアクセストークン`
    - ※後で使う：`チャネルシークレット`
    - ※後で更新する： `Webhook URL`

#### リッチメニューを作成する
今回は[LINE Official Account Manager 画面](https://manager.line.biz/)から作成しました。
<img width="400" alt="リッチメニュー" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168550/72085bb8-ab18-6988-781b-d12b8a1bbea6.png">

画面上で、

- 表示期間
- メニューで表示したい画像
- メニューをクリックした時のアクション

を設定すれば完成。

### 2. kintone
各種アプリの作成と、REST APIで利用したいためトークンを発行しておく。

- kintoneアプリ
    - 餃子検定問題集[非公式]
    - 餃子検定ランク
    - 餃子検定挑戦者

### 3. コード解説

- 使用したSDK
  - [LINE Messaging API SDK](https://github.com/line/line-bot-sdk-nodejs/blob/master/README.md)
  - [kintone REST API Client ](https://github.com/kintone/js-sdk/tree/master/packages/rest-api-client)


#### ①LINEからデータを受け取る
<img width="400" alt="餃子検定" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168550/f459e26e-3a88-2249-00a4-5f4aefefbd5e.png">

ユーザーがLINE上でアクションをすると、LINEのPlatformがアクションを検知し、設定したWebhook URLにデータが送信されます。データを受け取ったサーバー(今回はHeroku)は、handleEvent関数の引数(event)にLINEから送信されたデータを受け取り、関数内のコードを実行していきます。

```javascript:index.js
// webhookのhandler設定
app.post('/callback', line.middleware(config), (req, res) => {
  Promise
    .all(req.body.events.map(handleEvent))
    .then((result) => res.json(result))
    .catch((err) => {
      console.error(err);
      res.status(500).end();
    });
});

async function handleEvent(event) {
  let reply_msg;
  switch (event.type){

    // **************************
    // メッセージイベント受信時に実行
    // **************************
    case "message":
      reply_msg = await func.messageFunc(event);
      console.log(reply_msg)
      break;

    // **************************
    // 問題の返信(postback)イベント受信時に実行
    // **************************
    case "postback":
      reply_msg = await func.postbackFunc(event);
      break;
  }

  if (reply_msg !== undefined) {
    return client
      .replyMessage(event.replyToken, reply_msg)
      .catch((err) => {
        console.error(err);
      })
  }
}
```

#### ②「餃子検定を受験する」を受け取った場合

<img width="700" alt="「餃子検定を受験する」を受け取った場合" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168550/3f2ef4f9-8098-4752-6161-f2fccb0273b2.png">


「餃子検定を受験する」文字列を受け取った場合は、
1. `event.message`が`'message'`
2. `event.message.type`が`'text'`
3. `event.message.text`が`'餃子検定を受験する'`と判断されて、最終的に、getGyozaQuizFunc(関数)を実行します。

```javascript
// **************************
// メッセージの内容毎に返信を変える
// **************************
async function messageFunc(event) {
	let message;
	switch (event.message.type) {
		case 'text':
            message = await messageTextFunc(event);
			break;
		case 'image':
		case 'sticker':
            message = { 
              type: 'sticker',
              packageId: conf.package_id,
              stickerId: conf.stickers[Math.floor(Math.random() * conf.stickers.length)]
            };
			break;
	}
	return message;
}

// **************************
// メッセージのテキストを判断して返信内容を変更する
// **************************
async function messageTextFunc(event) {
  let message;
  switch (event.message.text) {
    case ('餃子検定を受験する'):
      message = await getGyozaQuizFunc(event);
      break;
    case ('おすすめの餃子'):
      console.log('おすすめの餃子');
      message = { type: 'text', text: 'おすすめのお店を紹介します！' + conf.gyoza_stores[Math.floor(Math.random() * conf.gyoza_stores.length)] };
      break;
    //省略
    //:
    //:
    default:
      console.log(event);
      message = { type: 'text', text: event.message.text };
      break;
  }
  return message;
}

```

getGyozaQuizFunc関数は、kintoneの餃子検定問題アプリからランダムに問題を1問取得、
kintoneの餃子検定挑戦者アプリにデータを登録する仕組みになっています。

```javascript
// **************************
// 餃子問題を取得する関数
// **************************
async function getGyozaQuizFunc(event) {
  const min = 1;
  const max = 10;

  // ランダムで1問取得
  const result = await kintone_client.record.getRecord({
    app: kintoneGyozaQuizAppId,
    id: Math.floor(Math.random() * (max + 1 - min)) + min
  });

  const current_user_data = await kintone_user_client.record.getRecords({
    app: kintoneGyozaUserAppId,
    query: `user_id="${event.source.userId}" order by $id desc limit 1`,
  });

  // ログインしているユーザーのクイズ数を確認
  // 最新のレコードが0,10問の時は新規レコード,10問以内は更新
  if (current_user_data.records[0].length === 0 || current_user_data.records[0].table.value.length === max) {
    const postUser = await postGyozaQuizChallengerRecord(event, result.record);
  } else {
    let params_obj = [];
    current_user_data.records[0].table.value.forEach(element => {
      let table_id = {
        id: element.id
      };
      params_obj.push(table_id);
    });
    const postUser = await updateGyozaQuizChallengerRecord(event, result.record, current_user_data.records[0].$id.value,  params_obj);
  }

  const correct_arry = [];
  const correct_radio = result.record.answer.value;
  result.record.table.value.forEach(sub_table => {
    let val = (correct_radio === sub_table.value.select_radio.value) ? '正解' : '-';
    correct_arry.push(val);
  });

  let message = {
      type: "flex",
      altText: "解答を表示",
      contents: {
        "type": "bubble",
        "body": {
          "type": "box",
          "layout": "vertical",
          "contents": [
            {
              "type": "text",
              "text": "問題",
              "weight": "bold",
              "size": "xl"
            },
            {
              "type": "text",
              "text": result.record.question.value,
              "size": "md",
              "wrap": true
            },
            {
              "type": "button",
              "action": {
                "type": "postback",
                "label": "A:" + result.record.table.value[0].value.select_answer.value,
                "data": correct_arry[0]
              },
              "height": "sm",
              "margin": "sm",
              "style": "primary",
              "color": "#87ceeb"
            },
            {
              "type": "button",
              "action": {
                "type": "postback",
                "label": "B:" + result.record.table.value[1].value.select_answer.value,
                "data": correct_arry[1]
              },
              "height": "sm",
              "margin": "sm",
              "style": "primary",
              "color": "#87ceeb"
            },
            {
              "type": "button",
              "action": {
                "type": "postback",
                "label": "C:" + result.record.table.value[2].value.select_answer.value,
                "data": correct_arry[2]
              },
              "height": "sm",
              "margin": "sm",
              "style": "primary",
              "color": "#87ceeb"
            }
          ]
        }
      }
    }
  return message;
}
```

##### 💡ポイント
Botの返信メッセージをmessage変数に入れていますが、今回のようにFLEX MESSAGEを利用したい場合は、[FLEX MESSAGE SIMULATOR](https://developers.line.biz/flex-simulator/?status=success)を利用してデータを作成してから、コーディングすると簡単に実装することができます。
<img width="400" alt="Flex_Message_Simulator" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168550/7538e80b-ca0c-3d79-4b5e-1778aa3b5a99.png">

#### ③Botの返信メッセージをreturnする

最終的に`reply_msg`変数に、kintnoeから取得した問題データが代入され、Botの返信メッセージを送信する仕組みです。

```javascript
if (reply_msg !== undefined) {
    return client
      .replyMessage(event.replyToken, reply_msg)
      .catch((err) => {
        console.error(err);
      })
  }
```



### 4. Heroku

最終的に、Herokuにデプロイする。
デプロイの詳細手順は、公式ドキュメントを参考にすると良いです！
[Herokuでサンプルボットを作成する](https://developers.line.biz/ja/docs/messaging-api/building-sample-bot-with-heroku/#page-title)

デプロイが終わったら、LINE Developer Consoleの`Webhook URL`をデプロイした
` https://~~~.herokuapp.com/callback` に修正する。

<font color="LightCoral">※今回は、Webhookのエンドポイントを/callbackとしているため、  `/callback` を忘れずに！</font>

```javascript:index.js
// webhookのhandler設定
app.post('/callback', line.middleware(config), (req, res) => {
  Promise
    .all(req.body.events.map(handleEvent))
    .then((result) => res.json(result))
    .catch((err) => {
      console.error(err);
      res.status(500).end();
    });
});
```

## ソースコード
全ての解説はしませんでしたが、全体がどのような実装になっているのか気になる方は、
[GitHub](https://github.com/naberina/gyoza-test-line-bot)で管理しているので、参考にしてみて下さい。


## 今後
- [ ] Lambdaに移行する
- [ ] 問題10問を重複しないようにしたい(現状はランダムのため重複してしまう)
- [ ] 「結果をシェアする」 アクションを実装？
- [ ] APIリクエストしすぎ問題
- [ ] 10問解き終わったら画像を返す

自分の中でも課題がまだあるので、時間を見て追加・改修して行けたら良いな〜

## 終わりに
餃子検定の結果は、いかがでしたか？
私はもちろん...
<img width="400" alt="餃子検定" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/168550/d4a4fa7b-1c2a-64e1-e039-2eedc6564d2a.jpeg">

これで本物の「餃子好き」と認めてもらえること間違いなしですね！？

~~問題も回答も自分で作成したので、当たり前の結果ですが...(笑)~~
ということで、餃子検定に出題して欲しい問題を大募集中です。

今回は、kintoneとLINE Botを利用して、餃子検定(クイズ)アプリを作成しました。
一見、クソアプリに見えるかもしれませんが、
勉強や資格の問題を貯めておいて暇な時に使ったり、複数のメンバーで問題を作成し合って使う...
なんてことができたら、面白いかもしれませんね？

皆様も、良き餃子ライフで今年を締めくくりましょう！