読者です 読者をやめる 読者になる 読者になる

L is Bエンジニアブログ

ビジネス用メッセンジャーdirectのエンジニアによるブログ

LisBエンジニアブログ

ビジネスチャットdirectのエンジニアブログ

directでクイズができる!Googleスプレッドシートを利用してクイズBOTをつくってみた

daab開発者向け

こんにちは、鍋山です。
20代でいられるのもあとわずかです。
この10年で最も身についたスキルを一つだけあげるとしたら「耐久性」でしょうか。

今回はdirectでクイズを出題するBotをつくってみました!
クイズにこだわらないですが、なにか覚えたいことや学習したいことがある場合に使えそうです!

f:id:nabeyama:20151224111637p:plain

完成後の動作イメージですが、縦長ですのでVimeoにてご覧いただければ見やすくなります。
https://vimeo.com/149930867

vimeo.com

directならではのセレクトスタンプを活用して選択式のクイズを出題するBotになります。

出題データを置いておく為に、Googleスプレッドシートを使います。
Googleスプレッドシートを誰でもアクセスできるようにする必要はなく、Googleの特定のアカウントサービスに対してのみ共有することになります。

方法については記事の中で説明していきます。

今回はGoogleスプレッドシートを利用しますが、カスタマイズすることでExcelを利用していくこともできます。

やっていくこと

  • Googleスプレッドシートにクイズ出題データを記述
  • Google Developer Consoleでスプレッドシートへのアクセス設定
  • クイズBotの作成

Googleスプレッドシートにクイズ出題データを記述

まずはクイズデータを用意します。
スプレッドシートの1行目をタイトル行とします。
タイトルの文字には英字を使います。

このタイトル行の文字は、Botからアクセスした際にキーとして使うことになりますので、英字にしておきます。
「select_stamp」などとした場合、「_」が省略されて「selectstamp」をキーとしてアクセスすることとなりますので、ご注意を。
また、大文字も小文字になってしまいます。

f:id:nabeyama:20151224112710p:plain

タイトル行は下記を記述しています。
「question correctAnswer commentary select1 select2 select3 select4 select5 select6 select7 select8 select9」

Google Developer Consoleでスプレッドシートへのアクセス設定

次に、用意したクイズ出題データのスプレッドシートをクイズBotからアクセスできるようにします。
その方法にGoogleの認証機能を利用します。
この認証方法を利用することによって、セキュアなアクセスが実現できます。

まずは Google Cloud Platform にアクセスします。

console.developers.google.com

ここから先にGoogleDevelopverConsoleのキャプチャが複数ありますが、サイドカラムやコンテンツのデザインや位置などは変更される可能性がありますのでご了承ください。

既にいくつかのプロジェクトがある場合は一覧が表示されている画面に移ります。

「プロジェクトを作成」をクリックします。

f:id:nabeyama:20151224114505p:plain

f:id:nabeyama:20151224114621p:plain

プロジェクト名を入力します。

今回は「QuizChatBot」としました。

f:id:nabeyama:20151224114739p:plain

作成すると、次はダッシュボートに戻ります。

f:id:nabeyama:20151224115104p:plain

「GoogleAPIを利用する」をクリックします。 GoogleDriveの利用をする為に、有効化する操作をしていきます。

f:id:nabeyama:20151224115255p:plain

こちらで「GoogleDrive」をクリックするか、検索して探してください。

GoogleDriveを選んだら、次の画面で「APIを有効にする」をクリックします。

f:id:nabeyama:20151224115637p:plain

新しく表示される部分の「認証情報に進む」をクリックします。

f:id:nabeyama:20151224115807p:plain

プロジェクトへの認証情報の追加画面にて「使用するAPI」を「DriveAPI」に、「APIを呼び出す場所」を「ウェブサーバー(node.js、Tomcatなど)」と選択して「必要な認証情報」をクリックします。

f:id:nabeyama:20151224120326p:plain

左サイドカラムにある「認証情報」をクリックし、「新しい認証情報」をクリックしていきます。

f:id:nabeyama:20151224120528p:plain

「サービスアカウントキー」を選択します。

f:id:nabeyama:20151224120656p:plain

