勝手に問題を出してくれるLINE Bot【LINE✖️GAS】百人一首で実装

### はじめに
暗記系の勉強をしている時、ある程度インプットが終わったら、誰かにクイズ形式で問題出してほしいと思うことありませんか？そういう時のためのLINE Botです。
今回は題材として百人一首を使いました。
スプレッドシートにリスト化されていれば（リストがない場合作る手間がありますが）、百人一首に限らず、「〇〇なら△△」と1対1対応するものであれば同じ方法で作成できます。

### LINEでいかに解答を返すか？
1対1対応させる時に問題となるのは、"ユーザーが送信する言葉が一定しない場合"です。
半角全角の違い、スペースの有無、ちょっとした表記の違い、は全てコンピューターにとっては全く別物です。
その解決方法として【リッチメニューから選択してもらう】方法があります。
これならユーザーからの送信内容は統一されるので、それに対するアクションを起こすのが簡単です。

### スプレッドシート
問題部分と解答部分の列を作ります。
こうすれば問題部分の列番号を[クイックリプライ ](https://developers.line.biz/ja/docs/messaging-api/using-quick-reply/)の["ポストバックアクション"]()で返してあげれば、解答が返ってくるシステムの出来上がりです。

### LINEアカウント作成
[こちらを参考にしました](https://qiita.com/cog1t0/items/0d121ee6d48a8c1634fc)

### 題材は百人一首に決定
- 上の句（全部）を出題→「下の句を見る」を選択すると下の句が表示される
- 上の句（初めの５文字のみ）を出題→「下の句を見る」を選択すると下の句が表示される
- 下の句を出題→「決まり字を見る」で決まり字（その始まりからはこの下の句と決まっている文字列）が表示される→さらに歌全体を見たい場合はクイックリプライで歌全体も表示できる

このような3つのパターンで勉強できるようにしました。
100首の中からランダムで出題されます。
デモとしては↓こんな感じ。

https://twitter.com/ped_yi/status/1599637412313194496

## 実装
全体像としては図のような感じで、GASを介してスプレッドシートから情報を取ってくるイメージです。
![百人一首全体図.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2653579/a7f02b0b-76c8-51d1-0f5e-6cc04b01c4e6.jpeg)

### ランダムに歌を選ぶ
どの歌を問題に出すかを選ぶ関数です。
```javascript
function getRandomInt(min, max) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min) + min);
}
```
min, maxに該当する列番号（maxは該当する列番号+1にすること）を入れます。

### postbackで答えを返す
スプレッドシートの作成時に答えで返す部分を同じ列にするという一工夫をすることで、postbackを返すif文の中身は比較的シンプルになります。

```javascript
if (json.events[0].type == "postback") {
    let replynumber = json.events[0].postback.data;
    let replytext = mainsheet.getRange(replynumber, 4).getValue();
    if (replynumber > 203 && replynumber < 304) {
      let all = parseInt(replynumber) + 101;
      let alltext = mainsheet.getRange(all, 3).getValue();
      messages = [
        {
          type: "text",
          text: replytext,
          quickReply: {
            items: [
              {
                type: "action", 
                action: {
                  type: "postback",
                  label: "歌全部を見る",
                  text: "歌全部を見る",
                  data: all,
                },
              },
            ],
          },
        },
      ];
    } else {
      messages = [
        {
          type: "text",
          text: replytext,
        },
      ];
    }
```
postbackのdata部分に今扱っている列番号を入れることが肝です。
今回問題パターン３つ目である「下の句→決まり字」の場合は、さらに「決まり字→歌全体」という返しをするために、列番号での場合分けをしています（文章で説明しづらいので、LINE Bot自体を触っていただくと分かりやすいかもしれません）。


## 全体

```javascript
let CHANNEL_ACCESS_TOKEN =
  "チャンネルアクセストークン";
let line_endpoint = "https://api.line.me/v2/bot/message/reply";
const mainsheet = SpreadsheetApp.getActiveSpreadsheet().getSheetByName("main");

function doPost(e) {
  let json = JSON.parse(e.postData.contents);
  let reply_token = json.events[0].replyToken;
  if (typeof reply_token === "undefined") {
    return;
  }
  if (json.events[0].type == "message") {
    const usermessage = json.events[0].message.text;
    if (usermessage == "上の句から初級編") {
      let enumber = getRandomInt(103, 203);
      let ekaminoku = mainsheet.getRange(enumber, 2).getValue();
      messages = [
        {
          type: "text",
          text: ekaminoku,
          quickReply: {
            items: [
              {
                type: "action", // ③
                action: {
                  type: "postback",
                  label: "下の句を見る",
                  text: "下の句を見る",
                  data: enumber,
                },
              },
            ],
          },
        },
      ];
    } else if (usermessage == "上の句から上級編") {
      let dnumber = getRandomInt(103, 203);
      let dkaminoku = mainsheet.getRange(dnumber, 3).getValue();
      messages = [
        {
          type: "text",
          text: dkaminoku,
          quickReply: {
            items: [
              {
                type: "action", // ③
                action: {
                  type: "postback",
                  label: "下の句を見る",
                  text: "下の句を見る",
                  data: dnumber,
                },
              },
            ],
          },
        },
      ];
    } else if (usermessage == "下の句から決まり字") {
      let kimarijinumber = getRandomInt(204, 304);
      let shimonoku = mainsheet.getRange(kimarijinumber, 3).getValue();
      messages = [
        {
          type: "text",
          text: shimonoku,
          quickReply: {
            items: [
              {
                type: "action", // ③
                action: {
                  type: "postback",
                  label: "決まり字を見る",
                  text: "決まり字を見る",
                  data: kimarijinumber,
                },
              },
            ],
          },
        },
      ];
    }
  } else if (json.events[0].type == "postback") {
    let replynumber = json.events[0].postback.data;
    let replytext = mainsheet.getRange(replynumber, 4).getValue();
    if (replynumber > 203 && replynumber < 304) {
      let all = parseInt(replynumber) + 101;
      let alltext = mainsheet.getRange(all, 3).getValue();
      messages = [
        {
          type: "text",
          text: replytext,
          quickReply: {
            items: [
              {
                type: "action", 
                action: {
                  type: "postback",
                  label: "歌全部を見る",
                  text: "歌全部を見る",
                  data: all,
                },
              },
            ],
          },
        },
      ];
    } else {
      messages = [
        {
          type: "text",
          text: replytext,
        },
      ];
    }
  }
  UrlFetchApp.fetch(line_endpoint, {
    headers: {
      "Content-Type": "application/json; charset=UTF-8",
      Authorization: "Bearer " + CHANNEL_ACCESS_TOKEN,
    },
    method: "post",
    payload: JSON.stringify({
      replyToken: reply_token,
      messages: messages,
    }),
  });
  return ContentService.createTextOutput(
    JSON.stringify({ content: "post ok" })
  ).setMimeType(ContentService.MimeType.JSON);
}

function getRandomInt(min, max) {
  min = Math.ceil(min);
  max = Math.floor(max);
  return Math.floor(Math.random() * (max - min) + min);
}

```

### おわりに
LINEは1個前のメッセージの記憶がないため、サーバーの使用やuserIDに紐づけた記録をしない限り、1対1以上のやりとりができません（と私は認識しています）が、クイックリプライの利用で会話のやりとりを続けることができます。

このLINE Botを作るに当たって、既にエクセル化されている百人一首のデータ（[こちら](http://www2e.biglobe.ne.jp/shinzo/shiryou/misc/benri.html)）があったので、とても短時間に作ることができました。激しく感謝です。