一人でこっそり使ってた仕組みを、他の人にも使ってもらえるようにした話。

#はじめに

　この記事はHameeアドベントカレンダー2019 24日目のものです。
こっそりで一人で使っていた仕組みを、社内の他の人たちにも使ってもらえるようにするために、一手間加えた、という話をします。

#そもそも...
　そもそも、こっそりと何を作って使っていたのか、から話さないといけませんね......
Hameeでは、GoogleCalendarを社員のスケジュール管理に使用しています。僕も毎日ではありませんが、MTGや社内の勉強会などの予定が入っています。その日のスケジュールは朝に把握して、今日はMTGがあるから、この時間までに、これをやろう、みたいなことを考えています。それを、僕は朝起きてからすぐにやりたかったのですが、それはデフォルトではできませんでした。社内のカレンダーを、自分のスマホで確認することはできないからです。

　そこで、GoogleCalendarに入っている自分の予定をGoogleAppsScriptを用いて取得し、それを社内で使用しているChatworkのマイチャットに整形して送信する仕組みを実装しました。これでスマホでChatworkから、毎朝、起きた時に1日のスケジュールを把握することができます、やったね！

　GoogleAppsScriptは、ざっくりいうと、Googleが提供するJavaScriptの実行環境です。
GoogleCalendarやGoogleSpreadSheetをはじめとするGoogleの各種サービスを、JavaScriptで操作することができます。このときは、GoogleCalendarを操作できるように用意されている　[CalendarApp](https://developers.google.com/apps-script/reference/calendar/calendar-app)を用い、calendarIdを指定して自分のカレンダーを取得しています。

カレンダーを取得し、予定の詳細を保持しているCalendarEventから、
・ イベントのタイトル
・ 開始時刻と終了時刻
・ カレンダーイベントへのリンク([ここ](https://qiita.com/ousiu0730/items/19c087c8a1ffc00bba3f)を参考に)
を取得し、

```
[info][title]Today's Your Schedules!  2019/12/23 (Mon) [/title]Title: 予定のテスト
StartTime:14:00
EndTime  :15:00
Location:小田原のどこか
URL: https://www.google.com/calendar/event?eid=
[/info]
```
のような文面を生成して、以下の図のようなメッセージをマイチャットに送信しています。![20191222-203913.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71289/1d4da801-8f8f-ac18-e9dc-97969e97207c.png)

マイチャットへの送信は、ChatWorkが公式で提供している[API](http://developer.chatwork.com/ja/endpoint_rooms.html#GET-rooms)を用いて、以下のようなエンドポイントでマイチャットのルームIDを指定して行なっています。
/rooms/{room_id}/messages

これをGoogleAppsScript(GAS)から実行するのですが、GAS用の(非公式)のライブラリ、[Chatwork Client for Google Apps Script](https://github.com/cw-shibuya/chatwork-client-gas)があったのでそれを使用させていただいています。

ここまでのコードをまとめると、以下のようになります。

```javascript

// sender
function sendChat(message, roomId, client) {
    client.sendMessage({
        room_id: roomId,
        body: message
    });
}
// calendarIdに対応するcalendarからその日の予定を取得する
function getMyEvents(calendarId) {
    var myCal = CalendarApp.getCalendarById(calendarId);
    var date = new Date();
    return myCal.getEventsForDay(date);
}
//全ての予定に対して必要な情報を集めた配列を取得
function getMySchedules(calendarId) {
    var myEvents = getMyEvents(calendarId);
    var mySchedules = [];
    if (myEvents.length === 0) {
        return mySchedules;
    }
    for (var i = 0; i < myEvents.length; i++) {
        var myEvent = myEvents[i];
        var splitEventId = myEvent.getId().split('@');
        var eventURL = "https://www.google.com/calendar/event?eid=" + Utilities.base64Encode(splitEventId[0] + " " + calendarId);
        var startTimeStr = Utilities.formatDate(myEvent.getStartTime(), "JST", "HH:mm");
        var endTimeStr = Utilities.formatDate(myEvent.getEndTime(), "JST", "HH:mm");
        var mySchedule = 'Title: ' + myEvent.getTitle() + '\nStartTime:' + startTimeStr + '\nEndTime  :' + endTimeStr + '\nLocation:' + myEvent.getLocation() + '\n';
        mySchedule += 'URL: ' + eventURL + '\n';
        mySchedules.push(mySchedule);
    }
    return mySchedules;
}
// マイチャットに、1日のスケジュールを配信する
function sendMyTodaysSchedules(roomId, calendarId, chatworkToken) {
    // chatworkに配信するためのclientインスタンス
    var client = ChatWorkClient.factory({ token: chatworkToken });
    // 取得したカレンダーの予定情報
    var mySchedules = getMySchedules(calendarId);
    var date = new Date();
    var dateStr = Utilities.formatDate(date, 'JST', "yyyy/MM/dd (E)");
    var message = '[info][title]Today\'s Your Schedules!  ' + dateStr + ' [/title]';
    if (mySchedules.length === 0) {
        message += '本日の予定は特にありません。';
    }
    for (var i = 0; i < mySchedules.length; i++) {
        message += mySchedules[i];
        message += '\n';
    }
    message += '[/info]';
    sendChat(message, roomId, client);
}
function  main(){
    var roomId = '/* roomId */';
    var calendarId = '/* calendarId */';
    var chatworkToken = '/* chatworkToken */';
    sendMyTodaysSchedules(roomId, calendarId, chatworkToken);
}
```

あとはこれに、トリガーを設定して毎朝送られてくるようにすれば完成です。

# 他の人にも使ってもらえるようにする

ここからが本題です笑 この仕組みを使っているうちに、他の人にも使わせてあげたくなってきました。そこで、どうやったら使わせてあげられるか、もとい、使っていただけるかを考えました。

まず、利用者の登録を、GoogleSpreadSheetで行うようにしました。
このシートに、名前とcalendarId、予定を送ってほしいルームのroomIdを記入してもらいます。これを、朝の決まった時間に読み取って、カレンダーの情報をユーザーごとにTo付けをして送信します。

![image.png](https://qiita-image-store.s3.ap-northeast-1.amazonaws.com/0/71289/abcd95b6-49f0-9ce5-3dca-1c52236bfdac.png)

これをやるにあたって、発生した問題がいくつかありました。

　1つ目は、ChatworkでのTo送信の際のaccount_idの取得方法の変更が必要となった点です。To送信先のaccount_idの取得を、送信先のルームにいるユーザーのaccount_idを取得することで行なっています。マイチャットが送信先になっている場合は、1つのaccount_idしか存在しませんでしたが、他のroomIdも指定できるようにしたので、名前で、送信したいユーザーのaccount_idを検索してあげる必要性が発生しました。今回は新たに、この部分のロジックを実装しています。また、実運用中にルームから検索できないケースがあったので、contactからも検索できるようにしています。

```javascript
// To:[account_id]形式のテキストを出力
function makeTo(name, room_id, client) {
	var account_id = getAccountId(name, client, room_id);
	var message = '[To:' + account_id + ']' + name + '\n';
	return message;
}
// roomから取得できないときは、コンタクトから取得
function getAccountId(name_, client, room_id) {
	if (room_id === '') {
		var account_id = getUserId(name_,room_id,client);
		return account_id;
	}
	try {
		var account_id = getUserId(name_, room_id, client);
	} catch(error) {
		var account_id = getUserIdFromContact(name_, client);
	}
	return account_id;
}
// room_idが示すchatroomのメンバー一覧を取得し、ユーザー名に対するaccount_idを取得する
function getUserId(name_, room_id, client) {
	var members = client.get('/rooms/' + room_id + '/members');
	if (members.length == 1) {
		return members[0].account_id;
	}
	return searchUserId(members, name_);
}
// account_idの取得をcontactから行うように変更
function getUserIdFromContact(name_, client) {
	var members = client.get('/contacts/');
	return searchUserId(members, name_);
}
// 検索部分の共通化
function searchUserId(members, name_){
	var user = members.filter(function (member) {
		name_ = name_.replace(/\s+/g, '');
		name = member['name'].replace(/\s+/g, '');
		return (name.match(name_) != null);
	});
	return user[0].account_id;
}
```

 2つ目は、僕のGoogleCalendarに追加されていないと、CalendarAppの検索対象にならないことです。今は仕方なく、Calendarに手動で追加しています。あとで、根本対応？として、SpreadSheetの変更をトリガーにして、Calendarに追加する仕組みを実装します。(仮にユーザーが100名になったら、僕のカレンダーに100名登録されてしまうのですが...)
今回は、一時対応として、配信に失敗した時にエラーメッセージを表示する仕組みを以下のように実装しています。また、スプレッドシートのセルにtrueが入ったユーザーにだけ、手動で再送信する仕組みも実装しました。(直接scriptEditorから関数を実行します。実装はほとんど下記と同じなので割愛します)

```javascript
    var chatworkToken = '/* chatworkToken */';
    // スプレッドシートのid
    var id = '/* spreadsheetId */';
    var spreadsheet = SpreadsheetApp.openById(id);
    var sheet = spreadsheet.getSheetByName('members');
    var range = sheet.getRange(2, 1, 1000, 4);
    var values = range.getValues();
  
    for (var i = 0; i < values.length; i++) {
        var userVal = values[i];
        if (userVal[0] === '')
            break;
        var name = userVal[0];
        var calendarId = userVal[1];
        var roomId = userVal[2].toString();
        try {
            sendMyTodaysSchedules(roomId, name, calendarId, chatworkToken);
            var date = new Date();
            sheet.getRange('D'+(i+2).toString()).setValue(date);
        }
        catch (error) {
            // エラーをspreadSheetに表示し、再送フラグも追加
            Logger.log(error);
            sheet.getRange('D'+(i+2).toString()).setValue(error);
            sheet.getRange('E'+(i+2).toString()).setValue('true');
        }
    }
```

 3つ目は、土日にも送られてしまうことがあげられます。これは自分一人で使っている時にはあまり気にならなかったのですが、お休みの日にもChatworkにチャットが送られてしまうことに対する罪悪感から対策しています。

```javascript
 // 土日は送らない
 var day = new Date().getDay();
 if (day === 0 | day === 6) {
    Logger.log("土日は送らない");
    return;
 }
```
# おわりに
  ひとりで使っている時には問題にならなかったことも、複数人で使うとなると大きな問題になることがあります。そのような問題を解決して、他の人にも使っていただけるようなものを作ることで、ユーザーの目線に立って考えるいい機会になった、と思います。なによりも自分が作ったものを他の人に使っていただける、というのが結構嬉しいです。会議室、今日どこだっけってなった時にスマホから確認できるとか、想定していない使い方も出てきて面白いです。
ただ、僕のChatworkTokenを使用しているので、毎朝、僕からToでユーザーに自分の予定が送られるという不思議な状況になっています。(あとでBotのアカウントに変えようかな...)
 
ちなみに、現在、僕を含めた7名にこの仕組みは利用されています。ご利用ありがとうございます！