勤怠管理のまとめを報告してくれるBOTを作ってみた

- 2017/10/22追記
    - より精度の高い時間設定を行えるように修正しました（APIの使用回数も削減）

# 前提
チャットワークが普及し、勤怠報告をチャットワークで報告する会社も増えてきたかと思います。
ただ、各々が自分のタイミングで発言していたりするので、朝会の時などに勤怠連絡チャットを見返すということがありました。

# できること
- 勤怠連絡チャットの内容を抽出し、同チームのメンバーの勤怠のみをまとめて特定の時間に報告してくれます
- 「了解」「承知」というキーワードが含まれる発言は勤怠連絡としてみなされません（勤怠連絡者への返信を除外）

# 環境
- GoogleAppsScript(通称：GAS)
- スプレッドシート
- チャットワーク

# 配布
- 下記のスプレッドシートをコピーして使って下さい
    - https://docs.google.com/spreadsheets/d/1n0I9vexGs7VXlH_P0E2RAaTVEFQ1sUjMPXf2ORAADJQ/edit?usp=sharing

# 使い方
- スプレッドシートのメンバーリストにチームメンバーのチャットワーク名とチャットワークIDを入力します（チャットワークIDはなくても大丈夫です）
- ツール ⇒ スクリプトエディタを開き、定数部分を自分の環境にあったものに修正して下さい（SCRIPT_NAMEは変更不要です）
    - SPREADSHEET_URL：スプレッドシートのURL
    - CHATWORK_TOKEN：チャットワークのAPIトークン
    - GET_ROOM_ID：各々が勤怠連絡を行うための対象ルームID
    - SEND_ROOM_ID：一日一回送信される勤怠まとめの発言対象ルームID
- ツール ⇒ スクリプトエディタを開き、setTimerTriggerメソッド内の値を任意の時間に設定して下さい
- ツール ⇒ スクリプトエディタを開き、スクリプトの実行タイマー（時計マーク）を下記に設定して下さい（以下は入力例です）
    - 設定時に承認を求められる場合があります
    - dataReset - 時間主導型 - 日タイマー - 午前3時～4時
    - setTimerTrigger - 時間主導型 - 日タイマー - 午前6時～7時

# プログラムの説明（エンジニア向け）
- 勤怠連絡チャットの発言を一括取得します
- 発言内容を精査し、正しい勤怠連絡のみをスプレッドシートに保存します
- 特定の時間に保存された勤怠連絡をまとめて発言します

