GAS,JavaScript初心者でも作れる、積読管理&解消Slack bot【GAS x Slack x Spreadsheet】

#はじめに
私は書店やamazonで本や雑誌をよく衝動買いしてしまいます。
しかし、「とりあえず買うけど積読してしまい、もはや買ったことすら忘れてしまう。」ということがよくあり、頭を悩ませています。
その理由として、以下の理由があると考えました。
・もっている書籍の管理ができていない。
・あまり興味のない本を勢いで買ってしまっている。

そこで、所持してる書籍+欲しい書籍管理用データベース、そこから定期的にリマインド、レコメンドしてくれるアプリケーションが欲しいと思ったので、Slack bot、通称Tsundokunを作りました。


#実現したいこと
・書籍の管理(スプレッドシート)
スプレッドシート上に以下の内容を管理する。
-書籍ID(1から順に書籍にIDを付与する)
-書籍タイトル
-amazonの商品ページURL
-ステータス(購入済みや読破など)
-データベースへの追加日時

・Slack botへの応答内容をスプレッドシートに追加(GASxSlack)

・週一回、Slack botによるおすすめの提案(GASxSlack)


#必要な前提知識
今回はGASのスクリプトのみを紹介するので、以下の内容は省略しています。
・GAS,JavaScriptの基礎
・GASアプリケーションのデプロイ方法
・outgoing webhook, incoming webhookの設定方法
・利用するチャンネルにslack botを追加方法

#システム概要図
以下の画像が、ざっくりですがシステム概要図になります。

![スクリーンショット 2021-04-24 13.54.40.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569973/7c908ab9-40c6-5bf0-385d-7d6cc7bc750e.png)



具体的にこんな動きをします。
<strong>・欲しい本、もしくは購入した本のamazon商品ページのURLをbotを追加したslackのチャンネルに投稿する。
</strong>→入力を受け付けるとSlack botがこんな感じに応答
![スクリーンショット 2021-04-24 14.30.10.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569973/d7e4b9de-5229-85ec-6519-f3aca8ae222c.png)
→入力されたデータがスプレッドシート上に入力される。
（importXML関数を用いて、amazonのURLから書籍タイトルを取得。)
![スクリーンショット 2021-04-24 14.34.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569973/d575bb35-eca9-7a31-655d-3608ec7c3fcc.png)



<strong>・週一回、slack botがスプレッドシートにある書籍の中からランダムに１冊提案。</strong>
![スクリーンショット 2021-04-24 14.32.18.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569973/dc176085-fb56-cdcb-9a67-17ca21780ebe.png)




<strong>・週一回、slack botが面白い本がないかを聞いてくれる。</strong>
![スクリーンショット 2021-04-24 14.32.39.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569973/7297f001-0625-2c91-95d4-1e81d446b8f5.png)





・コマンドを入力することで様々な機能を利用することができる。
-list(スプレッドシート上の書籍一覧をslack上に出力)
-recommend(スプレッドシート上の書籍一覧よりランダムに1冊をおすすめ)
-help(コマンド一覧を表示)
![スクリーンショット 2021-04-24 14.37.01.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569973/ddd2f628-9110-25c9-f738-6ca67b05b428.png)


#スプレッドシートの準備

GASと連携させるSpreadsheetを新規で用意します。
今回の場合だと以下の画像のようになります。
![スクリーンショット 2021-04-24 14.50.28.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/569973/c817f309-0a08-b966-9f9f-b246e7b8c7ab.png)

各列の1行目にデータの名称を入れておきます。
またA列のIDには1から順に最終行まで数字を入れておきます。

加えてC列の2行目以降にはimportXML関数を入れます。
importXML関数はWEBサイトのデータをURLと条件をいれることで簡単に取得できる関数になります。
例えば2行目に入れる場合、書式は以下の通りです。

>=IMPORTXML(B2,"//*[@id='productTitle']")

これでamazonの商品ページから書籍のタイトルを引っ張ってくることができます。

以上でスプレッドシートの準備完了です！！



<strong>（+αでできたら）</strong>
上記の関数でも十分なのですが、B列にURLが入っていないとその行のC列では「#VALUE」エラーが出てしまうので見栄えが悪いです。
iferror関数で、エラーを吐いてしまう場合には空白を表示するとすることで見た目がすっきりします。
>=iferror(IMPORTXML(B2,"//*[@id='productTitle']"),"")



#スクリプト
スクリプト全文がこちらです。

