チャットワークへスケジュールをリマインドしてくれるBOT

- 2017/10/22追記
    - 終日のスケジュール入力に対応しました（Ver2.0.0へ変更）

# 前提
依頼されたタスクや参加する会議の日程、その他雑務など、仕事をしているとスケジュール管理が重要になってくる場面がたくさんでてくると思います。
自分のスケジュール管理ならできても、それだけではチームの状況は良くならないことも多いので（1人のミスは最終的には全員のミスになるので）、全員が幸せになれるようにリマインダーBOTを作ってみました。

# できること
スケジュールを一括で簡単に登録でき、その時刻になったらチャットワークにTo付きで指定のメンバーにリマインドすることができます

# メリット
- Googleカレンダーでスケジュールのデータを管理しているので非エンジニアでも使いやすい
- Googleカレンダーを使っているので定期スケジュールの登録、スケジュールの途中変更、削除などもしやすい
- リマインドできるグループチャットや対象者をスケジュールごとに設定可能

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

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

# 使い方
- ツール ⇒ スクリプトエディタを開き、定数部分を自分の環境にあったものに修正して下さい（SCRIPT_NAMEは変更不要です）
    - SPREADSHEET_URL：スプレッドシートのURL
    - CHATWORK_TOKEN：チャットワークのAPIトークン
    - CALENDAR_ID：GoogleカレンダーのID（基本的にはGmailアドレスと同じです）
    - ROOM_ID：一日一回送信される全体スケジュールの発言対象ルームID
- ツール ⇒ スクリプトエディタを開き、スクリプトの実行タイマー（時計マーク）を下記に設定して下さい（以下は入力例です）
    - カレンダーとデータを連携する必要性があるため、設定時に承認を求められる場合があります
    - setTodaySchedule - 時間主導型 - 日タイマー - 午前4時～5時
        - この時間にその日のスケジュールを一括でスプレッドシートにコピペしてきます
    - sendSchedule - 時間主導型 - 分タイマー - 5分ごと
    - sendTodaySchedule - 時間主導型 - 日タイマー - 午前8時～9時
- リマインドしたい内容を登録フォームに入力します。入力が終わったらツールバーの「スケジュールBOT」を実行して下さい
    - 終了日時は必要があれば手動で入力して下さい（終了日時を入力しておくとカレンダーが見やすいです）
    - スケジュールは複数同時登録可能です。複数登録する場合は、複数行入力して下さい
    - リマインドは開始日時に設定した時間に送られます
    - 発言対象、対象メンバーは適時VlookSheetを編集して追加して下さい
    - 行の追加は可能ですが、列を追加すると機能が正常に動かなくなります。列追加する場合はスクリプトも修正して下さい
    - 「登録リスト」「デイリー発言リスト」リートは編集しないで下さい
- スケジュールの登録時に時間の部分を0:00で登録すると「終日」のスケジュールになります
- 毎日1回だけ1日の全体スケジュールを特定のルームに送信することができます
- スケジュール設定が「終日」の予定に関してはリマインドは行われません（全体スケジュールの時のみ予定に含まれます）
- 定期的なスケジュール（リマインド）を登録したい場合は1つだけ登録した後、Googleカレンダーを開いて手動で定期登録に切り替えて下さい

# プログラムの説明（エンジニア向け）
- 「登録フォーム」シートに入力してある行を1行ずつ参照し、その内容をカレンダーに登録しています
- GoogleAppsScriptのタイマー機能を使って、早朝に当日分の全スケジュールをデイリー発言リストにコピーしてきます
- GoogleAppsScriptのタイマー機能を使って、一定時間ごとにスケジュールの時間が過ぎていないかどうか判定しています
- スケジュールの時間になったらチャットワークの所定のグループへBOT発言します
- リマインドは1回のみでいいので、発言したかどうかの判定を「デイリー発言リスト」のE列で行っています

