背景
タイムトラッキングサービスのToggl
大まかに言えば、「ストップウォッチで測った時間がログとして残ることで、時間の使い方を振り返ることができる」サービス。Webアプリやスマホアプリ、Windowsデスクトップアプリ等多くのプラットフォームで利用できる。
遊びと勉強のバランス取りたい的な思惑で、プライベートで利用していた。
(※本来想定されているユースケースは「デスクワーカーが仕事中に利用する」っぽい)
が…。
ボタンを押してストップウォッチを開始するのが面倒臭い。
何をしていた時間かをキーボード入力するのも面倒臭い。
→測らなくなる。
意味がない…
→Google Homeを使って、音声コマンドでストップウォッチを開始・停止できれば計測のハードルが下がるかも?
IFTTTで可能?→不可能
IFTTTのGoogle Assistantトリガーは引数を取ることができるし、TogglはWeb APIを持っているので、トリガーで聞き取った言葉を含めてアクションでAPIを叩けばすぐに実現できる、と初めは考えた。
- IFTTTアクションで送れるリクエストは1つのみ
- リクエストは送り捨てであり、レスポンスは扱えない
- IFTTT アプレット同士は独立で情報の受け渡しは不可能
- タイムエントリ停止APIを叩くにはエントリIDが必要
- エントリIDは開始APIを叩いた際のレスポンスに含まれるが、前述の通り扱う術がない
なんの情報もないところからエントリ停止を行いたい場合、
- 計測中のエントリ情報取得API
- タイムエントリ停止API
の2つを叩く必要がある。
必要なAPI
Start a time entry
https://www.toggl.com/api/v8/time_entries/start
にPOSTする。
Get running time entry
https://www.toggl.com/api/v8/time_entries/current
にGETする。
Stop a time entry
https://www.toggl.com/api/v8/time_entries/{time_entry_id}/stop
にPUTする。
Google Apps Scriptでラッパー作成
- タイムエントリ開始
- 計測中のタイムエントリ停止
ができるラッパーを作成。
※例外処理もなにもない
※おおよそコピペ可能だが、startEntry
関数のoptions
オブジェクトのpayload
プロパティのwidの部分は自分のワークスペースの値に置き換える必要あり。https://toggl.com/app/workspaces で特定のワークスペースの"settings"を選択してワークスペース設定画面に遷移後、URLから確認できる。https://toggl.com/app/workspaces/[ワークスペースID]/settings?
のようになっている。
function doPost(e){
var jsonStr = e.postData.getDataAsString();
var data = JSON.parse(jsonStr);
Logger.log(data);
var authData = Utilities.base64Encode(data.user + ':api_token');
if(data.word != "")
{
var response = startEntry(data.word, authData);
}
else
{
var response = stopEntry(authData);
}
return ContentService.createTextOutput(response);
}
function startEntry(entryName, authData){
var startUrl = "https://www.toggl.com/api/v8/time_entries/start";
var options = {
'method' : 'post',
'headers' : {"Authorization" : "Basic " + authData},
'contentType' : 'application/json',
'payload' : "{\"time_entry\":{\"description\":\"" + entryName + "\",\"wid\":自分のワークスペースID,\"created_with\":\"Google Apps Script\"}}"
}
var response = UrlFetchApp.fetch(startUrl, options);
return response;
}
function stopEntry(authData){
var entry_id = getEntryId(authData);
Logger.log(entry_id);
var response = stopEntryById(entry_id, authData);
return response;
}
function getEntryId(authData){
var currentUrl = "https://www.toggl.com/api/v8/time_entries/current";
var options = {
'headers' : {"Authorization" : "Basic " + authData}
}
var response = UrlFetchApp.fetch(currentUrl, options);
var json = JSON.parse(response.getContentText());
return json.data.id;
}
function stopEntryById(id, authData){
var stopUrl = "https://www.toggl.com/api/v8/time_entries/" + id + "/stop";
var options = {
'method' : 'put',
'headers' : {"Authorization" : "Basic " + authData},
'contentType' : 'application/json'
}
var response = UrlFetchApp.fetch(stopUrl, options);
return response;
}
アプリの公開
GoogleドライブでGoogle Apps Scriptプロジェクトを開き、
- 「公開」→「ウェブアプリケーションとして導入」
- 次のユーザーとしてアプリケーションを実行:自分
- アプリケーションにアクセスできるユーザー:全員(匿名ユーザーを含む)
ここで表示されるURLはIFTTTアプレット作成時に利用する。
ポイント?
Toggl APIの認証はBasic認証であり、
- 認証におけるユーザー名:APIトークン
- 認証におけるパスワード:api_token(固定文字列)
であるという点。
https://www.toggl.com/app/profile
で自分のAPIトークンを確認できる。
IFTTTアプレット作成
開始アプレットThis部 Google Assistantを選択
トリガーを引くためのフレーズ設定等はお好みで
開始アプレットThen部 Webhookを選択
URL欄にはGASを「Webアプリケーションとして導入」したときのURLをコピペして入力
Bodyには以下を入力
{ "user": "自分のAPIトークン", "word": "{{TextField}}" }
{{TextField}}
にはThis部で$
に相当する言葉が入り、設定したフレーズに続けた言葉がタイムエントリのタイトルになる。
停止アプレット
- This部は引数なしの"Say a simple phrase"を選択しお好みで設定
- Then部はBody以外開始アプレットと同様に設定
Bodyには以下を入力
{ "user": "自分のAPIトークン", "word": "" }
※上記のラッパーはJSONのword属性が空かどうかで開始か停止かを判断している。IFTTTアプレットのThis部の設定がまずいと、開始したいのに停止のJSONが送られることがある点には注意。
開始と停止のアプレットを保存すれば、呪文を唱えて開始・停止できるようになる。
ちょっとした問題
タイムエントリはタグやプロジェクト情報を持つことができるが、今回作った仕組みではそこに触れないので、エントリを分別したい場合あとでタグ付け等しないといけない。
余談:なぜ「計測中のエントリを停止」APIが公式にないのか?
「計測中のエントリを停止」APIを用意してくれていれば今回の目的はIFTTTを使うだけで済ませられた。
なぜないのか?
→あるとステートフルになってしまう
「計測中のエントリを停止」APIがあるとすると、クライアント側は今計測中かどうか気にせず叩ける代わりに、サーバー側では状態に応じて振る舞いを変えなければならない(ステートフル)。
Toggl APIの隅々まで検証したわけではないが、REST APIとして設計されているのだとすれば、ステートレスであることが必要になるため「計測中のエントリ取得」「IDを指定して計測停止」の2つのAPIに分かれているのは自然なことである。