Google Apps ScriptでLINEbotで3択クイズを実装する(リッチメニューでクイズモードを実装)

LINEのリッチメニューでクイズを選択するとクイズモードへ移行し、3択クイズが始まるという簡単なアプリケーションを作成した。

![A2F3C7BC-6318-4FD9-8D15-1803D09FDD81.jpg](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/2971002/ab846337-0cd7-8faf-6223-b1295ffe75ed.jpeg)

リッチメニューからクイズのボタンを選択すると「クイズ」というテキストが送られて、それをもとにクイズが送られてくるというものである。

https://nksg.net/lab/blog/linebot-6/

このリンク先のスクリプトを参考にしたが、このリンク先のアプリケーションと異なる点は、以下の２つの点である。

    ・ユーザーIDに問題の進捗を記録させ、進捗に応じて問題を適宜出す
    ・クイズを開始するとステータスを"1"へ変更することでクイズの最中とクイズ中以外で返信するメッセージの内容を変更する

これらを実行するために、以下のようなスクリプトを作成している。

```
// doPost関数: LINEボットがメッセージを受信したときに呼び出される

function doPost(e) {
  const json = JSON.parse(e.postData.contents);
  const event = json.events[0];
  const userId = event.source.userId;

  // postbackイベントの処理
  if (event.type === 'postback') {
    if (checkQuizStatus(userId) !== 0) {
      if (event.postback.data === 'continueQuiz') {
        sendNextQuizQuestion(userId);
      } else if (event.postback.data === 'endQuiz') {
        endQuiz(userId);
      } else {
        handleQuizAnswer(event, SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName('quiz'));
      }
    } else {
        var error_message = [{
          type: 'text',
          text: `エラーが発生しています。\n管理者までお知らせしてください。`
        }];
        pushMessageToUser(userId, error_message);
    }
    return; // postbackイベントの処理を終了
  }

  // メッセージイベントの処理
  if (event.type === 'message' && event.message.type === 'text') {
    const userMessage = event.message.text;

    if (checkQuizStatus(userId) === 0) {
      if (userMessage === 'スポット') {
        replyToUser(event.replyToken, "只今実装に向けて鋭意努力しております。\nもう少しお待ち下さい。");
        //sendSpotRecommendation(event);
      } else if (userMessage === 'スケジュール') {
        replyToUser(event.replyToken, "只今実装に向けて鋭意努力しております。\nもう少しお待ち下さい。");
        //sendSchedule(event);
      } else if (userMessage === 'クイズ') {
        startQuiz(event, userId);
      } else if (userMessage === '天気') {
        replyToUser(event.replyToken, "只今実装に向けて鋭意努力しております。\nもう少しお待ち下さい。");
        //sendWeather(event);
      } else {
        sendDefaultMessage(event);
      }
    } else {
        askContinueQuiz(userId);
    }
  }
}

// replyToUser関数、pushMessageToUser関数、その他の関数も合わせて実装する必要があります


// ステータスを管理する

function checkQuizStatus(userId){
  const userProgressSheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName('statusProgress');
  let userRow = getUserRow(userProgressSheet, userId);
  Logger.log(userRow)

  if (!userRow) {
    userProgressSheet.appendRow([userId, 1, 0]);
    userRow = userProgressSheet.getLastRow();

    return 0;
  }
  return  userRow[1]; // クイズモードであるかを判定
}

// スポットを紹介する関数
function sendSpotRecommendation(event) {
  const replyToken = event.replyToken;
  const message = "おすすめの観光地はこちらです！"; // ここに実際の観光地紹介のロジックを追加
  replyToUser(replyToken, message);
}

// スケジュールを送信する関数
function sendSchedule(event) {
  const replyToken = event.replyToken;
  const message = "こちらが旅行のスケジュールです！"; // ここに実際のスケジュール送信のロジックを追加
  replyToUser(replyToken, message);
}





// デフォルトメッセージを送信する関数
function sendDefaultMessage(event) {
  const replyToken = event.replyToken;
  const message = "只今、クイズの機能を楽しめます。始めたい場合は「クイズ」と送信してください。";
  replyToUser(replyToken, message);
}

// クイズを開始する関数
function startQuiz(event, userId) {
  const replyToken = event.replyToken;
  const userProgressSheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName('statusProgress');
  let userRow = getUserRow(userProgressSheet, userId);
  console.log(userRow)

  if (!userRow) {
    userProgressSheet.appendRow([userId, 1, 1]);
    userRow = userProgressSheet.getLastRow();
    
  } else {
    userProgressSheet.getRange(`C${userRow[0]}`).setValue(1); // クイズモードに設定
  }

  const message = "クイズを始めます！";
  console.log(message);
  replyToUser(replyToken, message);
  sendNextQuizQuestion(userId);
}

function getUserRow(sheet, userId) {
  const data = sheet.getDataRange().getValues();
  for (let i = 0; i < data.length; i++) {
    if (data[i][0] === userId) {
      return [i + 1, data[i][2]];
    }
  }
  return [ ,0];
}

function sendNextQuizQuestion(userId) {
  const quizSheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName('quiz');
  const userProgressSheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName('statusProgress');
  const userRow = getUserRow(userProgressSheet, userId);
  const questionNumber = userProgressSheet.getRange(`B${userRow[0]}`).getValue();

  const question = quizSheet.getRange(`B${questionNumber + 1}`).getValue();
  const choices = ['C', 'D', 'E'].map((col, i) => ({
    type: 'postback',
    label: quizSheet.getRange(`${col}${questionNumber + 1}`).getValue(),
    data: `${col}${questionNumber + 1}`,
    displayText: quizSheet.getRange(`${col}${questionNumber + 1}`).getValue()
  }));

  const message = [
    {
      type: 'template',
      altText: 'クイズ',
      template: {
        type: 'buttons',
        title: 'クイズ',
        text: question,
        actions: choices
      }
    }
  ];

  pushMessageToUser(userId, message);
}

function handleQuizAnswer(event, quizSheet) {
  const answer = event.postback.data.slice(0, 1); // 'C4' -> 'C'
  const rowNumber = Number(event.postback.data.slice(1)); // 'C4' -> 4

  const userId = event.source.userId;
  const userProgressSheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName('statusProgress');
  const userRow = getUserRow(userProgressSheet, userId);
  const currentQuestion = userProgressSheet.getRange(`B${userRow[0]}`).getValue();

 

  let responseMessage = "";
  if (answer === 'C') {
    responseMessage = quizSheet.getRange(`F${rowNumber}`).getValue(); // 正解メッセージ
    replyToUser(event.replyToken, responseMessage);
    userProgressSheet.getRange(`B${userRow[0]}`).setValue(currentQuestion + 1); // 次の問題に進む
    askContinueQuiz(userId);
  } else {
    responseMessage = quizSheet.getRange(`G${rowNumber}`).getValue(); // 不正解メッセージ
    replyToUser(event.replyToken, responseMessage);
  }
}

function askContinueQuiz(userId) {
  const message = [
    {
      type: 'template',
      altText: 'クイズ続けますか？',
      template: {
        type: 'confirm',
        text: 'クイズを続けますか？',
        actions: [
          {
            type: 'postback',
            label: 'はい',
            data: 'continueQuiz',
            displayText: 'はい'
          },
          {
            type: 'postback',
            label: 'いいえ',
            data: 'endQuiz',
            displayText: 'いいえ'
          }
        ]
      }
    }
  ];

  pushMessageToUser(userId, message);
}

function endQuiz(userId) {
  const userProgressSheet = SpreadsheetApp.openById(SPREADSHEET_ID).getSheetByName('statusProgress');
  const userRow = getUserRow(userProgressSheet, userId);
  userProgressSheet.getRange(`C${userRow[0]}`).setValue(0); // クイズモードを終了
  const message = "クイズを終了しました。お疲れ様でした！";
  pushMessageToUser(userId, [{ type: 'text', text: message }]);
}

// LINEに返信メッセージを送信する関数
function replyToUser(replyToken, message) {
  message = addBrightEmojisToMessage(message);
  const url = 'https://api.line.me/v2/bot/message/reply';
  const headers = {
    'Content-Type': 'application/json',
    'Authorization': 'Bearer ' + LINE_ACCESS_TOKEN,
  };
  const postData = {
    'replyToken': replyToken,
    'messages': [{
      'type': 'text',
      'text': message,
    }],
  };
  UrlFetchApp.fetch(url, {
    'method': 'post',
    'headers': headers,
    'payload': JSON.stringify(postData),
  });
}

//LINEにメッセージを送信する

function pushMessageToUser(userId, message) {
  const options = {
    method: 'post',
    headers: {
      'Content-Type': 'application/json',
      'Authorization': `Bearer ${LINE_ACCESS_TOKEN}`
    },
    payload: JSON.stringify({
      to: userId,
      messages: message
    })
  };

  UrlFetchApp.fetch(PUSH_API_URL, options);
}


// 明るいイメージを持つ絵文字のリスト
const brightEmojis = ['🌟', '😊', '✨', '🎉', '🌞', '😃', '👍', '☺', '🎈', '🌈',"❤"];

// 受信メッセージの句点にランダムな絵文字を挿入する関数
function addBrightEmojisToMessage(message) {
  // メッセージを句点で分割
  let parts = message.split('。');
  
  // 各句点の後にランダムな絵文字を挿入
  for (let i = 0; i < parts.length - 1; i++) {
    let randomEmoji = brightEmojis[Math.floor(Math.random() * brightEmojis.length)];
    parts[i] += '。' + randomEmoji;
  }
  
  // 再度メッセージを結合
  return parts.join('');
}
```

実際には、LINEのアクセストークンの定義やスプレッドシートIDの定義がいるがそこは、一番上に定義してくれれば、きれいに実行されるであろう。

とても長いスクリプトになったせいでGASで動作させると返信までに3秒程度時間を要するため結局は実用的ではない。

また、私自身は数ページにスクリプトを分けて管理しているのでこのスクリプトよりはよっぽど見やすい。

まだまだ、このアカウントのサービスを充実させるつもりなので動作はもっさりするだろうが彼女との旅行のためのアカウントなので御愛嬌である。

なにか疑問点があればできる限り答えるのでぜひ送っていただきたい。