LINE BOTで単語帳作る(第2回:メイン処理編)

# 資料構成

全3回ぐらいになる予定。

- [第1回 : 準備(オウム返しBOT作成まで)](https://qiita.com/Ki2neudon/items/1738298652ac0077b5c0)
- 第2回 : 問題POSTまで
- 第3回 : 便利機能追加(正答率とか)

この記事は第2回です。

# 問題POSTまで

## 問題集の作成

![GoogleDrive_01_ドライブメイン画面.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259373/f86090ed-426c-9702-f236-7272ed50f705.png)

GoogleDriveに新規Spreadsheetを作成します。
左上の ``+ 新規`` ボタンから空白のSpreadsheetを作ります。

![問題集_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259373/ef768469-2a02-2173-0205-2bacc091618a.png)

上段の赤枠、Book名を適当な名前に変更し(BOTの中では使いません)、下段の赤枠シート名(こっちは使います)を変更します。
A列に問題、B列に答えを記載します。

なお今回は時事問題の単語帳として以下から引用させていただきました。
参考記事：[一般常識一問一答.com 2020年7月/時事問題クイズ一問一答問題～期末試験,就職活動に！～](https://jyosiki.com/jiji/2020_7.html)

そしてブラウザで開いているSpreadsheetのURLからSpreadsheetのIDをコピーします。

```
https://docs.google.com/spreadsheets/d/★ここのランダムな文字列★/edit#gid=0
```

## BOT応答の作成

### Spreadsheetの読込

前回作成した``GoogleAppsScript``を開きます。
まずは作成した問題集を定数定義します。

最上段の``LINE BOT 返信URL``は前回書いたやつです。その下に書きます。

```js
// LINE BOT 返信URL
const LINE_ENDPOINT = "https://api.line.me/v2/bot/message/reply";

// 問題集 Spreadsheet ID
const MONDAI_SS_ID = '★SpreadsheetのID★';
const ST_COL_Q = 1;  // 問題列
const ST_COL_A = 2;  // 解答列
// 時事問題 シート名
const JIJI_ST_NAME = '時事問題';
```

続いて``doPost()``関数内でこのシートを読み込む準備をします。
最上段の``//返信に使うトークルームのトークンを取得``は前回書いたやつです。その下に書きます。

```js
  // 返信に使うトークルームのトークンを取得
  const rep_token = json.events[0].replyToken;
  // 未定義だったら終了する
  if (typeof rep_token === "undefined")
  {
    return; // 未定義だった
  }

  // 問題集 Spreadsheetのオブジェクトを取得
  const mondaiSs = SpreadsheetApp.openById(MONDAI_SS_ID);
  // 時事問題シートのオブジェクトを取得
  const jijiSt = mondaiSs.getSheetByName(JIJI_ST_NAME);
```

### 1番目の問題と解答を返す

試しにユーザがトークルームに何か発言すると1個目の問題と回答を返信する処理を書いてみます。
まず前回書いた以下の処理を削除します。

```js
  /** 削除するよ **
  // 応答用メッセージ作成。後ろに「だってばよ。」をつける
  addMsg = {
    type: "text",
    text: message + "。だってばよ！"
  };
  // 応答用メッセージの箱に詰める。
  repMsg.push(addMsg);
  ** 削除したよ **/
```

すぐ下に以下を書き込みます。

```js
  /** 削除するよ **
  :(省略)
  ** 削除したよ **/

  // 1個目の問題を取得して返信に詰める
  addMsg = {
    type: "text",
    text: jijiSt.getRange(1,ST_COL_Q).getValue()
  };
  repMsg.push(addMsg);
  // 1個目の解答を取得して返信に詰める
  addMsg = {
    type: "text",
    text: jijiSt.getRange(1,ST_COL_A).getValue()
  };
  repMsg.push(addMsg);
```

``シートオブジェクト.getRange(行,列).getValue()``は対象シートの(行,列)のセルの値を取得する関数です。
今回は1行目をベタ指定するためマジックナンバー``1``が出ています。

これを前回の記事に書いたとおり新たなウェブアプリケーションとして公開します。

![時事問題テスト.PNG](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259373/88b86dca-6fcc-3c7f-3b21-948533088b86.png)

LINEのトークルームで発言すると上記のように出力されました。ヤッタネ！

### 指定した番号の問題と解答を返す

いまは1問目固定でしたが、問題番号を指定できるようにしましょう。
まず最大問題番号を取得します。

```js
  // 問題集 Spreadsheetのオブジェクトを取得
  const mondaiSs = SpreadsheetApp.openById(MONDAI_SS_ID);
  // 時事問題シートのオブジェクトを取得
  const jijiSt = mondaiSs.getSheetByName(JIJI_ST_NAME);
  // 時事問題シートの最大行数を取得する
  const jijiMax = jijiSt.getLastRow();
```

``シートオブジェクト.getLastRow()``は対象シートの最大行数を取得する関数です。

次にユーザのメッセージに含まれる数字を取得します。
先程の1個目の問題・解答を返信に詰めていた処理を書き換えます。

```js
  // ユーザのPOSTからメッセージ部を取得
  let message = json.events[0].message.text;

  // 数字が含まれているか
  if(message.match(/\d+/))
  {
    // 数字を問題番号として確保
    const qNum = message.match(/\d+/);
    // 問題番号が範囲内か
    if(1 <= qNum && qNum <= jijiMax)
    {

      // qNum番目の問題と解答を返信に詰める
      addMsg = {
        type: "text",
        text: jijiSt.getRange(qNum,ST_COL_Q).getValue()
      };
      repMsg.push(addMsg);
      // 1個目の解答を取得して返信に詰める
      addMsg = {
        type: "text",
        text: jijiSt.getRange(qNum,ST_COL_A).getValue()
      };
      repMsg.push(addMsg);
    } else {
      
      // 範囲外です
      addMsg = {
        type: "text",
        text: "指定された番号の問題はありません(1～" + jijiMax + "の範囲で指定)"
      };
      repMsg.push(addMsg);
    }
  } else {
      
    // 番号が含まれていない
    addMsg = {
      type: "text",
      text: "問題番号を指定して下さい(1～" + jijiMax + "の範囲で指定)"
    };
    repMsg.push(addMsg);
  }
```

では公開です。

![時事問題テスト_02.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259373/9faf26fd-a59f-dfb3-ed8e-bc2e4faf097c.jpeg)

やりました。大好き。

### 指定した番号の問題もしくは解答を返す

しかし問題と解答が同時に見えるのは困りものです。
ユーザに指定を促しましょう。

先程記載した部分を書き換えます。

```js
  // ユーザメッセージ解析
  if(message.match(/^\d+(問題|解答)$/))
  {
    // 問題番号と種別を確保
    const uRes = message.match(/^(\d+)(問題|解答)$/);
    // 列番号
    let reqCol;
    // 種別を判定
    if(uRes[2] === "問題")
    {
      // 問題を要求された
      reqCol = ST_COL_Q;
    } else {
      // 解答を要求された
      reqCol = ST_COL_A;
    }
    // 問題番号が範囲内か
    if(1 <= uRes[1] && uRes[1] <= jijiMax)
    {
      // uRes[1]番目の問題||解答を返信に詰める
      addMsg = {
        type: "text",
        text: jijiSt.getRange(uRes[1],reqCol).getValue()
      };
      repMsg.push(addMsg);
    } else {
      
      // 範囲外です
      addMsg = {
        type: "text",
        text: "指定された番号の問題はありません(1～" + jijiMax + "の範囲で指定)"
      };
      repMsg.push(addMsg);
    }
  } else {
      
    // フォーマット誤り
    addMsg = {
      type: "text",
      text: "問題番号+{問題|解答}を指定して下さい\n例)1問題、2解答 など"
    };
    repMsg.push(addMsg);
  }
```

結果は以下のとおり

![時事問題テスト_03.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259373/028af46a-798b-7c9a-0576-53d5bbb13ae5.jpeg)

やりました。うれしみ。

### 解答表示ボタンを作る

しかし毎回「問題」「解答」は面倒です。
1つ前のとおり問題番号だけを指定して、続けて解答表示ボタンを設けましょう。

解答表示ボタンは``FlexMessage``で実現します。

``LINE Developers``にログインした後に``ドキュメント`` > ``Messaging API`` > ``Flex Message Simlator``を開きます。
もしくはこのリンクを開いて下さい : [FLEX MESSAGE SIMULATOR](https://developers.line.biz/flex-simulator/)
※ LINE Developersにログインしていないと開けません

![Flex Message Simulator_01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259373/c8ecb521-4ce1-fe3c-b490-76ffd50454e5.png)


操作方法は。。。正直直感で操作できるので語れません。
どうしてもわからなければコメント欄で質問をください。

![Flex Message Simulator_02.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259373/3812b421-84b7-3cf7-f67d-f4fbf794b262.png)

このボタンを押すと「test」という文字列がトークルームに飛ぶようにしていますが組み込みのときにココを可変にします。
デザインした後に右上の``</>View as JSON``を押すとJSONイメージが表示されます。

```json
{
  "type": "bubble",
  "body": {
    "type": "box",
    "layout": "vertical",
    "contents": [
      {
        "type": "button",
        "action": {
          "type": "message",
          "label": "解答を表示",
          "text": "text"
        },
        "height": "sm",
        "style": "primary"
      }
    ]
  }
}
```

さてコレを組み込みます。先程と同じ場所を書き換えます。
冗長な部分がありますが、わかりやすさを優先して敢えて記載しています。決して私のスキルが足りn...

```js
  // ユーザメッセージ解析
  if(message.match(/^\d+$/))
  {
    // 数字だけだったら問題表示要求
    // 問題番号を確保
    const qNum = message.match(/^\d+$/);
    // 問題番号が範囲内か
    if(1 <= qNum && qNum <= jijiMax)
    {
      // qNum番目の問題を返信に詰める
      addMsg = {
        type: "text",
        text: jijiSt.getRange(qNum,ST_COL_Q).getValue()
      };
      repMsg.push(addMsg);
      // 解答表示ボタンを返信に詰める
      addMsg = {
        type: "flex",
        altText: "解答を表示",
        contents: {
          "type": "bubble",
          "body": {
            "type": "box",
            "layout": "vertical",
            "contents": [
              {
                "type": "button",
                "action": {
                  "type": "message",
                  "label": "解答を表示",
                  "text": qNum + "解答"
                },
                "height": "sm",
                "style": "primary"
              }
            ]
          }
        }
      };
      repMsg.push(addMsg);
    } else {
      // 範囲外です
      addMsg = {
        type: "text",
        text: "指定された番号の問題はありません(1～" + jijiMax + "の範囲で指定)"
      };
      repMsg.push(addMsg);
    }
  } else if(message.match(/^\d+解答$/)) {

    // 数字+解答だったら解答表示要求
    // 問題番号を確保
    const qNum = message.match(/^\d+/);
    // qNum番目の解答を返信に詰める
    addMsg = {
      type: "text",
      text: jijiSt.getRange(qNum,ST_COL_A).getValue()
    };
    repMsg.push(addMsg);
  } else {
    
    // フォーマット誤り
    addMsg = {
      type: "text",
      text: "問題番号+{問題|解答}を指定して下さい\n例)1問題、2解答 など"
    };
    repMsg.push(addMsg);
  }
```

公開します。

![時事問題テスト_04.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259373/04173aa5-af69-fff8-fc8b-f58f010d7195.jpeg)

いいでしょういいでしょう。``LGTM``を押してくれてもいいんですよぉ？

### ランダムに問題が出るようにする

というかもうユーザによる問題番号指定とか要らないでしょう。
何か発言を受けたらランダムに問題を返すことにします。

```js
const qNum = Math.floor(Math.random() * jijiMax) + 1;
```

ランダムな問題番号はこれでとれそうです。
参考：[JavaScriptで乱数を取得する方法を現役エンジニアが解説【初心者向け】](https://techacademy.jp/magazine/34866)

```js
  // ユーザメッセージ解析
  if(message.match(/^\d+解答$/)) {

    // 数字+解答だったら解答表示要求
    // 問題番号を確保
    const qNum = message.match(/^\d+/);
    // qNum番目の解答を返信に詰める
    addMsg = {
      type: "text",
      text: jijiSt.getRange(qNum,ST_COL_A).getValue()
    };
    repMsg.push(addMsg);
  } else {
    // ランダムな問題番号を生成
    const qNum = Math.floor(Math.random() * jijiMax) + 1;
    // qNum番目の問題を返信に詰める
    addMsg = {
      type: "text",
      text: jijiSt.getRange(qNum,ST_COL_Q).getValue()
    };
    repMsg.push(addMsg);
    // 解答表示ボタンを返信に詰める
    addMsg = {
      type: "flex",
      altText: "解答を表示",
      contents: {
        "type": "bubble",
        "body": {
          "type": "box",
          "layout": "vertical",
          "contents": [
            {
              "type": "button",
              "action": {
                "type": "message",
                "label": "解答を表示",
                "text": qNum + "解答"
              },
              "height": "sm",
              "style": "primary"
            }
          ]
        }
      }
    };
    repMsg.push(addMsg);
  }
```

ランダム表示の方が行数が少なくて済むっていう。。。
公開します。

![時事問題テスト_05.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259373/0172a6d6-dfc1-0089-bc9a-213820bb29fd.jpeg)

最高やんけ。

### 次の問題を表示するボタンを作る

ココまできたら楽勝ですね。解答の後に``Flex Message``を付け足します。

```js
  // ユーザメッセージ解析
  if(message.match(/^\d+解答$/)) {

    // 数字+解答だったら解答表示要求
    // 問題番号を確保
    const qNum = message.match(/^\d+/);
    // qNum番目の解答を返信に詰める
    addMsg = {
      type: "text",
      text: jijiSt.getRange(qNum,ST_COL_A).getValue()
    };
    repMsg.push(addMsg);
    // 次の問題表示ボタンを埋め込む
    addMsg = {
      type: "flex",
      altText: "次の問題を表示",
      contents: {
        "type": "bubble",
        "body": {
          "type": "box",
          "layout": "vertical",
          "contents": [
            {
              "type": "button",
              "action": {
                "type": "message",
                "label": "次の問題を表示",
                "text": "次の問題"
              },
              "height": "sm",
              "style": "primary"
            }
          ]
        }
      }
    };
    repMsg.push(addMsg);
  } else {
    // ランダムな問題番号を生成
    const qNum = Math.floor(Math.random() * jijiMax) + 1;
    // qNum番目の問題を返信に詰める
    addMsg = {
      type: "text",
      text: jijiSt.getRange(qNum,ST_COL_Q).getValue()
    };
    repMsg.push(addMsg);
    // 解答表示ボタンを返信に詰める
    addMsg = {
      type: "flex",
      altText: "解答を表示",
      contents: {
        "type": "bubble",
        "body": {
          "type": "box",
          "layout": "vertical",
          "contents": [
            {
              "type": "button",
              "action": {
                "type": "message",
                "label": "解答を表示",
                "text": qNum + "解答"
              },
              "height": "sm",
              "style": "primary"
            }
          ]
        }
      }
    };
    repMsg.push(addMsg);
  }
```

![時事問題テスト_06.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/259373/041752fe-2a57-b32b-af3f-4fcf0a7f7ddc.jpeg)

今日のところはこのへんで。
次回、正答率を集計したりユーザランキング付を実装がんばります！