ここで「サービスアカウント」「名前」「キーのタイプ」をそれぞれ入力していきます。

「名前」については任意ですが、「サービスアカウントID」については後に利用する為、控えておきます。

「キーのタイプ」はJSONを選択します。

f:id:nabeyama:20151224121625p:plain

作成すると、JSONファイルがダウンロードされます。
これはBotを開発するディレクトリに格納する必要があります。(後述)

f:id:nabeyama:20151224133554p:plain

さて、これで Google Cloud Platform での操作は完了しました。

次にGoogleDriveで先ほど作成したスプレッドシートが格納されているディレクトリを見てみましょう。

f:id:nabeyama:20151224131954p:plain

ファイル名を右クリックして、「共有」をクリックします。

f:id:nabeyama:20151224132200p:plain

「ユーザー」に先ほどの「サービスアカウントID」を入力します。
つまり、このGoogleのアカウントに対してのみ、スプレッドシートの共有をしておくということになります。

このスプレッドシートを誰でもアクセスできるようにする必要はないということですね。

f:id:nabeyama:20151224132832p:plain

f:id:nabeyama:20151224133617p:plain

クイズBotの作成

ここからはローカル環境を使用することを前提に進めていきます。
まずはいつも通りのBotの初期環境構築をします。

daab commandline interfaceを使って1分台でつくってしまいましょう。

blog.lisb.direct

環境ができたら、次に今回使用するnpmパッケージをインストールします。

google-spreadsheetというnpmパッケージを使用しました。

www.npmjs.com

インストールする為に、下記を実行してください。

npm install --save google-spreadsheet

先ほどダウンロードされたJSONファイルを格納します。
これはBot側に持たせておくことによって、スプレッドシートに共有した「サービスアカウントID」を通してアクセスできるようになる、という仕組みです。

f:id:nabeyama:20151224134043p:plain

Botの動作を制御するプログラムを書いていきますが、格納場所は /scripts/ となります。

f:id:nabeyama:20151224140818p:plain

/scripts/ping.coffee は不要となりますので、削除してください。

コードについては下記のようにしました。
サンプルコードとしてご利用ください。

注意としてましては、こちらのコードは完全ではありません。
細かなエラーハンドリングや、挙動や条件に応じた制御まではコードに含めていません。
ECMAScript6 (ES2015) でコーディングしていますので、Node.js v4.0以上の環境が必要となります。

// Description:
//   Utility commands surrounding Hubot uptime.
'use strict';

const GS = require('google-spreadsheet');
const creds = require('../ダウンロードされたJSONファイル名をここに記載してください');
const sheet = new GS('スプレッドシートのID部分(URLのランダムな文字列部分)をここに記載してください');

let quizList = {};


module.exports = (robot) => {

  // 確認の為にPING/PONGは残しておく
  robot.respond(/PING$/i, (res) => res.send('PONG'));
  
  // 自分がトークルームに招待されたタイミングで始める処理
  robot.join((res) => {
    if (!quizList[0]) return;
    
    res.send('こんにちは!クイズをどんどん出していきます!');
    
    sendQuiz(robot, res);
  });
  
  // セレクトスタンプ受信時
  robot.respond('select', (res) => {
    receivedQuiz(robot, res);
    sendQuiz(robot, res);
  });
  
};


// クイズを送信する場合
const sendQuiz = (robot, res) => {
  const roomId = res.message.room;
  
  // 出題中の問題番号
  const sendLastQuizNo = robot.brain.get(roomId);
  
  // 次の問題番号
  const nextQuizNo = (() => {
    // 保存情報がない場合は0問目から出題
    if (!sendLastQuizNo && (sendLastQuizNo !== 0)) return 0;
    
    // 次の問題番号を判定
    const nextNo = Number(sendLastQuizNo) + 1;
    if (nextNo < quizList.length) {
      return nextNo;
    } else {
      return 0;
    }
  })();
  
  // 問題送信
  res.send({
    question: quizList[nextQuizNo].quiz.question,
    options: quizList[nextQuizNo].quiz.options
  });
  
  // 問題番号の保存(トークルームIDと紐付け)
  robot.brain.set(roomId, nextQuizNo);
};