```javascript:main.gs
//引数に指定したtextをslackにポストする用の関数
function postSlack(text){
    const webhookUrl = "https://hooks.slack.com/services/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
    const options = {
        "method" : "POST",
        "headers" : {"Content-type": "application/json"},
        "payload" : '{"text":"' + text + '"}',
    };
    UrlFetchApp.fetch(webhookUrl, options);
}

//Slackからの入力を受け取る
function doPost(e) {
    if(e.parameter.user_name === "slackbot") return;
    const data = e.parameter.text;
    //入力した内容によってBotの返答を分岐させる。
    switch (true) {
        //URLを含む場合、スプレッドシートに出力
        case /https.*/.test(data):
            postSlack("入力を受け付けたよ！面白そうな本だね！"); 
            //スプレッドシートに内容を出力
            outputToSheet(data);
            break;
        //「list」と入力した場合、showListを呼び出し
        case /list/.test(data):
            showList();
            break;
        //「recommend」と入力した場合、recommendBookを呼び出し
        case /recommend/.test(data):
            recommendBook();
            break;
        //「help」と入力した場合、helpを呼び出し
        case /help/.test(data):
            postSlack("↓コマンドの一覧を表示↓");
            help();
            break;
    }
}

//スプレッドシート用の変数定義
//現在のスプレッドシートを取得
const aBook = SpreadsheetApp.getActiveSpreadsheet();
//対象のシートを取得
const aSheet = aBook.getSheetByName("book_info_DB");
//A列の行数取得。
const rangeData = aSheet.getRange('B:B').getValues();
let lastRow = 0;
for(let i=0; i<rangeData.length; i++){
    if(rangeData[i][0]){
        lastRow = i + 1;
    }
}
//本のタイトル,URL,ステータスの一覧を取得し配列に格納
const bookIdArray = aSheet.getRange(2,1,lastRow-1,1).getValues();
const bookUrlArray = aSheet.getRange(2,2,lastRow-1,1).getValues();
const bookTitleArray = aSheet.getRange(2,3,lastRow-1,1).getValues();
const bookStatusArray = aSheet.getRange(2,4,lastRow-1,1).getValues();

//スプレッドシートにslackでの投稿内容を出力するための関数
function outputToSheet(data) {
    //取得したデータを整形
    const array = data.split(",");
    //取得したデータを整形するための正規表現を定義
    const myRegeXp = "https.*[^>]";
    //URLを整形
    const url = array[0].match(myRegeXp);
    //ステータスを取得
    const status = array[1];
    //投稿日時を取得
    const date = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy-MM-dd');
    //整形した値と入力日時を最終行に出力
    aSheet.getRange(lastRow+1,2).setValue(url);
    aSheet.getRange(lastRow+1,4).setValue(status);
    aSheet.getRange(lastRow+1,5).setValue(date);

    //もしステータスが未入力の場合は「未設定」を入力
    if(aSheet.getRange(lastRow+1,4).getValue() === "") {
        aSheet.getRange(lastRow+1,4).setValue("未設定");
    }
}

//登録している書籍一覧を「書籍名:ステータス」の形式でslack上に表示
function showList() {
    postSlack("リストを表示するね↓");
    for(let i=0; i<bookIdArray.length; i++) {
        postSlack("ID：" + bookIdArray[i] +  "\nTitle：" + bookTitleArray[i] + "\nURL：" + bookUrlArray[i] + "\nStatus：" + bookStatusArray[i]);
    }
}



//一週間に一度、DBの中からランダムで一冊提案する
function recommendBook() {
    //ランダムにおすすめする本のタイトル、URLを取得
    const bookNum = Math.floor( Math.random() * bookIdArray.length);
    const recommendBookId = bookIdArray[bookNum];
    const recommendBookTitle = bookTitleArray[bookNum];
    const recommendBookUrl = bookUrlArray[bookNum];
    //slackに出力用の文章
    const recommendMessage = "こんにちは！\n今週はこの本をを読んでみるのはどう？\nID：" + recommendBookId + "\n" + "Title：" + recommendBookTitle + "\n↓にamazonのURL貼っとくね。\n" + recommendBookUrl;
    postSlack(recommendMessage);
}

//HELP用のコマンドリスト表示
function help(){
    //コマンドリストを配列に格納
    const commandList = [
        {command:'help', description:"Show a list of commands."},
        {command:'list', description:"Show a list of books."},
        {command:'recommend', description:"Recommend a book at random in the list."},
        {command:'[url]', description:"Put information of books to SpreadSheet."}
    ]
    //投稿用にコマンドリストを整形
    postSlack(commandList[0].command + "　　　　　　　　　" + commandList[0].description);
    postSlack(commandList[1].command + "　　　　  　　　 　　" + commandList[1].description);
    postSlack(commandList[2].command + "　　　 　 　" + commandList[2].description);
    postSlack(commandList[3].command + "　　　　 　　　　　" + commandList[3].description);
}

//一週間に一度、最近面白い本があったか聞く
function doMessage(e) { 
    const message = "こんにちは！\n最近何か読みたい本見つけた？";
    postSlack(message);
}

```

