Google Apps Script で TimeCrowd を操作する
勉強時間の管理として、時間管理ツール TimeCrowd を使用している。このサービスには API が用意されているものの、リファレンスや実装例は十分でないのが現状である。ここでは、Google Apps Script (GAS) を用いて TimeCrowd から情報の取得を試みる。
GAS の下準備
- Google Apps Script から新規プロジェクトを作成。
- 公開 > ウェブアプリケーションとして実装 > 導入 と進み、「アプリケーションにアクセスできるユーザー」の部分を「全員(匿名ユーザーを含む)」として「更新」を選択。「現在のウェブアプリケーションの URL」をコピーしておく。
TimeCrowd を用いたアプリケーションの作成
- ブラウザで TimeCrowd にログインしておく。
- こちら からアプリケーション作成フォームに入る。リダイレクト URL には前節 2 でコピーした URL の末尾「exec」を「usercallback」に置換したものを入力する。スコープに関してはドキュメントが見当たらず詳細不明だが、空白のままでも今回は問題なく動作した。
- 「登録」をクリックして表示された画面で、「アプリケーション ID」と「シークレット」をコピーしておく。
GAS の記述
Google Apps Script に下記のスクリプトを記述する。CLIENT_ID
と CLIENT_SECRET
には前節 3 で取得したアプリケーション ID とシークレットを入れる。基本的な流れとしては、このアプリケーションに対して GET リクエストがあった際に関数 doGet
が実行され、API を叩くのに必要なアクセストークンを取得するようになっている。なおリフレッシュトークンのようなものは実装されていないようだが、トークンに有効期限があるのかは不明。
var CLIENT_ID = ''; // Application ID var CLIENT_SECRET = ''; // Secret function doGet(e) { var scriptProperties = PropertiesService.getScriptProperties(); var accessToken = scriptProperties.getProperty('access_token'); if (accessToken == null) { var param = { response_type: 'code', client_id: CLIENT_ID, redirect_uri: getCallbackURL_(), state: ScriptApp.newStateToken() .withMethod('callback') .withArgument('name', 'value') .withTimeout(2000) .createToken(), approval_prompt: 'force' }; var params = []; for (var name in param) params.push(name + '=' + encodeURIComponent(param[name])); var url = 'https://timecrowd.net/oauth/authorize?' + params.join('&'); return HtmlService.createHtmlOutput('<a href="' + url + '" target="_blank">認証</a>'); } return HtmlService.createHtmlOutput('<p>設定済です</p>'); } function getCallbackURL_() { var url = ScriptApp.getService().getUrl(); if (url.indexOf('/exec') >= 0) return url.slice(0, -4) + 'usercallback'; return url.slice(0, -3) + 'usercallback'; } function callback(e) { var credentials = fetchAccessToken_(e.parameter.code); var scriptProperties = PropertiesService.getScriptProperties(); scriptProperties.setProperty('access_token', credentials.access_token); } function fetchAccessToken_(code) { var prop = PropertiesService.getScriptProperties(); var res = UrlFetchApp.fetch('https://timecrowd.net/oauth/token', { method: 'POST', payload: { code: code, client_id: CLIENT_ID, client_secret: CLIENT_SECRET, redirect_uri: getCallbackURL_(), grant_type: 'authorization_code' }, muteHttpExceptions: true }); return JSON.parse(res.getContentText()); }
認証
- TimeCrowd のアプリケーション登録完了画面に戻り、「コールバック URL」横の「認証」ボタンをクリック。
- 新しいタブに表示されたリンク「認証」をクリックし、認証プロセスを経て「スクリプトが完了しましたが、何も返されませんでした。」の画面が表示されれば成功である。
- GAS に戻り、ファイル > プロジェクトのプロパティ > スクリプトのプロパティ で、
access_token
が設定されていることを確認しておく。
API を叩いてみる
以下は TimeCrowd の API を叩いてその日のアクティビティを取得するスクリプトの一例である。
function getTimeCrowdEntries() { var scriptProperties = PropertiesService.getScriptProperties(); var accessToken = scriptProperties.getProperty('access_token'); var requestUrl = 'https://timecrowd.net/api/v1/calendar'; var response = UrlFetchApp.fetch(requestUrl, { method: 'GET', headers: { authorization: 'Bearer ' + accessToken }, contentType: 'application/json', muteHttpExceptions: true }); var json = JSON.parse(response); return json; }
ドキュメントには何の記載もないが、レスポンスの形式は以下のようである。タスクの継続時間は time_entries 以下の配列の「duration」に秒単位で格納されているものと考えられる。
{ "date": "2019-09-01", "time_entries": [ { "id": xxxxxxx, "started_at": "2019-09-01T14:29:00.000+09:00", "stopped_at": "2019-09-01T15:50:00.000+09:00", "time_trackable_id": xxxxxx, "time_trackable_type": "Task", "time_tracker_id": xxxxx, "time_tracker_type": "User", "created_at": "2019-09-01T14:29:26.000+09:00", "updated_at": "2019-09-01T14:41:37.000+09:00", "deleted_at": null, "duration": 4860, "amount": 1350, "team_id": xxxxx, "input_type": "realtime", "stopped_at_seconds": 1567320600, "deleted_at_seconds": 0, "task": { "id": xxxxxx, "key": "免疫膠原病内科", "title": "免疫膠原病内科", "url": "", "team_id": xxxxx, "created_at": "2019-08-29T15:14:52.000+09:00", "updated_at": "2019-09-01T14:41:37.000+09:00", "deleted_at": null, "description": null, "category": { "id": xxxxxx, "key": "医学", "title": "医学", "url": null, "team_id": xxxxx, "created_at": "2019-08-29T14:43:01.000+09:00", "updated_at": "2019-09-01T14:41:37.000+09:00", "deleted_at": null, "description": null, "category": true, "sequential_id": 22, "state": "open", "bill": 0, "ancestry": null, "ancestry_depth": 0, "closed_at": null, "color": 10, "position": 4 }, "sequential_id": 24, "state": "open", "bill": 0, "ancestry": "xxxxxx", "ancestry_depth": 1, "closed_at": null, "color": 1, "position": null, "team": { "id": xxxxx, "name": "個人用", "created_at": "2019-03-03T15:53:42.000+09:00", "updated_at": "2019-09-01T14:41:38.000+09:00", "deleted_at": null, "time_limit": 0, "slack_webhook_url": null, "slack_notification_message": null, "avatar": null, "personal": true, "premium": false, "ancestry": null, "rounding": "floor", "new_design": true, "capacity": null, "hierarchized": false, "default_duration": 15 } }, "user": { "id": xxxxx, "nickname": "hoge", "avatar_url": "https://example.com" } } ], "total": { "hours": 1, "minutes": 21, "duration": 4860 }, "user": { "id": 13085, "nickname": "hoge", "image": "https://example.com", "created_at": "2019-03-03T15:53:42.000+09:00", "updated_at": "2019-09-01T14:41:37.000+09:00", "deleted_at": null, "options": null, "avatar": null, "daily": false, "daily_time": "2000-01-01T17:00:00.000+09:00", "weekly": false, "time_zone": "Tokyo", "locale": "ja", "profile": null, "domain": "timecrowd.net", "reset_password_sent_at": null, "remember_created_at": null, "sign_in_count": 0, "current_sign_in_at": null, "last_sign_in_at": null, "confirmation_token": null, "confirmed_at": null, "confirmation_sent_at": null, "getting_started": true, "team_getting_started": false, "notify_exported": true, "deactivated_at": null } }
参考にしたページ
- GAS での OAuth2 の利用