[GoogleAppsScript] 赤ちゃんのおむつ交換記録LINE-BOTを作成する

# はじめに
LINE を使用して 赤ちゃんのおむつ交換記録を行うBOTを作成しました。（※大きい方です。）
子供がたびたび便秘になっていたこと、便秘が続いていた日数を忘れてしまうことが多かったことがありましたので、BOTを作成しました。しかし、この記事を執筆時にはすでに子供のおむつは取れていました。（時の流れは速いです...。）

![unk_3.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50563/63521e43-4265-1383-3d6e-51fb00ea9131.png)

![unk_2.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50563/5509a47c-2673-b655-3ae8-e1a357273159.png)

![unk_1.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/50563/2b292ab9-8e8f-5cfe-a4df-7423765420be.png)

機能としては下記の通りです。

- キーワードを入力すると、現在日時が記録されます。
- `履歴`メッセージを入力すると、過去の記録を閲覧できます。
- 便秘が続いていた場合、毎日何日目か通知します。

# 事前準備
[詳しくはこちら](https://qiita.com/yun_bow/items/33879bca8f82d1ac6cfa)

1. GoogleDriveにてGoogleAppsScriptを開いて、以下のスクリプトを実装します。

- `LINE_CHANNEL_TOKEN`にLINE Messaging APIで発行したトークンを指定します。
- `SSID`にスプレッドシートのIDと`SSN_USER`、`SSN_HISTORY`にシート名を指定します。

```JavaScript:main.gs
/**
 * 赤ちゃんのおむつ交換BOT
 */
const LINE_CHANNEL_TOKEN = '*****'; // LINE NOTIFYのアクセストークン
const SSID = '*****';
const SSN_USER = 'user';
const SSN_HISTORY = 'history';
```

# フォロー登録
[詳しくはこちら](https://qiita.com/yun_bow/items/33879bca8f82d1ac6cfa)

# フォロー解除
[詳しくはこちら](https://qiita.com/yun_bow/items/33879bca8f82d1ac6cfa)

# リプライ
メッセージを入力すると`LINE Messaging API`からGASにPOSTが通知されます。GASでは、スプレッドシートにメッセージ入力時刻を保存します。結果を`LINE Messaging API`にPOSTで返します。

```plantuml
@startuml
actor ユーザー
ユーザー -> LINEアプリ: メッセージ入力
activate LINEアプリ
LINEアプリ -> "LINE Messaging API"
deactivate LINEアプリ
activate "LINE Messaging API"
  "LINE Messaging API" -> GoogleAppsScript: ...POST (type: message)
deactivate "LINE Messaging API"
  activate GoogleAppsScript
    GoogleAppsScript -> GoogleAppsScript: スプレッドシートにメッセージ入力時刻を記録
    GoogleAppsScript -> "LINE Messaging API": POST /message/reply
  deactivate GoogleAppsScript
activate "LINE Messaging API"
"LINE Messaging API" -> LINEアプリ
deactivate "LINE Messaging API"
activate LINEアプリ
LINEアプリ -> LINEアプリ: 結果表示
deactivate LINEアプリ
@enduml
```

```JavaScript:main.gs
/**
 * メッセージイベント処理
 * @param {Object} reqEvent 
 */
function executeMessage(reqEvent) {
    let msgList = [];
    let user = getUser(reqEvent.source.userId);
    if (user) {
        let reqReport = -1;
        switch (reqEvent.message.type) {
            case 'text':
                reqReport = getReportFromText(reqEvent.message.text);
                break;
            case 'sticker':
                reqReport = getReporFromSticker(reqEvent.message.packageId, reqEvent.message.stickerId);
                break;
        }

        let nowTime = (new Date()).getTime();

        switch (reqReport) {
            case 0:
                msgList.push({
                    'type': 'text',
                    'text': 'うんこ承りました。',
                });

                if (user.item.lastDate) {
                    let diffTime = nowTime - user.item.lastDate;
                    if (DAY_TIME < diffTime) {
                        let day = Math.floor(diffTime / DAY_TIME);
                        msgList.push({
                            'type': 'text',
                            'text': `${day}日ぶりに出たね。`,
                        });
                    }
                }

                sendLineReply(reqEvent.replyToken, msgList);
                userSheet.getRange(user.index + 2, 4).setValue(nowTime);
                historySheet.appendRow([user.item.userId, nowTime]);
                break;
            case 1:
                let historyList = getHistoryList(reqEvent.source.userId);
                let msg = '直近の記録（最大10件）\n\n';
                for (let i in historyList) {
                    let history = historyList[i];
                    let timestamp = Utilities.formatDate(new Date(history.recortDt), 'Asia/Tokyo', 'MM月dd日 HH:mm');
                    msg += timestamp + '\n';
                }
                msgList.push({
                    'type': 'text',
                    'text': msg,
                });
                sendLineReply(reqEvent.replyToken, msgList);
                break;
            default:
                msgList.push({
                    'type': 'text',
                    'text': 'わからないよ\nもう一度入力してね',
                });
                sendLineReply(reqEvent.replyToken, msgList);
                break;
        }
    }
}

/**
 * テキストからイベント種別を取得する
 * @param {String} text 
 */
function getReportFromText(text) {
    const REPORT_LIST = [
        ['うんこ', 'ウンコ', '出た', 'でた'],
        ['直近', '履歴']
    ];
    for (let i in REPORT_LIST) {
        let itemList = REPORT_LIST[i];
        for (let j in itemList) {
            let item = itemList[j];
            if (text == item) {
                return parseInt(i);
            }
        }
    }
}

/**
 * スタンプからイベント種別を取得する
 * @param {String} packageId 
 * @param {String} stickerId 
 */
function getReporFromSticker(packageId, stickerId) {
    const REPORT_LIST = [
        [{
            packageId: '4',
            stickerId: '284'
        }, {
            packageId: '2000011',
            stickerId: '455795'
        }, {
            packageId: '2000011',
            stickerId: '455798'
        }, {
            packageId: '2000013',
            stickerId: '483667'
        }, {
            packageId: '2000013',
            stickerId: '483668'
        }]
    ];
    for (let i in REPORT_LIST) {
        let itemList = REPORT_LIST[i];
        for (let j in itemList) {
            let item = itemList[j];
            if (item.packageId == packageId && item.stickerId == stickerId) {
                return parseInt(i);
            }
        }
    }
}

/**
 * 履歴一覧を取得する
 * @param {String} userId 
 */
function getHistoryList(userId) {
    let historyList = [];
    let allHistoryList = getAllHistoryList();
    for (let i in allHistoryList) {
        let hitory = allHistoryList[i];
        if (hitory.userId === userId) {
            historyList.push(hitory);
        }
    }
    return historyList.slice(-10);
}

/**
 * 全履歴一覧を取得する
 */
function getAllHistoryList() {
    let allHistoryList = [];
    let lastRow = historySheet.getLastRow();
    if (1 < lastRow) {
        allHistoryList = historySheet.getRange(1, 1, lastRow, 2).getValues();
        allHistoryList = allHistoryList.map((row) => {
            return {
                userId: row[0],
                recortDt: row[1],
            }
        });
    }
    return allHistoryList;
}

/**
 * LINEに応答メッセージを送信する
 * @param {String} replyToken リプライトークン
 * @param {Object} msgList メッセージリスト
 */
function sendLineReply(replyToken, msgList) {
    let url = 'https://api.line.me/v2/bot/message/reply';
    let options = {
        'method': 'post',
        'headers': {
            'Content-Type': 'application/json; charset=UTF-8',
            'Authorization': `Bearer ${LINE_CHANNEL_TOKEN}`
        },
        'payload': JSON.stringify({
            replyToken: replyToken,
            messages: msgList
        })
    };
    let response = UrlFetchApp.fetch(url, options);
    return JSON.parse(response.getContentText('UTF-8'));
}
```
- `execute`関数では、LINE Messaging APIからの通知内容により、処理を振り分けしています。`message`の場合は、メッセージの内容を解析します。メッセージの現在日時を記録します。
- 保存結果メッセージを作成して`sendLineReply`関数でLINE Messaging APIにPOSTします。

# プッシュ通知
GASにて、1日1回、赤ちゃんの便秘が続いていた場合、何日目であるかを通知します。

```plantuml
@startuml
actor ユーザー
  activate GoogleAppsScript
    GoogleAppsScript -> GoogleAppsScript: 1日1回起動
    GoogleAppsScript -> "LINE Messaging API": ...POST /message/push
  deactivate GoogleAppsScript
activate "LINE Messaging API"
"LINE Messaging API" -> LINEアプリ
deactivate "LINE Messaging API"
activate LINEアプリ
LINEアプリ -> LINEアプリ: 何日目を通知
deactivate LINEアプリ
@enduml
```

```JavaScript:main.gs
/**
 * 通知
 */
function notify() {
    try {
        let nowTime = (new Date()).getTime();
        let userList = getUserList();
        for (let i in userList) {
            let user = userList[i];
            if (user.lastDate) {
                let diffTime = nowTime - user.lastDate;
                if (DAY_TIME < diffTime) {
                    let day = Math.floor(diffTime / DAY_TIME);
                    let msgList = [{
                        'type': 'text',
                        'text': `うんこが止まって${day}日目です。`,
                    }];
                    sendLinePush(user.userId, msgList);
                }
            }
        }
    } catch (e) {
        console.error(e.stack);
    }
}

/**
 * LINEにメッセージを送信する
 * @param {String} targetId ターゲットID（userId/groupId/roomId）
 * @param {Object} msgList メッセージリスト
 */
function sendLinePush(targetId, msgList) {
    let url = 'https://api.line.me/v2/bot/message/push';
    let options = {
        'method': 'post',
        'headers': {
            'Content-Type': 'application/json; charset=UTF-8',
            'Authorization': `Bearer ${LINE_CHANNEL_TOKEN}`
        },
        'payload': JSON.stringify({
            to: targetId,
            messages: msgList
        })
    };
    let response = UrlFetchApp.fetch(url, options);
    return JSON.parse(response.getContentText('UTF-8'));
}
```
- 1日1回`notify`関数を実行します。

# 参考リンク
- [Google Homeに話しかけて娘のおむつ交換を記録する](https://qiita.com/rechiba3/items/ef163b0fc21b37e869ef)
- [Google Homeを使って娘のオムツの情報をGoogle Spreadsheetに記録する](https://qiita.com/ahiru_z/items/8385256228625c3a6674)
- [(GAS)スプレッドシートに記録した排泄記録をSlackにポストする](https://qiita.com/mtane0412/items/ac511f2bb0b2089f9a68)

# さいごに
ソースコードをGitHubに公開しています。

- [ソースファイルはこちら](https://github.com/yunbow/GoogleAppsScript-LINE-Constipation)

以上です。