#スクリプト解説
ではスクリプトの内容を1ブロックずつ解説していきます。

```javascript:main.gs
//引数に指定したtextをslackにポストする用の関数
function postSlack(text){
    const webhookUrl = "https://hooks.slack.com/services/xxxxxxxxxxxxxxxxxxxxxxxxxxxxxx";
    const options = {
        "method" : "POST",
        "headers" : {"Content-type": "application/json"},
        "payload" : '{"text":"' + text + '"}',
    };
    UrlFetchApp.fetch(webhookUrl, options);
}
```

関数postSlackはbotが関数により生成されたテキストやデータを投稿するための関数です。
のちのちたくさん登場します。

内容としてはwebhookの設定+UrlFetchAppでslackにリクエストを飛ばしています。
webhookURLは個人個人で違うので、↑では伏字にしてます。

<br>


```javascript:main.gs
//Slackからの入力を受け取る
function doPost(e) {
    if(e.parameter.user_name === "slackbot") return;
    const data = e.parameter.text;
    //入力した内容によってBotの返答を分岐させる。
    switch (true) {
        //URLを含む場合、スプレッドシートに出力
        case /https.*/.test(data):
            postSlack("入力を受け付けたよ！面白そうな本だね！"); 
            //スプレッドシートに内容を出力
            outputToSheet(data);
            break;
        //「list」と入力した場合、showListを呼び出し
        case /list/.test(data):
            showList();
            break;
        //「recommend」と入力した場合、recommendBookを呼び出し
        case /recommend/.test(data):
            recommendBook();
            break;
        //「help」と入力した場合、helpを呼び出し
        case /help/.test(data):
            postSlack("↓コマンドの一覧を表示↓");
            help();
            break;
    }
}
```

関数doPostはslackに投稿された内容から各種処理を行うための関数です。
例えば、URLを投稿するとSpreadsheetにデータを入れるみたいな感じです。
caseを使い、入力内容によって呼び出す関数を変えます。
呼び出す関数は後ほど定義します。

<br>



```javascript:main.gs
//スプレッドシート用の変数定義
//現在のスプレッドシートを取得
const aBook = SpreadsheetApp.getActiveSpreadsheet();
//対象のシートを取得
const aSheet = aBook.getSheetByName("book_info_DB");
//A列の行数取得。
const rangeData = aSheet.getRange('B:B').getValues();
let lastRow = 0;
for(let i=0; i<rangeData.length; i++){
    if(rangeData[i][0]){
        lastRow = i + 1;
    }
}
//本のタイトル,URL,ステータスの一覧を取得し配列に格納
const bookIdArray = aSheet.getRange(2,1,lastRow-1,1).getValues();
const bookUrlArray = aSheet.getRange(2,2,lastRow-1,1).getValues();
const bookTitleArray = aSheet.getRange(2,3,lastRow-1,1).getValues();
const bookStatusArray = aSheet.getRange(2,4,lastRow-1,1).getValues();
```

GASでデータのやりとりをするためにスプレッドシート用の変数を定義します。
またタイトル,URL,ステータスを配列に格納するための変数も定義します。

<br>


```javascript:main.gs
//スプレッドシートにslackでの投稿内容を出力するための関数
function outputToSheet(data) {
    //取得したデータを整形
    const array = data.split(",");
    //取得したデータを整形するための正規表現を定義
    const myRegeXp = "https.*[^>]";
    //URLを整形
    const url = array[0].match(myRegeXp);
    //ステータスを取得
    const status = array[1];
    //投稿日時を取得
    const date = Utilities.formatDate(new Date(), 'Asia/Tokyo', 'yyyy-MM-dd');
    //整形した値と入力日時を最終行に出力
    aSheet.getRange(lastRow+1,2).setValue(url);
    aSheet.getRange(lastRow+1,4).setValue(status);
    aSheet.getRange(lastRow+1,5).setValue(date);

    //もしステータスが未入力の場合は「未設定」を入力
    if(aSheet.getRange(lastRow+1,4).getValue() === "") {
        aSheet.getRange(lastRow+1,4).setValue("未設定");
    }
}
```