# ソースコード
```php
// 定数指定
SPREADSHEET_URL = "";
CHATWORK_TOKEN = "";
GET_ROOM_ID = 999999999;
SEND_ROOM_ID = 999999999;

/**
 * その日の所定の時間にタイマーを設定する（※GoogleAppsScriptは定期的なぴったり時間のタイマーを設定することが容易にできないので、プログラム側で制御する）
 * 時間は適時自由に変更して下さい
 */
function setTimerTrigger()
{
    // チャットの発言を取得する時間(毎分取得などに設定した場合API使用量が多くなるので出来る限りAPIの使用量を減らす方針で設計)
    var triggerDay = new Date();
    triggerDay.setHours(10);
    triggerDay.setMinutes(00);
    ScriptApp.newTrigger("getChatWorkTimer").timeBased().at(triggerDay).create();

    // 勤怠のまとめを発言する時間
    triggerDay.setHours(10);
    triggerDay.setMinutes(15);
    ScriptApp.newTrigger("sendAttendDataTimer").timeBased().at(triggerDay).create();
}

/**
 * その日のトリガーを削除する関数(消さないと残る)
 */
function deleteGetChatWorkTimer()
{
    var triggers = ScriptApp.getProjectTriggers();
    for (var i = 0; i < triggers.length; i++) {
        if (triggers[i].getHandlerFunction() == "getChatWorkTimer") {
            ScriptApp.deleteTrigger(triggers[i]);
        }
    }
}

/**
 * その日のトリガーを削除する関数(消さないと残る)
 */
function deleteSendAttendDataTimer()
{
    var triggers = ScriptApp.getProjectTriggers();
    for (var i = 0; i < triggers.length; i++) {
        if (triggers[i].getHandlerFunction() == "sendAttendDataTimer") {
            ScriptApp.deleteTrigger(triggers[i]);
        }
    }
}

/**
 * getChatWorkプログラムを実行するためのタイマー実行時に呼ばれる部分
 */
function getChatWorkTimer()
{
    deleteGetChatWorkTimer();
    getChatWork();
}

/**
 * sendAttendDataプログラムを実行するためのタイマー実行時に呼ばれる部分
 */
function sendAttendDataTimer()
{
    deleteSendAttendDataTimer();
    sendAttendData();
}

/**
 * チャット内容を取得する
 */
function getChatWork()
{
    // チャットワークから発言を取得するためのデータを設定
    var params = {
        headers: {
            "X-ChatWorkToken": CHATWORK_TOKEN
        },
        method: "get"
    };

    // みんなが勤怠報告をする対象のチャットルームID  
    var roomId = GET_ROOM_ID;
    var url = "https://api.chatwork.com/v2/rooms/" + roomId + "/messages";
    var response = UrlFetchApp.fetch(url, params);

    // 取得する発言がなかったら何も行わない
    if (response == "") {
        return 0;
    }

    // JSONにデータを直す
    var jsonList = JSON.parse(response.getContentText("UTF-8"));

    // チャット1件、1件ずつに分けて処理を行う
    for each(json in jsonList) {
        // チャット本文全体ではなく、勤怠連絡などのToを除いた本文のみなので、最終部分の配列を取得する
        var body = json["body"].replace(/\r?\n/g, "");
        var text = body.split("]");
        text = text[text.length - 1];

        // 勤怠連絡に対する返信者の発言は勤怠連絡に当てはまらないので除外する（正規表現で除外）
        if (!(text.match(/承知/) || text.match(/了解/))) {
            // チームメンバーかどうかの判定
            if (isTeamMember(json["account"]["name"]) == true) {
                // 勤怠連絡表に内容をまとめる
                writeTodayAttend(json["account"]["name"], text);
            }
        }
    }
}

/**
 * 当日の勤怠連絡のデータ取得を行う前にリストのクリア及び前日の始業後に送信された勤怠連絡を除外する
 * チャットワークの発言取得APIは前回取得時との差分を取得する仕組みになっている
 */
function dataReset()
{
    // スプレッドシートを操作するための準備
    var url = SPREADSHEET_URL;
    var spreadsheet = SpreadsheetApp.openByUrl(url);
    var sheet = spreadsheet.getSheetByName("本日の勤怠連絡");

    getChatWork();

    // 一旦、シートをクリアにする
    sheet.getRange("A2:B100").clearContent();
}

/**
 * チームメンバーかどうかの判定
 */
function isTeamMember(name)
{
    var url = SPREADSHEET_URL;
    var spreadsheet = SpreadsheetApp.openByUrl(url);
    var sheet = spreadsheet.getSheetByName("メンバーリスト");

    //シートの最終行番号、最終列番号を取得
    var startRow = 1;
    var startColumn = 1;
    var lastRow = sheet.getLastRow();
    var lastColumn = sheet.getLastColumn();

    // シートの縦横すべてを取得
    var sheetData = sheet.getSheetValues(startRow, startColumn, lastRow, lastColumn);

    for (var i = 0; i < lastRow; i++) {
        if (sheetData[i][0] == name) {
            return true;
        }
    }

    return false;
}

/**
 * 勤怠連絡シートに記入する
 */
function writeTodayAttend(name, content)
{
    // スプレッドシートを操作するための準備
    var url = SPREADSHEET_URL;
    var spreadsheet = SpreadsheetApp.openByUrl(url);
    var sheet = spreadsheet.getSheetByName("本日の勤怠連絡");
    var lastRow = sheet.getLastRow();

    sheet.getRange(lastRow + 1, 1).setValue(name);
    sheet.getRange(lastRow + 1, 2).setValue(content);
}

/**
 * 時間になったらチャットワークへ勤怠連絡を発言する
 */
function sendAttendData()
{
    // スプレッドシートを操作するための準備
    var url = SPREADSHEET_URL;
    var spreadsheet = SpreadsheetApp.openByUrl(url);
    var sheet = spreadsheet.getSheetByName("本日の勤怠連絡");

    // 勤怠報告のまとめを発言する対象のチャットルームID
    var roomId = SEND_ROOM_ID;

    //シートの最終行番号、最終列番号を取得
    var startRow = 1;
    var startColumn = 1;
    var lastRow = sheet.getLastRow();
    var lastColumn = sheet.getLastColumn();

    // シートの縦横すべてを取得
    var sheetData = sheet.getSheetValues(startRow, startColumn, lastRow, lastColumn);

    // チャットワークに送る文字列のヘッダー
    var honbun = "[info][title]本日の勤怠連絡まとめ：" + Utilities.formatDate(new Date(), 'JST', 'yyyy/MM/dd') + " (roger)[/title]";

    // イベントの数だけ繰り返し
    for (var i = 1; i < lastRow; i++) {
        honbun += sheetData[i][0] + "\n";
        honbun += sheetData[i][1] + "\n";
        if (!(i == lastRow - 1)) {
            honbun += "[hr]";
        }
    }

    honbun += '[/info]';

    sendMessageToChatWork(roomId, honbun)
}

/**
 * チャットワークへの発言プログラムの本体
 */
function sendMessageToChatWork(roomId, message)
{
    var token = CHATWORK_TOKEN;

    var parameter = {
        headers: {
            "X-ChatWorkToken": token
        },
        method: "post",
        payload: {
            body: message
        }
    };

    // 送信先情報
    var url = "https://api.chatwork.com/v2/rooms/" + roomId + "/messages";

    // メッセージ送信
    UrlFetchApp.fetch(url, parameter);
}
```

# 参考
- チャットワークAPI
    - http://developer.chatwork.com/ja/
- Apps Script
    - https://www.google.com/script/start/
- GASでSpreadsheetを操作する自分的ベストプラクティス
    - https://qiita.com/ryan5500/items/e72eb205fbe006c2eb6f
- 【保存版】Google Apps Scriptリファレンス～キーワード別インデックス
    - https://tonari-it.com/google-apps-script-reference/
- Google Apps Script リファレンス
    - https://developers.google.com/apps-script/reference/calendar/
- 毎朝、当日の予定をGoogleカレンダーから取得してチャットワークに送る
    - https://tonari-it.com/gas-calendar/
- チャットワークへスケジュールをリマインドしてくれるBOT
    - https://qiita.com/yunomi/items/7f6e1ed09c3002585443
- Google Apps Scriptの日毎のトリガーで時間をもっと細かく設定する
    - https://qiita.com/sumi-engraphia/items/465dd027e17f44da4d6a