// クイズの返信を受信した場合
const receivedQuiz = (robot, res) => {
  const roomId = res.message.room;
  
  // 最新の出題問題を取得
  const sendLastQuizNo = robot.brain.get(roomId);
  
  if (!sendLastQuizNo && (sendLastQuizNo !== 0)) {
    res.send('出題情報が保存できていないようです。');
    return;
  }
  
  // 返信された問題番号
  const receivedQuizNo = (() => {
    // 返信された問題の表題は最新出題問題と等しいか判定
    if ((!!quizList[sendLastQuizNo]) &&
        (quizList[sendLastQuizNo].quiz.question === res.json.question)) {
      return sendLastQuizNo;
    }
    
    // 返信された問題を探索
    let searchQuizNo = null;
    for (let listNo in quizList) {
      if (quizList[listNo].quiz.question === res.json.question) {
        searchQuizNo = listNo;
        break;
      }
    }
    
    // 返信された問題が見つからなかった場合
    if (!searchQuizNo) {
      return false;
    }
    
    return searchQuizNo;
  })();
  
  
  // 返信された問題
  const quizData = quizList[receivedQuizNo];
  
  // 正解時に送信するスタンプ
  const correct = [{
    stamp_set: '3',
    stamp_index: '1152921507291203575'
  }, {
    stamp_set: '3',
    stamp_index: '1152921507291204347'
  }, {
    stamp_set: '3',
    stamp_index: '1152921507291204250'
  }];
  
  // 不正解時に送信するスタンプ
  const incorrect = [{
    stamp_set: '3',
    stamp_index: '1152921507291203877'
  }, {
    stamp_set: '3',
    stamp_index: '1152921507291204201'
  }, {
    stamp_set: '3',
    stamp_index: '1152921507291204251'
  }];
  
  // 正誤判定とその結果返信
  const shuffleNo = Math.floor(Math.random() * 3);
  if (Number(quizData.correct) === Number(res.json.response + 1)) {
    res.send({
      text: '正解!',
      stamp_set: correct[shuffleNo].stamp_set,
      stamp_index: correct[shuffleNo].stamp_index
    });
    res.send([
      '【解説】',
      quizData.comment
    ].join('\n'));
  } else {
    res.send({
      text: '不正解...',
      stamp_set: incorrect[shuffleNo].stamp_set,
      stamp_index: incorrect[shuffleNo].stamp_index
    });
    res.send([
      `正解は、「${quizData.quiz.options[quizData.correct - 1]}」でした。`,
      '',
      '【解説】',
      quizData.comment
    ].join('\n'));
  }
};


// GoogleSpreadSheetからクイズデータを取得する
const sheetParse = (sheet, creds) => {
  return new Promise((resolve, reject) => {
    let sheetData = [];

    sheet.useServiceAccountAuth(creds, (err) => {
      if (err) {
        console.log(err);
        reject(err);
        return;
      }
      sheet.getInfo((err, sheetInfo) => {
        if (err) {
          console.log(err);
          reject(err);
          return
        }

        const sheet1 = sheetInfo.worksheets[0];
        sheet1.getRows((err, rows) => {
          if (err) {
            console.log(err);
            reject(err);
            return;
          }
          sheetData = rows.map((row) => {
            return {
              quiz: {
                question: row.question,
                options: [
                  row.select1,
                  row.select2,
                  row.select3,
                  row.select4,
                  row.select5,
                  row.select6,
                  row.select7,
                  row.select8,
                  row.select9
                ].filter((select) => (select === 0 || !!select))
              },
              correct: row.correctanswer,
              comment: row.commentary
            };
          });
          resolve(sheetData);
        });
      });
    });
  });
};


// Bot起動時に実行
sheetParse(sheet, creds)
  .then(
    (quizData) => quizList = quizData,
    () => process.exit(0)
  );

これで、クイズBotを招待すると同時にクイズがスタートします!
動作しなかった場合に備えて、PING/PONGの確認用ロジックは残してありますので、Botを招待しても反応がなかった際には、「PING」とメッセージ送信をしてください。

それでは、なにかに応用できそうな予感のするクイズBotをお楽しみください!!