【Linebot＋GAS】単語の一問一答ができるボットの完成まで（1）

# 作成した経緯
友人がITパスポートの勉強をしていると聞いたので、アプリではなくLinebotで一問一答を作成し、スプレッドシートで単語を管理して自分だけの一問一答ができるのではないかと考え…という建前の元、何か作りたかっただけ（笑）
# 使用技術
* Google App Script
* LINE Messaging API
# 機能一覧
まだ未実装のものもありますが、ざっと基本的なところまで
### 1. 問題出題
スプレッドシートから単語を取得して、その単語の意味を出題してくれる機能です。<br>
<img width='400' alt="IMG_0909.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115537/cc307ccb-8da9-d175-74bb-c164f25ee26f.png">
### 2. 選択肢
答えの選択肢を ダミー(2) + 正答(1) で出してくれる機能です。<br>
<img width='400' alt="IMG_0910.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115537/bb52fc97-172a-4d74-579d-acf1abfb1d7e.png">
### 3. 答え
問題の答えとその参考URLを出してくれる機能です。<br>
<img width='400' alt="IMG_0911.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115537/f2e96774-f0aa-6eca-5d4d-4f7aba3b37b1.png">
### 4. 終了
問題の回答を終了する機能（？）これ、いらないな…<br>
<img width='400' alt="IMG_0912.png" src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/3115537/3a42c60a-2f15-2aa6-81e3-99b00952cd8c.png">
### 5. ジャンル変更
出題される単語のジャンルを変更する機能。ITパスポートは`ストラテジ系`, `マネジメント系`, `テクノロジ系`があったはずだからその変更に使います。まだ実装できていませんが^^;
### 6. リマインダー
定時でリマインドする機能。こちらもまだできていません^^;
# 各機能のコード
各機能をGAS（Google Apps Script）で作成したのでその説明
## メッセージに対する分岐
**Userが送ってきた各メッセージに対する分岐の部分。**
```javascript
var ACCESSTOKEN = 'YOUR_ACCESS_TOKEN';
const URL = 'YOUR_LINEBOT_REPLY_URL';
const PUSH_URL = 'YOUR_LINEBOT_PUSH_URL';
const SPREADSHEET_ID = 'YOUR_SPREADSHEET_ID';
const SpreadsheetName = "問題用のスプレッドシート";
const ManegerSpreadsheetName = "管理用のスプレッドシート";


function doPost(e) {
  let data = JSON.parse(e.postData.contents)
  var event = data.events[0];
  let targetColumn = getColumn(event);
  switch (event.message.text) {
    case '出題':
      let answer = replyGame(event);
      insertData(answer, targetColumn);
      break;
    case '終了':
      quitReply(event, targetColumn);
      deleteData(targetColumn);
      break;
    case '答え':
      correctReply(event, targetColumn);
      break;
    case '増やす':
      let options = makeOption(event, "ごめん！その機能は今実装中だから待って！");
      UrlFetchApp.fetch(URL, options);
      break;
    case 'リマインド':
      let options2 = makeOption(event, "ごめん！その機能は今実装中だから待って！");
      UrlFetchApp.fetch(URL, options2);
      break;
    case '選択肢':
      choices(event, targetColumn);
      break;
    default:
      let options4 = makeOption(event, "メニューから言葉を選んでね！");
      UrlFetchApp.fetch(URL, options4);
      break;
  }
}
```
## コードの解説
この2行はおまじない
```javascript
let data = JSON.parse(e.postData.contents)
var event = data.events[0];
```
これは今回のLinebotの仕様上、登録されているユーザー識別列が必要になるので回収しているだけ
```javascript
let targetColumn = getColumn(event);
```
```event.message.text```にUserのテキストメッセージが代入されるのでそれをSwitchで分岐
```javascript
switch (event.message.text) {
    case '出題':
      let answer = replyGame(event);
      insertData(answer, targetColumn);
      
      (以下略)
```
## 問題出題機能
**まずは使っている関数をざっと**<br>
先ほど紹介した分岐処理の```doPost```
```javascript:doPost
    case '出題':
      let answer = replyGame(event);
      insertData(answer, targetColumn);
      break;
```
スプレッドシートを読み込み、単語を一つ抽出して返答を作成する```replyGame```
```javascript:replyGame
function replyGame(event) {
  var Spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID);
  let sheet = Spreadsheet.getSheetByName(SpreadsheetName);
  let questionNumber = generateRandomNumArray(35, 1); // (※1)
  var question = sheet.getRange(questionNumber[0], 2).getValue();
  var answer = sheet.getRange(questionNumber[0], 1).getValue();
  let options = makeQuestionOption(event, question); // (※2)
  UrlFetchApp.fetch(URL, options);
  return answer
}
```
## コードの解説
### 1. スプレッドシートを取得
```javascript
  var Spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID);
  let sheet = Spreadsheet.getSheetByName(SpreadsheetName);
```
### 2. 問題の決定
```generateRandomNumArray```でランダムな整数を一つ取得。今回は試験用として最大値の第1引数に```35```を渡しています（1～35までの数が取得可能）。ですが、本来なら単語の総数を代入。
スプシでは、1列目が単語、2列目が単語の意味になっているので、```questionNumber```行目の単語、単語の意味をそれぞれ```answer```、```question```に代入。
```javascript
  let questionNumber = generateRandomNumArray(35, 1); // (※1)
  var question = sheet.getRange(questionNumber[0], 2).getValue();
  var answer = sheet.getRange(questionNumber[0], 1).getValue();
```
(※1) ランダムな整数を配列にして返してくれる```generateRandomNumArray```
```javascript:generateRandomNumArray
/** 
ランダム整数を生成して配列に格納するクラス
引き数1：最大値
引き数2：配列に格納するランダムな数字の数
**/
function generateRandomNumArray(maxNum, generateArrayLength) {
  let generateArray = []; //ランダム格納用配列
  let numberArray = []; // ランダム生成用配列

  //(2~maxNum)ランダム生成用配列を作成
  for (let i = 2; i <= maxNum; i++) {
    numberArray[i - 2] = i;
  }

  //ランダム格納用配列にランダム整数を格納
  for (let j = 0, len = numberArray.length; j < generateArrayLength; j++, len--) {
    let rndNum = Math.floor(Math.random() * len);
    generateArray.push(numberArray[rndNum]);
    numberArray[rndNum] = numberArray[len - 1];
  }

  return generateArray;
}
```
### 3. 返答の作成
返答用の```options```を作成して2行目で返答
```javascript
let options = makeQuestionOption(event, question); // (※2)
UrlFetchApp.fetch(URL, options);
```
(※2) 返答用のFlexMessageを作成する```makeQuestionOption```
```javascript:makeQuestionOption
//問題用のFlexMessageOption作成
function makeQuestionOption(event, message) {
  HEADERS = {
    "Content-Type": "application/json; charset=UTF-8",
    "Authorization": "Bearer " + ACCESSTOKEN,
  }
  postData = {
    "replyToken": event.replyToken,
    "messages" : [
      {
        'type':'flex',
        'altText':'問題',
        'contents': 
        {
          "type": "bubble",
          "body": {
            "type": "box",
            "layout": "vertical",
            "contents": [
              {
                "type": "text",
                "text": "問題",
                "weight": "bold",
                "color": "#4F6F52",
                "size": "sm",
                "align": "center"
              },
              {
                "type": "separator",
                "color": "#4F6F52",
              },
              {
                "type": "text",
                "text": message,
                "weight": "bold",
                "size": "lg",
                "margin": "md",
                "wrap": true
              }
            ],
            "backgroundColor": '#E8DFCA'
          },
          "styles": {
            "footer": {
              "separator": true
            }
          }
        }
      }
    ]
  }
  let options = {
    "headers": HEADERS,
    "method": "post",
    "payload": JSON.stringify(postData),
  };
  return options;
}
```
```"type": "box"```、```"layout": "vertical"```で垂直レイアウト方向を定義。
```"type": "text"```でテキストコンポーネントを作成
```"type": "separator"```で分割線を作成
```wrap```プロパティを```true```にすることでテキストを折り返し表示指定
FlexMessageのコンポーネントについてより知りたい方は[こちら](URL"https://developers.line.biz/ja/docs/messaging-api/flex-message-elements/#component")。
FlexMessageのレイアウトについてより知りたい方は[こちら](URL "https://developers.line.biz/ja/docs/messaging-api/flex-message-layout/#other-component-size")。
### 4. 回答履歴の保存
```insertData```によって出題した単語（```answer```）をスプシに保存。（これは回答を出力するときに使用します）
具体的には、管理用スプシにおける解答Userの列を引数である```column```で特定済みなので、あとはその最終行```targetRow```に```answer```を書き込む
```javascript:insertData
//管理用シートの最終行に回答状況を登録する
function insertData(value = '空', column) {
  var Spreadsheet = SpreadsheetApp.openById(SPREADSHEET_ID);
  let sheet = Spreadsheet.getSheetByName(ManegerSpreadsheetName);
  const targetRow = sheet.getRange(1, column).getNextDataCell(SpreadsheetApp.Direction.DOWN).getRow() + 1;
  sheet.getRange(targetRow, column).setValue(value);
}
```
# 次回に続く
https://qiita.com/yugakai72/items/89b96b4a7f8760c3000c

# 参考サイト
https://note.com/koshikake0124/n/n5bb2107fad0f

https://qiita.com/kottyan/items/1674ad370fd4066ba563