「LINE Messaging API + Googleスプレッドシート + 猫画像API」による１問１答ではないチャットボットを作りました 

通常のチャットボットは１問１答式ですが、データベースと連携することで１問１答式以外の可能性が広がると思い、試しにこんなものを作りました。
私が若かりし頃（３０年前くらい）のコンパとかの宴会芸のネタの１つですww

##作ったもの
**LINEチャットボット**
「心を読む猫ボット」
![スクリーンショット_iphonexspacegrey_portrait.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291592/c0f53ad0-89df-a8ef-557f-fef33f0e3af6.png)
ちなみに、iPhoneXへのハメコミ画像はこちらで作りました。
https://mockuphone.com/


###何をするチャットボットなの？
数回の質問で、頭の中にある数字をあてます。
つまり、あなたの心を読むことができるボットなのです。
[![Image from Gyazo](https://i.gyazo.com/b1f36b3dc697bd8a6af40c55e8804c8f.png)](https://gyazo.com/b1f36b3dc697bd8a6af40c55e8804c8f)

###なぜ猫なの？
テキストだけでは寂しいですし、老若男女に大人気の猫に便乗して少しでも利用してもらおうという~~姑息な手~~マーケティング手法です。
それに、~~今思い付いたのですが、~~心を読むチャットボットに飽きても、猫画像に癒やされるチャットボットとして長く利用してもらえるという狙いもあるのです。
ちなみにリッチメニュー「タッチしてください」の画像は、我が家の猫ちゃんです。

###本当に人の心が読めるの？
チャットボットが人の心を読むなんて信じないでしょうから、その証拠動画をお見せいたしましょう。
[![Image from Gyazo](https://i.gyazo.com/982f15f0b4a972fbde78b2b5cc2c447d.gif)](https://gyazo.com/982f15f0b4a972fbde78b2b5cc2c447d)


ちなみにこの動画は、[@tkyko13](https://qiita.com/tkyko13)さんに教えてもらった[Gyazo](https://gyazo.com/)というサービスを利用しました。

これで、興味津々になった方は、今すぐこちらのQRコードにスマホのカメラを向けましょう。
<img src="https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291592/576cc5a9-5290-368c-7176-ca66ba0bf5d9.png" width=40%>


## システム
### 環境
- Node 14.5.0


### 構成図

[![Image from Gyazo](https://i.gyazo.com/cabdf78065abb1cc19a8790e78044df5.png)](https://gyazo.com/cabdf78065abb1cc19a8790e78044df5)

ボットが数回質問した内容からユーザーの頭の中にある数字を当てるという仕組みです。

過去のデータの蓄積が必要なのでデータベースとして**Googleスプレッドシート**を利用しました。

プログラムは **node.js**で、**heroku**に構築しました。
node.jsをherokuに構築するには、実行プログラムと同じディレクトリに Procfile を追加するだけで、できました。

```text:Procfile
web: node app.js
```



###使用API
- **LINE Messaging API**<br>
Node.jsでLINE BOTを作る方法はこちらを参考にしました<BR>
[1時間でLINE BOTを作るハンズオン (資料+レポート) in Node学園祭2017](https://qiita.com/n0bisuke/items/ceaa09ef8898bee8369d)<br><br>
- **Google Sheets API**<br>
    過去の情報を保有するためにGoogleスプレッドシートを使いました。
    - Googleスプレッドシートの導入部分はこちらを参考<br>
      [Node.js googleapis npmパッケージで Google スプレッドシートを await/async で読み取るメモ　〜1ft-seabass.jp.MEMO](https://www.1ft-seabass.jp/memo/2020/05/08/npm-googleapis-google-sheet-await-async/) <br><br>
    - データの読み書きはこちらのサイトを参考<br>
[【Node.js + Sheets API v4】Googleスプレッドシートを読み書きする](https://qiita.com/vicugna-pacos/items/f7bb0d97bbaa1371edc8)<BR>
<br>
- [**TheCatAPI**](https://docs.thecatapi.com/)<br>
いろんな猫APIがあります。その中からURLをたたくだけでランダムにネコの画像を取得できるAPIを使用しました。






###なぜ心を読めるの？

超能力です。

というのは冗談で、簡単な算数問題です。
最後の「最初の数を引いてください」ってところがポイントです。

答えはコードの中にもあります。
（コード読むより、普通に考えたほうが簡単ですが・・・）

###コード

```js:app.js
'use strict';
const express = require('express');
const app = express();
const PORT = process.env.PORT || 3000;
const axios = require("axios");
const line = require('@line/bot-sdk');
//LINE Messaging API のいろいろ
const config = {
    channelSecret: 'LINE_CHANNEL_SECRET',
    channelAccessToken: 'LINE_CHANNEL_ACCESS_TOKEN' 
  };

//Googleスプレッドシートを使う設定
let {google} = require('googleapis');
const creds = {
    "type": "service_account",
    "project_id": "PROJECT_ID",
    "private_key_id": "PRIVATE_KEY_ID",
    "private_key": "PRIVATE_KEY",
    "client_email": "CLIENT_EMAIL",
    "client_id": "CLIENT_ID",
    "auth_uri": "AUTH_URI",
    "token_uri": "TOKEN_URI",
    "auth_provider_x509_cert_url": "AUTH_PROVIDER_X509_CERT_URL",
    "client_x509_cert_url": "CLIENT_X509_CERT_URL"
  };
// JSON Web Token(JWT)の設定
let jwtClient = new google.auth.JWT(
    creds.client_email,
    null,
    creds.private_key,
    ['https://www.googleapis.com/auth/spreadsheets',
     'https://www.googleapis.com/auth/drive']
  );
const sheet = 'シートID';
//シートIDとは、スプレッドシートのIDのことで仮にスプレッドシートのURLが以下の場合は
//ABCDEFGABCDEFGABCDEFGABCDEFGABCDEFG の部分です。
//https://docs.google.com/spreadsheets/d/ABCDEFGABCDEFGABCDEFGABCDEFGABCDEFG/edit#gid=0

// スプレッドシートAPIはv4を使う
let sheets = google.sheets('v4');

app.post('/webhook', line.middleware(config), (req, res) => {
console.log(req.body.events);
    //ここのif分はdeveloper consoleの"接続確認"用なので削除して問題ないです。
    if(req.body.events[0].replyToken === '00000000000000000000000000000000' && req.body.events[1].replyToken === 'ffffffffffffffffffffffffffffffff'){
        res.send('Hello LINE BOT!(POST)');
        console.log('疎通確認用');
        return; 
    }
    Promise
      .all(req.body.events.map(handleEvent))
      .then((result) => res.json(result));
});

const client = new line.Client(config);

async function handleEvent(event) {
  //GoogleスプレッドシートのJSON Web Token(JWT) の認証
  let resultJwtClient;
  try {
    resultJwtClient = await jwtClient.authorize();
  } catch (error) {
    console.log("Auth Error: " + error);
  }

  //シートを読み込む
 let responseGetSheet;
 try {
   responseGetSheet =  await sheets.spreadsheets.values.get({
     auth: jwtClient,
     spreadsheetId: sheet,
     range: "シート1",
    });
   } catch (error) {
     console.log('The API returned an error: ' + error);
   }
 //シートから読み込んだデータ
 let data = responseGetSheet.data.values

 let bot_message; //返すセリフ
 let newComerFlag = 1;
 let userIndexNumber; //アクセス者のインデックス

　//アクセス者のインデックスを調べる（初めてかどうかも調べる）
 for (let i = 0; i < data.length ; i++ ) {
   if (data[i][0] == event.source.userId){
       newComerFlag =0;
       userIndexNumber= i ;  
   }
 }
//最初ののアクセスのときの処理
if (newComerFlag == 1){
    // googleスプレッドシートへの書き込み（ユーザーID、回数、数字の初期値）
    bot_message = "数字を１つ思い浮かべてください";
    appendData("シート1!A1",event.source.userId,0,0)
    }
    //最後の１つ前のアクセスのときの処理
    else if (data[userIndexNumber][1] == 4) {
      bot_message = "それに、最初に思った数字を引いてください";
      data[userIndexNumber][1] = Number(data[userIndexNumber][1]) + 1;
      //googleスプレッドシートのデータをアップデート
      updateData("シート1!"+String(userIndexNumber+1)+":"+String(userIndexNumber+1),data[userIndexNumber][0],data[userIndexNumber][1],data[userIndexNumber][2])
    }
    //最後のアクセスの処理
    else if (data[userIndexNumber][1] == 5) {
      bot_message = "今あなたの頭にある数字は "+ data[userIndexNumber][2] + " ですね";
      //googleスプレッドシートの内容を消去する
      updateData("シート1!"+String(userIndexNumber+1)+":"+String(userIndexNumber+1),"","","")  
    }
    else{
     //通常時のアクセスの処理
     let plusMinus;
     let add_mes;
     let random1 = Math.floor( Math.random()*10 )+ 1;  
     if (random1 > 2) {add_mes=" を足してください";plusMinus = 1;}
                   else{add_mes=" を引いてください";plusMinus =- 1;}
     let random2 = Math.floor( Math.random()*10 )+ 1;
     bot_message = "それに " + random2 + add_mes;
     data[userIndexNumber][2] = Number(data[userIndexNumber][2])+ (random2*plusMinus);
     data[userIndexNumber][1] = Number(data[userIndexNumber][1]) + 1;
     //googleスプレッドシート更新（回数、現在の数値）
      updateData("シート1!"+String(userIndexNumber+1)+":"+String(userIndexNumber+1),data[userIndexNumber][0],data[userIndexNumber][1],data[userIndexNumber][2])  
    }

 //猫の写真をAPIからランダムにとってくる
 let cat_picture = await axios.get("https://api.thecatapi.com/v1/images/search");
 let cat_url= cat_picture.data[0].url;
 
 //ボットのメッセージを返す処理
 return client.replyMessage(event.replyToken, {
    "type": "template",
    "altText": "This is a buttons template",
    "template": {
        "type": "buttons",
        "thumbnailImageUrl": cat_url,
        "imageAspectRatio": "rectangle",
        "imageSize": "cover",
        "imageBackgroundColor": "#FFFFFF",
        "title": bot_message,
        "text": " ",
        "defaultAction": {
            "type": "uri",
            "label": "View detail",
            "uri": cat_url
        },
        "actions": [
            {
                "type":"message",
                "label":"OK",
                "text":"OK"
            }
        ]
    }
  });
}

//googleスプレッドシートにデータを追加（行を追加）
async function appendData(range0,value1,value2,value3) {
  let responseAppendSheet;
    try {
      responseAppendSheet = await sheets.spreadsheets.values.append({
      auth: jwtClient,
      spreadsheetId: sheet,
      range: range0,
      valueInputOption: "USER_ENTERED",
      insertDataOption : "INSERT_ROWS",
      resource : {
        values : [[value1,value2,value3]]
      }
    });
  } catch (error) {
     console.log('The API returned an error: ' + error);
  }
}

//googleスプレッドシートのデータをアップデート（上書き）
async function updateData(range0,value1,value2,value3) {
  let responseAppendSheet;
    try {
      responseAppendSheet = await sheets.spreadsheets.values.update({
      auth: jwtClient,
      spreadsheetId: sheet,
      range: range0,
      valueInputOption: "USER_ENTERED",
      resource : {
        values : [[value1,value2,value3]]
      }
    });
  } catch (error) {
      console.log('The API returned an error: ' + error);
  }
}

app.listen(PORT);
console.log(`Server running at ${PORT}`);
```



###フローチャート
上記のコードを大雑把にフローチャートにすると、
![Untitled Diagram.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/291592/2d576bbb-fa41-1314-ca40-82a15e57da86.png)

ちなみに、このフローチャートはこちらで作りました。
draw.io
https://www.draw.io/



##可能性
LINEのチャットボットを外部のデータベースとやりとりできれば、可能性が広がります。
ぱっと浮かんだだけで、以下のようなことができそうなので、気が向いたらチャレンジしてみようと思います。

- しりとり、山手線ゲーム
- ポーカーなどのトランプ
- アドベンチャーゲーム、脱出ゲーム
- LINEから外部の機械やガジェットを操作
- センサーからマイコンを通じて情報取得
- 会話のやりとりをクラウドに保存して機械学習
- Unityと連携したゲームや、メディアアート