# ソースコード
```php
// 定数指定
SPREADSHEET_URL = "";
CHATWORK_TOKEN = "";
CALENDAR_ID = "";
SCRIPT_NAME = "カレンダーへ登録";

// 本日の予定を送るチャットワークのルームID
ROOM_ID = 9999999999;

/**
 * スプレッドシート読み込み時に起動される
 */
function onOpen()
{
    var spreadSheet = SpreadsheetApp.getActiveSpreadsheet();
    var menuFunction = [{
        name: SCRIPT_NAME,
        functionName: "createSchedule"
    }];

    spreadSheet.addMenu("スケジュールBOT", menuFunction);
}

/**
 * 登録リストの内容をカレンダーに登録する
 */
function createSchedule()
{
    // スプレッドシートを操作するための準備
    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 = 1; i < lastRow; i++) {
        if (sheetData[i][0] != "") {
            createEvent(sheetData[i][0], sheetData[i][1], sheetData[i][2], sheetData[i][3], sheetData[i][4], sheetData[i][5]);
            // 連続して作成するとエラーを返すので1秒ウェイト
            Utilities.sleep(1000);
        }
    }
}

/**
 * カレンダー登録の本体
 */
function createEvent(title, startTime, endTime, location, menber, agenda)
{
    var calendar = CalendarApp.getCalendarById(CALENDAR_ID);

    if (HHmm(startTime) == "00:00") {
        calendar.createAllDayEvent(title,
            startTime,
            {
                description: menber + "\n" + agenda,
                location: location
            }
        );
    } else {
        calendar.createEvent(title,
            new Date(startTime),
            new Date(endTime),
            {
                description: menber + "\n" + agenda,
                location: location
            }
        );
    }
}

/**
 * 今日の予定をスプレッドシートにコピーする（毎日1度だけ呼ばれる）
 */
function setTodaySchedule()
{
    // スプレッドシートを操作するための準備
    var url = SPREADSHEET_URL;
    var spreadsheet = SpreadsheetApp.openByUrl(url);
    var sheet = spreadsheet.getSheetByName("デイリー発言リスト");

    //特定のIDのカレンダーを取得
    var calendar = CalendarApp.getCalendarById(CALENDAR_ID);

    //カレンダーの本日のイベントを取得
    var schedules = calendar.getEventsForDay(new Date());

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

    // イベントの数だけ繰り返し
    for (var i = 0; i < schedules.length; i++) {
        var title = schedules[i].getTitle();
        var startTime = schedules[i].getStartTime();
        var location = schedules[i].getLocation();
        var agenda = schedules[i].getDescription();

        sheet.getRange(i + 2, 1).setValue(title);
        sheet.getRange(i + 2, 2).setValue(startTime);
        sheet.getRange(i + 2, 3).setValue(location);
        sheet.getRange(i + 2, 4).setValue(agenda);

        if (HHmm(startTime) == "00:00") {
            sheet.getRange(i + 2, 5).setValue("送信済");
        } else {
            sheet.getRange(i + 2, 5).setValue("未送信");
        }
    }
}

/**
 * 時間になったらチャットワークへ予定を発言する
 */
function sendSchedule()
{
    // スプレッドシートを操作するための準備
    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);

    var nowTime = new Date();
    var honbun = "";

    for (var i = 1; i < lastRow; i++) {
        if (sheet.getRange(i + 1, 5).getValue() == "未送信" && sheet.getRange(i + 1, 2).getValue() <= nowTime) {
            honbun = "";
            honbun += "[info][title]" + sheetData[i][0] + " (roger)[/title]";
            honbun += sheetData[i][3];
            honbun += "[/info]";

            sheet.getRange(i + 1, 5).setValue("送信済");
            sendMessageToChatWork(sheetData[i][2], 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);
}

/**
 * 本日の予定をチャットワークに送る
 */
function sendTodaySchedule()
{
    // 本日の予定を送るチャットワークのルームID
    var roomId = ROOM_ID;

    //特定のIDのカレンダーを取得
    var calendar = CalendarApp.getCalendarById(CALENDAR_ID);

    //カレンダーの本日のイベントを取得
    var schedules = calendar.getEventsForDay(new Date());

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

    // イベントの数だけ繰り返し
    for (var i = 0; i < schedules.length; i++) {
        var strTitle = schedules[i].getTitle();
        var strStart = HHmm(schedules[i].getStartTime());
        var strEnd = HHmm(schedules[i].getEndTime());
        honbun += strStart + ' - ' + strEnd + " " + strTitle + '\n';
    }

    honbun += '[/info]';

    sendMessageToChatWork(roomId, honbun)
}

/**
 * 時刻の表記をHH:mmに変更
 */
function HHmm(str)
{
    return Utilities.formatDate(str, 'JST', 'HH:mm');
}
```

# 参考
- チャットワーク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/