↑はslack上にURLを投稿したときに発動する関数です。
冒頭で分岐させた関数の一つです。
スプレッドシートにURLとステータス、投稿日時を取得し入れます。

一応、「URL,status」の形で入力すれば、statusに購入済みや読破などのステータスを入れることができます。(statusの入力がない場合は未設定。)
ただその後の変更をslack上で行う実装はしていないので、現状あまり使えません。。。

<br>


```javascript:main.gs
//登録している書籍一覧を「書籍名:ステータス」の形式でslack上に表示
function showList() {
    postSlack("リストを表示するね↓");
    for(let i=0; i<bookIdArray.length; i++) {
        postSlack("ID：" + bookIdArray[i] +  "\nTitle：" + bookTitleArray[i] + "\nURL：" + bookUrlArray[i] + "\nStatus：" + bookStatusArray[i]);
    }
}
```

↑はslack上に「list」と投稿したときに発動する関数です。
こちらも冒頭で分岐させた関数の一つです。
配列に格納されたデータを元にスプレッドシート上にある書籍の一覧をpostSlack関数の引数として取り、slack上に表示します。

<br>


```javascript:main.gs
//一週間に一度、DBの中からランダムで一冊提案する
function recommendBook() {
    //ランダムにおすすめする本のタイトル、URLを取得
    const bookNum = Math.floor( Math.random() * bookIdArray.length);
    const recommendBookId = bookIdArray[bookNum];
    const recommendBookTitle = bookTitleArray[bookNum];
    const recommendBookUrl = bookUrlArray[bookNum];
    //slackに出力用の文章
    const recommendMessage = "こんにちは！\n今週はこの本をを読んでみるのはどう？\nID：" + recommendBookId + "\n" + "Title：" + recommendBookTitle + "\n↓にamazonのURL貼っとくね。\n" + recommendBookUrl;
    postSlack(recommendMessage);
}
```

↑はslack上に「recommend」と投稿したときに発動する関数です。
こちらも冒頭で分岐させた関数の一つです。
配列に格納されたデータの中にあるうちランダムで一冊をpostSlack関数の引数として取り、slack上に表示します。

<br>

```javascript:main.gs

//HELP用のコマンドリスト表示
function help(){
    //コマンドリストを配列に格納
    const commandList = [
        {command:'help', description:"Show a list of commands."},
        {command:'list', description:"Show a list of books."},
        {command:'recommend', description:"Recommend a book at random in the list."},
        {command:'[url]', description:"Put information of books to SpreadSheet."}
    ]
    //投稿用にコマンドリストを整形
    postSlack(commandList[0].command + "　　　　　　　　　" + commandList[0].description);
    postSlack(commandList[1].command + "　　　　  　　　 　　" + commandList[1].description);
    postSlack(commandList[2].command + "　　　 　 　" + commandList[2].description);
    postSlack(commandList[3].command + "　　　　 　　　　　" + commandList[3].description);
}
```

↑はslack上に「help」と投稿したときに発動する関数です。
こちらも冒頭で分岐させた関数の一つです。
配列commandListをpostSlackの引数として取り、コマンド一覧をslack上に表示します。
コマンドリストのdescriptionを揃えるために空白を入れているのですが、うまく揃ってないし、気持ち悪いので治したいです。。。

<br>

```javascript:main.gs
//一週間に一度、最近面白い本があったか聞く
function doMessage(e) { 
    const message = "こんにちは！\n最近何か読みたい本見つけた？";
    postSlack(message);
}
```

↑はbotが一週間に一度、面白い本があったか聞いてくるように設定した関数です。
内容はシンプルで、messageを関数postSlackの引数としてとり、slack上に表示させるというのを一週間に一度行うようにGAS側にトリガーを設定しています。


#おわりに
今回のbotだと、一度入力した内容を修正するにはスプレッドシートを直接いじらないといけないのが自分的にすごくいけてないです。。。
slack上でbotとやりとりすることでスプレッドシートのデータを編集できるようにするのが次の目標です。